What you would have to build to hold your users' credentials yourself
Vault
"Connect your Google account" is a button. Behind the button, on every agent platform that acts for its users, is a system that captures credentials in four different forms, encrypts them, keeps them alive, hands them to an agent without letting the agent read them, lets the user take them back, and can answer an auditor's question about any of it. Nobody budgets for that system when they add the button.
This post is the inventory. If you are deciding whether to build it, it is the list to estimate against. If you are deciding whether to use Seekrit Vault, it is the list of what the five setup steps replace.
The inventory
| You need | Why it is harder than it looks | What Vault does |
|---|---|---|
| A capture page your users open from a text message | It handles third-party passwords, so it needs its own origin, a strict CSP, no third-party script, and a mobile-first design for a link opened from iMessage | Hosted Connect page, white-labelled with your name, logo, and consent sentence |
| Consent that holds up | Recorded before capture, with the copy version the person saw, revocable by them | Recorded per connection with its consent version; manage links let the person disconnect |
| Encryption with a key you can defend | A key your app server holds is a key your app server's attackers hold. Per-value data keys, authenticated against where they live, so a relabelled or replayed row fails rather than decrypts | Fresh data key per revision, wrapped to a key that exists only in your executor's Durable Object; AAD binds project, connection, slot, provider, revision |
| OAuth, per provider | Your own client per provider (Google requires it for Gmail), PKCE, state, the quirks each preset knows — access_type=offline, offline_access, GitHub's HTTP 200 on failure | Presets for Google, Microsoft, GitHub, a generic provider; the exchange runs on your executor's origin |
| Refresh that survives crashes | Concurrent refreshes, and single-use refresh tokens where a lost outcome means the grant is gone and a retry is a replay | Single writer per connection with a dispatch journal; refresh_outcome_unknown instead of a second use of a spent token |
| A way to use the credential without giving it to the agent | The agent is prompt-injectable; a token in its context is a token in a transcript. Allow rules per provider, redaction of echoes, a check that the agent is acting for the user it claims | vault.fetch: default-deny host/method/path rules, injection, streaming redaction, userRef ownership check, typed refusals |
| Browser sessions for sites with no API | A session blob is more dangerous than a password hash and expires without warning; restoring it needs a fenced browser, a lease, a write-back, and a plan for when the site wants a human | Capture through Live View on the person's phone, leases on fenced Browser Run sessions, fill under the password-manager rule, reauth links, CAS write-back — written up separately |
| Revocation that means it | By the platform, by the user, by an admin; must stop in-flight saves, delete the ciphertext, and leave a record of who did it | Immediate refusal of every further release; ciphertext deleted; revokedBy recorded; late write-backs lose the CAS |
| An end-user control surface | Your users should be able to see and cut what the agent can reach without filing a ticket | Manage links: every connection you hold for them, with Disconnect |
| Events | Signed, retried, replay-resistant, carrying no values | connection.ready / reauth_required / revoked, HMAC-SHA256, retried with backoff for about a day |
| Audit | Every mutation, who did it, never a value; releases high-volume enough to need metering rather than rows | Every mutating route audits synchronously into your org's audit log; releases metered; denied releases are durable rows with a reason |
| Rate limits that fail closed | A stolen key or a runaway agent should be loud, not expensive | Per platform key, per executor, per Connect session and address |
| Operations | Key rotation, replacing a compromised executor, reading why a connection stopped working | status smoke test, rotate-key, disable-and-replace, a reason on every reauth_required |
Twelve rows. A reasonable engineer looks at any one of them and says "a sprint." The problem is that they are coupled: the revocation design depends on the write-back design, which depends on the refresh design, which depends on where the key lives. And the system is on the critical path of every action the agent takes for every user, so it has to work before the agent does.
The row that decides the others: where does the key live?
Every design decision in the table follows from one choice. If the key that decrypts your users' credentials lives on your application servers, then your database is a credential store, your backend is in the plaintext path, and every other row is about hardening a system that holds the thing. If the key lives somewhere your backend cannot reach, most rows get simpler and the guarantee you can offer users gets stronger.
Vault puts it in an executor: a small Worker you deploy into your own Cloudflare account from a template. The private key is generated inside its Durable Object and never exported. Seekrit holds ciphertext it cannot open. Your backend holds a platform key that can open connect links and read metadata but cannot release a credential. The trust model walks through why a breach of either side — or both — comes up short.
The five-step version
Vault's design target was a platform's first credential working in under fifteen minutes, in at most five steps, with three environment variables on the backend. Here is what that looks like against the table above.
- Dashboard → Vault → New project, then copy the
skv_…platform key. - Deploy the executor and declare your providers:
npx @seekrit/vault-executor init my-vault-executor
cd my-vault-executor && npm install
npx wrangler secret put EXECUTOR_TOKEN # any long random string
npx wrangler secret put GOOGLE_CLIENT_SECRET # one per OAuth provider, if any
npx wrangler deploy
- Register it with a one-time code from the dashboard:
npx @seekrit/vault-executor register <code> --url https://my-vault-executor.<you>.workers.dev
- Three variables on your backend:
SEEKRIT_VAULT_KEY,VAULT_EXECUTOR_URL,VAULT_EXECUTOR_TOKEN. - Code:
import { Vault } from "@seekrit/sdk/vault";
const vault = new Vault();
const link = await vault.connect.create({ userRef: "u_123", providerId: "google-gmail" });
// text link.url to the user; vault.connect.get(link.id) or a webhook tells you when it completes
const res = await vault.fetch({
userRef: "u_123",
connectionId,
request: { method: "GET", url: "https://gmail.googleapis.com/gmail/v1/users/me/labels" },
});
You need a Cloudflare account on the Workers Paid plan (the executor uses
Durable Objects) and a seekrit organization with Vault on its plan; the unit
is active connections. npx @seekrit/vault-executor status --url … tells you
whether the executor is up, registered, and missing any OAuth secret.
What you still own
Buying custody does not buy the product. You still own:
- The agent and its tools. Vault is custody, not usage. There is no
send_email; your agent's Gmail tool makes the request and Vault makes it authenticated. - Your OAuth clients and their verification with each provider.
- Your browsers, if you use browser sessions: Browser Run in your account, on your plan.
- The decision of what to connect. Keep provider rules narrow; the executor enforces hosts, methods and paths, and cannot judge intent.
- What you tell your users. The Connect page says neither you nor seekrit can read their credentials; the trust model is the page to point them at for why.
That is a shorter list than the table, and every item on it is something only you could own anyway.
The guide has every step above in detail; the API reference has the three surfaces and which credential opens each.