diff --git a/.changeset/add-doppler-plugin.md b/.changeset/add-doppler-plugin.md new file mode 100644 index 00000000..e8e38ceb --- /dev/null +++ b/.changeset/add-doppler-plugin.md @@ -0,0 +1,5 @@ +--- +"@varlock/doppler-plugin": minor +--- + +Add Doppler plugin for loading secrets from Doppler projects and configs diff --git a/packages/plugins/doppler/README.md b/packages/plugins/doppler/README.md new file mode 100644 index 00000000..1633c15a --- /dev/null +++ b/packages/plugins/doppler/README.md @@ -0,0 +1,92 @@ +# @varlock/doppler-plugin + +Load secrets from [Doppler](https://www.doppler.com/) into your Varlock configuration. + +## Features + +- ✅ Fetch secrets from Doppler projects and configs +- ✅ Bulk-load secrets with `dopplerBulk()` via `@setValuesBulk` +- ✅ Service token authentication +- ✅ Efficient caching — single API call shared across all secret lookups +- ✅ Multiple plugin instances for different projects/configs +- ✅ Auto-infer secret names from variable names +- ✅ Helpful error messages with resolution tips + +## Installation + +```bash +npm install @varlock/doppler-plugin +``` + +Or load it directly from your `.env.schema` file: + +```env-spec +# @plugin(@varlock/doppler-plugin) +``` + +## Setup + +### 1. Create a Service Token in Doppler + +Navigate to your project config in the Doppler dashboard → **Access** → **Service Tokens** → Generate a token. + +### 2. Configure your `.env.schema` + +```env-spec +# @plugin(@varlock/doppler-plugin) +# @initDoppler( +# project=my-project, +# config=dev, +# serviceToken=$DOPPLER_TOKEN +# ) +# --- + +# @type=dopplerServiceToken @sensitive +DOPPLER_TOKEN= +``` + +## Usage + +### Basic secret fetching + +```env-spec +# Secret name defaults to the config item key +DATABASE_URL=doppler() +API_KEY=doppler() + +# Or explicitly specify the secret name +STRIPE_SECRET=doppler("STRIPE_SECRET_KEY") +``` + +### Multiple instances + +```env-spec +# @initDoppler(id=dev, project=my-app, config=dev, serviceToken=$DEV_DOPPLER_TOKEN) +# @initDoppler(id=prod, project=my-app, config=prd, serviceToken=$PROD_DOPPLER_TOKEN) +# --- + +DEV_DATABASE=doppler(dev, "DATABASE_URL") +PROD_DATABASE=doppler(prod, "DATABASE_URL") +``` + +### Bulk loading secrets + +```env-spec +# @plugin(@varlock/doppler-plugin) +# @initDoppler(project=my-project, config=dev, serviceToken=$DOPPLER_TOKEN) +# @setValuesBulk(dopplerBulk()) +# --- + +# @type=dopplerServiceToken @sensitive +DOPPLER_TOKEN= + +DATABASE_URL= +API_KEY= +REDIS_URL= +``` + +## Resources + +- [Doppler Documentation](https://docs.doppler.com) +- [Service Tokens](https://docs.doppler.com/docs/service-tokens) +- [Doppler API Reference](https://docs.doppler.com/reference) diff --git a/packages/plugins/doppler/package.json b/packages/plugins/doppler/package.json new file mode 100644 index 00000000..3933e343 --- /dev/null +++ b/packages/plugins/doppler/package.json @@ -0,0 +1,55 @@ +{ + "name": "@varlock/doppler-plugin", + "description": "Varlock plugin to load secrets from Doppler", + "version": "0.0.1", + "type": "module", + "homepage": "https://varlock.dev/plugins/doppler/", + "bugs": "https://github.com/dmno-dev/varlock/issues", + "repository": { + "type": "git", + "url": "https://github.com/dmno-dev/varlock.git", + "directory": "packages/plugins/doppler" + }, + "main": "dist/index.js", + "types": "dist/index.d.ts", + "exports": { + "./plugin": "./dist/plugin.cjs" + }, + "files": ["dist"], + "scripts": { + "dev": "tsup --watch", + "build": "tsup", + "test": "vitest", + "typecheck": "tsc --noEmit" + }, + "keywords": [ + "varlock", + "plugin", + "varlock-plugin", + "doppler", + "secrets", + "secret-management", + "env", + ".env", + "dotenv", + "environment variables", + "env vars", + "config" + ], + "author": "dmno-dev", + "license": "MIT", + "engines": { + "node": ">=22" + }, + "peerDependencies": { + "varlock": "workspace:^" + }, + "devDependencies": { + "@env-spec/utils": "workspace:^", + "@types/node": "catalog:", + "ky": "catalog:", + "tsup": "catalog:", + "varlock": "workspace:^", + "vitest": "catalog:" + } +} diff --git a/packages/plugins/doppler/src/plugin.ts b/packages/plugins/doppler/src/plugin.ts new file mode 100644 index 00000000..ac77c384 --- /dev/null +++ b/packages/plugins/doppler/src/plugin.ts @@ -0,0 +1,391 @@ +import { type Resolver, plugin } from 'varlock/plugin-lib'; +import ky from 'ky'; + +const { SchemaError, ResolutionError } = plugin.ERRORS; + +const DOPPLER_ICON = 'simple-icons:doppler'; +const DOPPLER_API_BASE = 'https://api.doppler.com/v3'; + +plugin.name = 'doppler'; +const { debug } = plugin; +debug('init - version =', plugin.version); +plugin.icon = DOPPLER_ICON; +plugin.standardVars = { + initDecorator: '@initDoppler', + params: { + serviceToken: { key: 'DOPPLER_TOKEN', dataType: 'dopplerServiceToken' }, + }, +}; + +class DopplerPluginInstance { + /** Doppler project name */ + private project?: string; + /** Doppler config name (e.g., dev, stg, prd) */ + private config?: string; + /** Service token for API access */ + private serviceToken?: string; + /** Cache for fetched secrets (keyed by project+config) */ + private secretsCache?: Promise>; + + constructor( + readonly id: string, + ) {} + + setAuth( + project: any, + config: any, + serviceToken: any, + ) { + if (project && typeof project === 'string') this.project = project; + if (config && typeof config === 'string') this.config = config; + if (serviceToken && typeof serviceToken === 'string') this.serviceToken = serviceToken; + debug('doppler instance', this.id, 'set auth - project:', project, 'config:', config); + } + + private getAuthHeaders(): Record { + if (!this.serviceToken) { + throw new SchemaError('Doppler service token is required', { + tip: 'Set serviceToken in @initDoppler() decorator', + }); + } + + // Service tokens use Bearer auth + return { + Authorization: `Bearer ${this.serviceToken}`, + }; + } + + /** + * Fetch all secrets for the configured project/config. + * Results are cached so multiple secret lookups share a single API call. + */ + private fetchAllSecrets(): Promise> { + if (this.secretsCache) return this.secretsCache; + + this.secretsCache = this._fetchAllSecrets(); + // Clear cache on failure so retries can try again + this.secretsCache.catch(() => { + this.secretsCache = undefined; + }); + return this.secretsCache; + } + + private async _fetchAllSecrets(): Promise> { + if (!this.project || !this.config) { + throw new ResolutionError('Project and config must be configured'); + } + + const headers = this.getAuthHeaders(); + + try { + debug(`Fetching all secrets for project="${this.project}" config="${this.config}"`); + + const response = await ky.get(`${DOPPLER_API_BASE}/configs/config/secrets`, { + headers, + searchParams: { + project: this.project, + config: this.config, + }, + }).json<{ secrets: Record }>(); + + const result: Record = {}; + for (const [key, value] of Object.entries(response.secrets)) { + result[key] = value.computed ?? value.raw; + } + debug(`Fetched ${Object.keys(result).length} secrets`); + return result; + } catch (err: any) { + return this.handleDopplerError(err, 'list secrets'); + } + } + + async getSecret(secretName: string): Promise { + if (!this.project || !this.config) { + throw new ResolutionError('Project and config must be configured'); + } + + // Use bulk fetch + cache for efficiency — Doppler's API is optimized for this + const secrets = await this.fetchAllSecrets(); + + if (!(secretName in secrets)) { + throw new ResolutionError( + `Secret "${secretName}" not found in project "${this.project}" config "${this.config}"`, + { + tip: [ + 'Check the secret exists in the Doppler dashboard:', + ` https://dashboard.doppler.com/workplace/projects/${this.project}/configs/${this.config}`, + 'Verify the secret name matches exactly (case-sensitive)', + ].join('\n'), + }, + ); + } + + return secrets[secretName]; + } + + async listSecrets(): Promise { + const secrets = await this.fetchAllSecrets(); + return JSON.stringify(secrets); + } + + private handleDopplerError(err: any, operation: string): never { + const errorMsg = err?.message || String(err); + const statusCode = err?.response?.status; + + const location = `project "${this.project}" config "${this.config}"`; + + if (statusCode === 401) { + throw new ResolutionError(`Authentication failed for ${location}`, { + tip: [ + 'Verify your Doppler service token is correct and not expired', + 'Generate a new service token in the Doppler dashboard:', + ` https://dashboard.doppler.com/workplace/projects/${this.project}/configs/${this.config}/access`, + ].join('\n'), + }); + } + + if (statusCode === 403) { + throw new ResolutionError(`Access denied for ${location}`, { + tip: [ + 'Verify your service token has access to this project and config', + 'Service tokens are scoped to a specific config — ensure you are using the correct one', + ].join('\n'), + }); + } + + if (statusCode === 404) { + throw new ResolutionError(`Project or config not found: ${location}`, { + tip: [ + 'Verify the project and config names are correct', + 'Check the Doppler dashboard: https://dashboard.doppler.com', + ].join('\n'), + }); + } + + throw new ResolutionError(`Failed to ${operation} in ${location}: ${errorMsg}`, { + tip: 'Check your network connection and Doppler service status at https://status.doppler.com', + }); + } +} + +const pluginInstances: Record = {}; + +plugin.registerRootDecorator({ + name: 'initDoppler', + description: 'Initialize a Doppler plugin instance for doppler() resolver', + isFunction: true, + async process(argsVal) { + const objArgs = argsVal.objArgs; + if (!objArgs) throw new SchemaError('Expected configuration arguments'); + + // Validate id (must be static if provided) + if (objArgs.id && !objArgs.id.isStatic) { + throw new SchemaError('Expected id to be static'); + } + const id = String(objArgs?.id?.staticValue || '_default'); + + if (pluginInstances[id]) { + throw new SchemaError(`Instance with id "${id}" already initialized`); + } + + // Validate required fields + if (!objArgs.project) { + throw new SchemaError('project is required', { + tip: 'Add project parameter: @initDoppler(project=my-project, ...)', + }); + } + + if (!objArgs.config) { + throw new SchemaError('config is required', { + tip: 'Add config parameter: @initDoppler(config=dev, ...)', + }); + } + + if (!objArgs.serviceToken) { + throw new SchemaError('serviceToken is required', { + tip: 'Add serviceToken parameter: @initDoppler(serviceToken=$DOPPLER_TOKEN, ...)', + }); + } + + // Create instance + pluginInstances[id] = new DopplerPluginInstance(id); + + return { + id, + projectResolver: objArgs.project, + configResolver: objArgs.config, + serviceTokenResolver: objArgs.serviceToken, + }; + }, + async execute({ + id, + projectResolver, + configResolver, + serviceTokenResolver, + }) { + // Even if these are empty, we can't throw errors yet + // in case the instance is never actually used + const project = await projectResolver?.resolve(); + const config = await configResolver?.resolve(); + const serviceToken = await serviceTokenResolver?.resolve(); + + pluginInstances[id].setAuth(project, config, serviceToken); + }, +}); + +plugin.registerDataType({ + name: 'dopplerServiceToken', + sensitive: true, + typeDescription: 'Doppler service token for API access', + icon: DOPPLER_ICON, + docs: [ + { + description: 'Doppler Service Tokens', + url: 'https://docs.doppler.com/docs/service-tokens', + }, + ], +}); + +plugin.registerResolverFunction({ + name: 'doppler', + label: 'Fetch secret value from Doppler', + icon: DOPPLER_ICON, + argsSchema: { + type: 'array', + arrayMinLength: 0, + }, + process() { + let instanceId = '_default'; + let secretNameResolver: Resolver | undefined; + + const argCount = this.arrArgs?.length ?? 0; + + if (argCount === 0) { + // doppler() - use item key as secret name + } else if (argCount === 1) { + // doppler("SECRET_NAME") + secretNameResolver = this.arrArgs![0]; + } else if (argCount === 2) { + // doppler(instanceId, "SECRET_NAME") + if (!this.arrArgs![0].isStatic) { + throw new SchemaError('Expected instance id (first argument) to be a static value'); + } + instanceId = String(this.arrArgs![0].staticValue); + secretNameResolver = this.arrArgs![1]; + } else { + throw new SchemaError('Expected 0-2 arguments'); + } + + // If no secret name provided, get it from the config item key + let itemKey: string | undefined; + if (!secretNameResolver) { + const parent = (this as any).parent; + if (parent && typeof parent.key === 'string') { + itemKey = parent.key; + } else { + throw new SchemaError('When called without arguments, doppler() must be used on a config item', { + tip: 'Either provide a secret name: doppler("SECRET_NAME") or use it on a config item', + }); + } + } + + // Validate instance exists + if (!Object.values(pluginInstances).length) { + throw new SchemaError('No Doppler plugin instances found', { + tip: 'Initialize at least one Doppler plugin instance using @initDoppler() decorator', + }); + } + + const selectedInstance = pluginInstances[instanceId]; + if (!selectedInstance) { + if (instanceId === '_default') { + throw new SchemaError('Doppler plugin instance (without id) not found', { + tip: [ + 'Either remove the `id` param from your @initDoppler call', + 'or use `doppler(id, secretName)` to select an instance by id', + `Available ids: ${Object.keys(pluginInstances).join(', ')}`, + ].join('\n'), + }); + } else { + throw new SchemaError(`Doppler plugin instance id "${instanceId}" not found`, { + tip: `Available ids: ${Object.keys(pluginInstances).join(', ')}`, + }); + } + } + + return { + instanceId, itemKey, secretNameResolver, + }; + }, + async resolve({ + instanceId, itemKey, secretNameResolver, + }) { + const selectedInstance = pluginInstances[instanceId]; + + // Resolve secret name - either from resolver or use item key + let secretName: string; + if (secretNameResolver) { + const resolved = await secretNameResolver.resolve(); + if (typeof resolved !== 'string') { + throw new SchemaError('Expected secret name to resolve to a string'); + } + secretName = resolved; + } else if (itemKey) { + secretName = itemKey; + } else { + throw new SchemaError('No secret name provided'); + } + + const secretValue = await selectedInstance.getSecret(secretName); + return secretValue; + }, +}); + +plugin.registerResolverFunction({ + name: 'dopplerBulk', + label: 'Load all secrets from a Doppler config', + icon: DOPPLER_ICON, + argsSchema: { + type: 'array', + arrayMaxLength: 1, + }, + process() { + // Optional positional arg = instance id + let instanceId = '_default'; + if (this.arrArgs?.length) { + if (!this.arrArgs[0].isStatic) { + throw new SchemaError('Expected instance id to be a static value'); + } + instanceId = String(this.arrArgs[0].staticValue); + } + + if (!Object.values(pluginInstances).length) { + throw new SchemaError('No Doppler plugin instances found', { + tip: 'Initialize at least one Doppler plugin instance using @initDoppler() decorator', + }); + } + + const selectedInstance = pluginInstances[instanceId]; + if (!selectedInstance) { + if (instanceId === '_default') { + throw new SchemaError('Doppler plugin instance (without id) not found', { + tip: [ + 'Either remove the `id` param from your @initDoppler call', + 'or use `dopplerBulk(id)` to select an instance by id', + `Available ids: ${Object.keys(pluginInstances).join(', ')}`, + ].join('\n'), + }); + } else { + throw new SchemaError(`Doppler plugin instance id "${instanceId}" not found`, { + tip: `Available ids: ${Object.keys(pluginInstances).join(', ')}`, + }); + } + } + + return { instanceId }; + }, + async resolve({ instanceId }) { + const selectedInstance = pluginInstances[instanceId]; + return await selectedInstance.listSecrets(); + }, +}); diff --git a/packages/plugins/doppler/tsconfig.json b/packages/plugins/doppler/tsconfig.json new file mode 100644 index 00000000..d69c25fa --- /dev/null +++ b/packages/plugins/doppler/tsconfig.json @@ -0,0 +1,5 @@ +{ + "extends": "@varlock/tsconfig/plugin.tsconfig.json", + "include": ["**/*.ts"], + "exclude": ["node_modules", "dist"] +} diff --git a/packages/plugins/doppler/tsup.config.ts b/packages/plugins/doppler/tsup.config.ts new file mode 100644 index 00000000..a4ffa83a --- /dev/null +++ b/packages/plugins/doppler/tsup.config.ts @@ -0,0 +1,14 @@ +import { defineConfig } from 'tsup'; + +export default defineConfig({ + entry: ['src/plugin.ts'], + dts: true, + sourcemap: true, + treeshake: true, + clean: false, + outDir: 'dist', + format: ['cjs'], + splitting: false, + target: 'esnext', + external: ['varlock'], +}); diff --git a/packages/varlock-website/astro.config.ts b/packages/varlock-website/astro.config.ts index a7011174..d27e768f 100644 --- a/packages/varlock-website/astro.config.ts +++ b/packages/varlock-website/astro.config.ts @@ -185,6 +185,7 @@ export default defineConfig({ { label: 'Azure Key Vault', slug: 'plugins/azure-key-vault' }, { label: 'Bitwarden', slug: 'plugins/bitwarden' }, { label: 'Dashlane', slug: 'plugins/dashlane' }, + { label: 'Doppler', slug: 'plugins/doppler' }, { label: 'GCP Secret Manager', slug: 'plugins/google-secret-manager' }, { label: 'HashiCorp Vault', slug: 'plugins/hashicorp-vault' }, { label: 'Infisical', slug: 'plugins/infisical' }, diff --git a/packages/varlock-website/src/content/docs/plugins/doppler.mdx b/packages/varlock-website/src/content/docs/plugins/doppler.mdx new file mode 100644 index 00000000..514ab177 --- /dev/null +++ b/packages/varlock-website/src/content/docs/plugins/doppler.mdx @@ -0,0 +1,318 @@ +--- +title: Doppler Plugin +description: Using Doppler with Varlock +--- + +import { Steps, Icon } from '@astrojs/starlight/components'; +import Badge from '@/components/Badge.astro'; + +
+ +
+ +Our [Doppler](https://www.doppler.com/) plugin enables secure loading of secrets from Doppler using declarative instructions within your `.env` files. + +The plugin uses service tokens for programmatic access to your Doppler secrets, making it suitable for both local development and production environments. + +## Features + +- **Fetch secrets** from Doppler projects and configs +- **Bulk-load secrets** with `dopplerBulk()` via `@setValuesBulk` +- **Service token authentication** for secure, scoped API access +- **Efficient caching** — a single API call is shared across all secret lookups in the same config +- **Multiple plugin instances** for different projects/configs +- **Auto-infer secret names** from variable names for convenience +- **Helpful error messages** with resolution tips + +## Installation and setup + +In a JS/TS project, you may install the `@varlock/doppler-plugin` package as a normal dependency. +Otherwise you can just load it directly from your `.env.schema` file, as long as you add a version specifier. +See the [plugins guide](/guides/plugins/#installation) for more instructions on installing plugins. + +```env-spec title=".env.schema" +# 1. Load the plugin +# @plugin(@varlock/doppler-plugin) +# +# 2. Initialize the plugin - see below for more details on options +# @initDoppler( +# project=my-project, +# config=dev, +# serviceToken=$DOPPLER_TOKEN +# ) +# --- + +# 3. Add your service token +# @type=dopplerServiceToken @sensitive +DOPPLER_TOKEN= +``` + +### Service token setup + + +1. **Navigate to your Doppler project config** + + Go to the [Doppler dashboard](https://dashboard.doppler.com), select your project, and open the config (e.g., `dev`, `stg`, `prd`) you want to access. + +2. **Generate a service token** + + Click on **Access** → **Service Tokens** → **Generate Service Token**. Give it a descriptive name. + +3. **Save the token** (displayed only once!) + + Copy the service token immediately — it will only be displayed once. + + :::caution[Save token securely] + Store the service token securely. You won't be able to see it again after this step! + ::: + +4. **Wire up the token in your config** + + ```env-spec title=".env.schema" + # @plugin(@varlock/doppler-plugin) + # @initDoppler( + # project=my-project, + # config=dev, + # serviceToken=$DOPPLER_TOKEN + # ) + # --- + + # @type=dopplerServiceToken @sensitive + DOPPLER_TOKEN= + ``` + +5. **Set the token in your environment** + + Use your CI/CD system or platform's env var management to securely inject the `DOPPLER_TOKEN` value. + + +For detailed instructions, see [Doppler Service Tokens documentation](https://docs.doppler.com/docs/service-tokens). + +### Multiple instances + +If you need to connect to multiple projects or configs, register multiple named instances: + +```env-spec title=".env.schema" +# @initDoppler(id=dev, project=my-app, config=dev, serviceToken=$DEV_DOPPLER_TOKEN) +# @initDoppler(id=prod, project=my-app, config=prd, serviceToken=$PROD_DOPPLER_TOKEN) +# --- + +DEV_DATABASE=doppler(dev, "DATABASE_URL") +PROD_DATABASE=doppler(prod, "DATABASE_URL") +``` + +## Loading secrets + +Once the plugin is installed and initialized, you can start adding config items that load values using the `doppler()` resolver function. + +### Basic usage + +Fetch secrets from Doppler: + +```env-spec title=".env.schema" +# Secret name defaults to the config item key +DATABASE_URL=doppler() +API_KEY=doppler() + +# Or explicitly specify the secret name +STRIPE_SECRET=doppler("STRIPE_SECRET_KEY") +``` + +When called without arguments, `doppler()` automatically uses the config item key as the secret name in Doppler. This provides a convenient convention-over-configuration approach. + +### Using a named instance + +```env-spec title=".env.schema" +# @initDoppler(id=backend, project=backend-app, config=dev, serviceToken=$BACKEND_TOKEN) +# --- + +DB_HOST=doppler(backend, "DB_HOST") +DB_PASSWORD=doppler(backend, "DB_PASSWORD") +``` + +### Bulk loading secrets + +Use `dopplerBulk()` with `@setValuesBulk` to load all secrets from a Doppler config at once, instead of wiring up each secret individually: + +```env-spec title=".env.schema" +# @plugin(@varlock/doppler-plugin) +# @initDoppler(project=my-project, config=dev, serviceToken=$DOPPLER_TOKEN) +# @setValuesBulk(dopplerBulk()) +# --- +# @type=dopplerServiceToken @sensitive +DOPPLER_TOKEN= + +API_KEY= +DB_PASSWORD= +REDIS_URL= +``` + +With a named instance: + +```env-spec title=".env.schema" +# @setValuesBulk(dopplerBulk(prod)) +``` + +--- + +## Reference + +### Root decorators +
+
+#### `@initDoppler()` + +Initialize a Doppler plugin instance for accessing secrets. + +**Key/value args:** +- `project` (required): Doppler project name +- `config` (required): Config name (e.g., `dev`, `stg`, `prd`, or a branch config) +- `serviceToken` (required): Doppler service token. Should be a reference to a config item of type `dopplerServiceToken`. +- `id` (optional): Instance identifier for multiple instances + +```env-spec "@initDoppler" +# @initDoppler( +# project=my-project, +# config=dev, +# serviceToken=$DOPPLER_TOKEN +# ) +# --- +# @type=dopplerServiceToken @sensitive +DOPPLER_TOKEN= +``` +
+
+ +### Data types +
+
+#### `dopplerServiceToken` + +Represents a Doppler service token. This type is marked as `@sensitive`. + +```env-spec "dopplerServiceToken" +# @type=dopplerServiceToken @sensitive +DOPPLER_TOKEN= +``` +
+
+ +### Resolver functions +
+
+#### `doppler()` + +Fetch a secret from Doppler. + +**Array args:** +- `instanceId` (optional): instance identifier to use when multiple plugin instances are initialized +- `secretName` (optional): secret name in Doppler. If omitted, uses the variable name. + +```env-spec /doppler\\(.*\\)/ +# Auto-infer secret name from variable +DATABASE_URL=doppler() + +# Explicit secret name +STRIPE_KEY=doppler("STRIPE_SECRET_KEY") + +# With instance ID +DEV_SECRET=doppler(dev, "DATABASE_URL") +``` +
+ +
+#### `dopplerBulk()` + +Bulk-load all secrets from a Doppler config. Intended for use with `@setValuesBulk`. + +**Array args:** +- `instanceId` (optional): instance identifier to use when multiple plugin instances are initialized + +```env-spec /dopplerBulk\\(.*\\)/ +# Load all secrets from default instance +# @setValuesBulk(dopplerBulk()) + +# With instance ID +# @setValuesBulk(dopplerBulk(prod)) +``` +
+
+ +--- + +## Example Configurations + +### Development setup with auto-named secrets + +```env-spec title=".env.schema" +# @plugin(@varlock/doppler-plugin) +# @initDoppler(project=my-app, config=dev, serviceToken=$DOPPLER_TOKEN) +# --- +# @type=dopplerServiceToken @sensitive +DOPPLER_TOKEN= + +# Secret names automatically match config keys +DATABASE_URL=doppler() +REDIS_URL=doppler() +STRIPE_KEY=doppler() +``` + +### Multi-environment setup + +```env-spec title=".env.schema" +# @plugin(@varlock/doppler-plugin) +# @initDoppler(id=dev, project=my-app, config=dev, serviceToken=$DEV_DOPPLER_TOKEN) +# @initDoppler(id=staging, project=my-app, config=stg, serviceToken=$STG_DOPPLER_TOKEN) +# @initDoppler(id=prod, project=my-app, config=prd, serviceToken=$PROD_DOPPLER_TOKEN) +# --- + +DEV_DATABASE=doppler(dev, "DATABASE_URL") +STAGING_DATABASE=doppler(staging, "DATABASE_URL") +PROD_DATABASE=doppler(prod, "DATABASE_URL") +``` + +### Bulk loading for simple setups + +```env-spec title=".env.schema" +# @plugin(@varlock/doppler-plugin) +# @initDoppler(project=my-app, config=dev, serviceToken=$DOPPLER_TOKEN) +# @setValuesBulk(dopplerBulk()) +# --- +# @type=dopplerServiceToken @sensitive +DOPPLER_TOKEN= + +# These will be populated from Doppler secrets with matching names +DATABASE_URL= +API_KEY= +STRIPE_SECRET_KEY= +SENDGRID_API_KEY= +``` + +--- + +## Troubleshooting + +### Secret not found +- Verify the secret exists in your Doppler project config +- Check the secret name matches exactly (case-sensitive) +- Ensure you're looking at the correct config (dev vs stg vs prd) + +### Authentication failed +- Verify the service token is correct and not expired +- Generate a new service token from the Doppler dashboard +- Check that the service token has access to the requested project/config + +### Access denied +- Service tokens are scoped to a specific config — ensure you're using the right one +- Verify the token hasn't been revoked + +### Wrong config +- Double-check the `config` parameter matches the Doppler config where your secrets are stored +- Remember Doppler configs are hierarchical (root → development/staging/production → branch configs) + +## Resources + +- [Doppler Documentation](https://docs.doppler.com) +- [Service Tokens](https://docs.doppler.com/docs/service-tokens) +- [Doppler API Reference](https://docs.doppler.com/reference) +- [Projects and Configs](https://docs.doppler.com/docs/enclave-project-setup) diff --git a/packages/varlock-website/src/content/docs/plugins/overview.mdx b/packages/varlock-website/src/content/docs/plugins/overview.mdx index b26ab729..0035183f 100644 --- a/packages/varlock-website/src/content/docs/plugins/overview.mdx +++ b/packages/varlock-website/src/content/docs/plugins/overview.mdx @@ -18,6 +18,7 @@ For now, only official Varlock plugins under the `@varlock` npm scope are suppor | [Azure Key Vault](/plugins/azure-key-vault/) | | | [Bitwarden](/plugins/bitwarden/) | | | [Dashlane](/plugins/dashlane/)
_CLI-backed Dashlane password manager_ | | +| [Doppler](/plugins/doppler/) | | | [Google Secrets Manager](/plugins/google-secret-manager/) | | | [HashiCorp Vault](/plugins/hashicorp-vault/)
_also compatible with OpenBao_ | | | [Infisical](/plugins/infisical/) | |