- ›
Create a tenant user— one identity per customer so each key is isolated to its owner. - ›
Store the key with vault put— it's envelope-encrypted with AWS KMS and tagged `kind: api_key`. - ›
Lock it if the agent shouldn't read it— a `locked` entry is used server-side but never revealed. - ›
Reveal over POST when a call needs it— the secret returns in the body, never in a URL. - ›
Expire and rotate— set `expires_at` for short-lived keys and use `vault_rotate` to re-wrap.
Your AI agent needs an API key to do real work — call a payments API, hit a partner service, resume a logged-in session. The wrong way to give it one is to paste the key into the prompt or pass it as a tool argument, where it can leak through prompt injection or logs. The right way is to store the key encrypted, per customer, and let the platform use it server-side.
This is a step-by-step version of the pattern in secrets management for AI agents, using Naïve's vault. Every step is a single call.
What counts as an "API key" here?
Before the steps, a quick definition, because it's broader than it sounds. For an AI agent, an "API key" is any long-lived credential it needs to authenticate a future action: a key it generated on a SaaS during onboarding, a token returned by an OAuth exchange, a session cookie captured after a login, or a password it set up on a customer's behalf. All of them share the same danger — if they enter the model's context to be "remembered," they can be exfiltrated. The safe path below is identical for each: store it encrypted and per-tenant, and let the platform use it server-side.
Step 1 — Create a tenant user
Give each customer their own identity so their keys are isolated to them. You address that customer's vault through naive.forUser(customerId); nothing you store leaks across tenants.
const alice = await naive.users.create({ email: "alice@acme.com" });Step 2 — Store the key (encrypted, per user)
Put the key in the vault under a descriptive name. It's envelope-encrypted with AWS KMS and scoped to that tenant user. put is idempotent on the key, so re-storing simply replaces it.
await naive.forUser(alice.id).vault.put("instantly.api_key", "key_xyz", {
kind: "api_key",
});The kind field (api_key, password, cookie, token, note, reference) is just a label for your own organization and UI — it doesn't change how the value is protected. Every value is envelope-encrypted with AWS KMS at rest, so you never hold plaintext keys in your own database, and because it's scoped to alice, no other tenant's agent can list or read it.
Step 3 — Lock it if the agent shouldn't read it
If the agent only needs the key's effect — not its value — store it locked. A locked entry can be used by the platform but never revealed back; a reveal returns forbidden.
await naive.forUser(alice.id).vault.put("stripe.restricted_key", "rk_live_...", {
kind: "api_key",
locked: true,
});This is the safest default for autonomous calls: even a fully compromised prompt can't extract a key the agent was never able to read.
Step 4 — Reveal server-side only when a call needs it
When your backend actually needs the key to make a call, reveal it — over POST, so the secret returns in the body and never appears in a URL or query log.
const { value } = await naive.forUser(alice.id).vault.reveal("instantly.api_key");
// use `value` immediately for the outbound call; don't put it back in the promptReveal at the moment of use and discard it — never round-trip a revealed secret back into the model's context. In practice this means your tool implementation (the server-side function the agent triggers) reveals the key, makes the outbound call, and returns only the result to the model — never the key itself. The agent sees "the email was sent," not the key that sent it.
Step 5 — Expire and rotate
For short-lived keys, set an expires_at when you store them; the entry drops out of listings and 404s on reveal once it passes. For long-lived keys, rotate on a schedule:
# re-wrap the encryption key (cheap); add ?regenerate_dek=true for a full re-encrypt
naive vault rotate instantly.api_key --user aliceRotation here is an operation, not a migration: you don't redeploy your app or touch your own database, because the key never lived there in the first place.
How do you know it worked?
You can list a tenant user's entries at any time — values come back masked, so listing is safe to log:
naive vault list --user aliceIf you stored an entry locked and then try to reveal it, you'll get a forbidden error — that's the system working as intended, confirming the agent genuinely cannot read the value back. An expired entry returns not-found on reveal and disappears from the list. Those two behaviors are the quickest way to confirm your keys are sealed the way you expect.
A note on what not to do
- Don't put the key in the system prompt or a tool argument — it becomes exfiltratable and ends up in logs.
- Don't share one key across tenants in an env var — one leak exposes everyone.
- Don't reveal a key and then feed it back into the model "so it can use it" — that reintroduces the exact risk you removed.
Why this matters most in multi-tenant apps
If you're building a product where every customer has their own agent, the stakes on this change completely. A key in a shared prompt or a shared environment variable isn't your test credential leaking — it's one customer's production key exposed to another customer's agent, or to your logs. Per-tenant vaulting turns that from a design you have to police into a boundary the platform enforces: naive.forUser(alice.id) can only ever reach Alice's entries. Scoping, encryption, and locked-until-used are the difference between "we're careful" and "the platform blocks cross-tenant access by default" — and the latter is the only story that holds up when real customers and real keys are involved.
Where to go next
That's the whole safe path: create a user, store the key, lock it, reveal server-side, expire and rotate. For the reasoning behind the pattern and the multi-tenant context, see secrets management for AI agents and the multi-tenant playbook. The vault docs are the source of truth for kinds, expiry, locking, and rotation.
How do I store an API key for an AI agent safely?+
Where should AI agent API keys be stored?+
How do I let an agent use an API key without reading it?+
How do I rotate or expire a stored API key?+
Why is reveal a POST instead of a GET?+
Building the agent-native backend.
Secrets management for AI agents means letting an agent use a credential without exposing it to the model. Here's the per-tenant vaulting pattern that fixes it.
Building multi-tenant AI agents into your SaaS means giving each customer an isolated, governed agent. Here's the full architecture and how to ship it.
The vault primitive: per-user encrypted storage for the secrets your agents hold — API keys, cookies, tokens — envelope-encrypted with a managed KMS.
Human-in-the-loop approvals that freeze sensitive actions until you say yes, a per-user audit trail of everything an agent did, short-lived scoped MCP sessions, and unified async job tracking — the controls that make an autonomous fleet safe to run.