Doppler's config secrets → the Naive vault primitive
doppler run -- <cmd>. It does infra-config secrets well — versioning, environment
branching, syncs to 30+ platforms. But when the thing holding secrets is an agent acting on
behalf of many end-users, a Doppler config is a disconnected vendor store:
- Secrets live behind a service token (
dp.st.<config>...) scoped to one config — a single shared bucket, not per-end-user. To isolate secrets per customer you build the mapping yourself (one config per user, naming conventions, or your own KV on top). - Every secret in a config is readable by any code holding the token. There is no write-only secret — nothing an agent can use but never read back.
- “Which of this agent’s secrets belong to which end-user, and who let it read them?” is answered in Doppler for secrets, and in unrelated systems for the agent’s cards, email, and KYC. The identity that owns the secret is a config, not the end-user the agent serves.
vault primitive gives the agent the same capability — an
envelope-encrypted key/value store it reads at execution time — but every entry is rooted in one
governed identity:
- The tenant user whose vault holds a secret is the same user that owns its cards, its email inboxes, its phone number, and its KYC.
- Each value is envelope-encrypted with a managed KMS under an encryption context bound to
{ company_id, tenant_user_id }, so sibling tenants should not be able to read each other’s secrets — you never build the per-user isolation. - Entries can be
locked(stored but never revealed), expiring (expires_at), and whether the agent may touch the vault at all is decided by that user’s Account Kit at execution time.
Doppler is a trademark of its owner, used here for identification only. No endorsement or affiliation is implied.
Tested against: the Doppler Node SDK
@dopplerhq/node-sdk against the Doppler
API v3 (base https://api.doppler.com, /v3/configs/config/secret(s) endpoints, docs snapshot
June 2026), and the Naive Node SDK @usenaive-sdk/server against the Naive API (base
https://api.usenaive.ai/v1, docs snapshot June 2026).Version notes:- Scope model differs. Doppler scopes secrets by project + config and authenticates with a
token bound to a config. Naive scopes by tenant user —
naive.forUser(id)— and the same key reaches every one of that user’s primitives. - Storage shape is a near-rename. Doppler
secrets.update({ secrets: { KEY: value } })→ Naivevault.put(key, value). Dopplersecrets.get(...).value.computed→ Naivevault.reveal(key).value. The core store/read path is close to a one-to-one swap. rotatemeans different things. Doppler rotates the secret value (rotated/dynamic secrets). Naivevault.rotatere-wraps the encryption key — the stored value is unchanged. Do not treat these as equivalent (see what doesn’t map yet).- No equivalent yet for Doppler’s env injection (
doppler run), config inheritance/branching, integrations/syncs, dynamic secrets, or secret versioning/rollback — see what doesn’t map yet.
Concept map
Before / after: the core path
The path that matters for almost every agent is store a secret, then read it back when calling the downstream service. Here it is on both platforms.- The scope is an identity, not a config. In Doppler the token scopes a config; to serve many
users you build the per-user split. In Naive
forUser(id)is the split — one line, isolated end to end, and the same handle owns the user’s cards, email, phone, and KYC. revealis a POST. Naive returns the secret in the response body, never in a URL — so it can’t land in a proxy log or browser history.kindclassifies the entry.api_key,password,cookie,token,reference, ornote(default). It’s a label for the dashboard/logs, not a behavior change.- Values come back masked in
list.vault.list()returns keys with••••••••placeholders and omits expired entries; onlyrevealdecrypts.
Minimal viable migration
The smallest swap that keeps a working agent running is store + read + delete.1
Install the SDK and set your key
NAIVE_API_KEY (a server-side key from the dashboard).2
Pick the identity the secret belongs to
Replace
new DopplerSDK({ accessToken }) + your project/config selection with
naive.forUser(id), where id is the end-user (or your own default user) the secret is for.
There is no config to choose — the user is the scope.3
Swap writes
Map
secrets.update({ secrets: { KEY: value } }) → vault.put(key, value, { kind }). put is
idempotent on the key, so a re-store overwrites in place. Prefer lowercase, dotted keys
(instantly.api_key) — Naive keys are renameable and not case-transformed.4
Swap reads
Map
secrets.get(project, config, name).value.computed → vault.reveal(key) (returns
{ key, value, expires_at }), and secrets.list(...) → vault.list() (values masked). Reveal
over POST, not GET.5
Swap deletes
Map
secrets.delete(project, config, name) → vault.delete(key).6
Ship it
At this point the agent’s store/read path is off Doppler. Everything below is upside, not a
requirement.
Consolidate further once you’re on Naive
This is where the migration pays for itself. In Doppler, a service token isolates a config and nothing else, and every secret in it is readable by whatever holds the token. On Naive, the unit of isolation is a tenant user, and the vault is one primitive on an identity that also owns the agent’s cards, email, and phone.Gain #1 — one identity across primitives
- With Doppler, the agent’s secrets are an island behind a config token. With Naive,
naive.forUser(acme.id)is a single handle to vault and cards and email and phone and KYC. - Each value is envelope-encrypted with a managed KMS under an encryption context bound to
{ company_id, tenant_user_id }— sibling tenants cannot read each other’s secrets, cards, or inboxes, and you never wrote the isolation. - Third-party connections (OAuth grants) surface alongside vault entries in one per-user credential view — the SaaS keys the agent generated and the OAuth apps it connected, one identity.
Gain #2 — execution-time permission enforcement
- Whether an agent may touch the vault at all is the
vaultprimitive on the user’s Account Kit, decided at execution time — not a token scope you manage separately.
- The agent’s code is identical for every tier —
client.vault.reveal("instantly.api_key"). Whether it runs is decided by policy: a user whose Account Kit does not enablevaultis refused withforbidden, with no code change on your side. - Two enforcement levers Doppler has no equivalent for:
lockedentries — stored and used indirectly, butrevealis typically refused (forbiddenper the vault API). A leaked agent process still should not be able to read the raw value back out.expires_at— an entry drops out oflistandrevealtypically returns not found once expired, reducing replay of stale session cookies or short-lived tokens.
Unlike money-moving primitives (cards, trading, domains), vault reads are not frozen for human
approval by default — the enforcement model is enable/disable the primitive, lock an entry, or
expire it, not an approval queue. If you need a human in the loop before a secret is used, gate the
downstream primitive that consumes it instead.
Gain #3 — unified accountability
- Every
put,reveal, androtatefor a customer lands in one per-user activity log — alongside their card, email, phone, and KYC events, not in a separate Doppler activity feed:
- That is the question that is hard to answer when secrets live in Doppler, cards live in Stripe, and the inbox lives somewhere else. Under Naive it is a single query.
What does not map yet
A migration guide that hides gaps is worse than none. The core path (store → reveal → list → delete, pluslocked/expires_at/rotate) maps cleanly, but the following Doppler capabilities have no
direct equivalent on Naive’s vault primitive today. Check this list against your app before you
commit.
Where to go next
vaultprimitive — full put/reveal/list/delete/rotate lifecycle,kind,locked,expires_atvaultSDK sub-client — typed method signaturesvaultCLI —naive vault put/reveal/listper user- Vault encryption — envelope encryption, KMS, and the
{ company_id, tenant_user_id }context - Account Kits — enabling/disabling the
vaultprimitive per user at execution time - Tenant users — the identity that owns the vault, cards, email, and phone
- Connections — OAuth grants that surface alongside vault entries in one per-user credential view
Related reading (blog)
- Why consolidate agent infra on one governed identity — migration thesis behind this guide
- Building AI Agents Into Your SaaS — tenant user anchor for every migration
- Secrets management for AI agents — vault concept post
- How to store API keys for AI agents — step-by-step vault patterns