The key kinds
| Kind | Reaches | Made with |
|---|---|---|
| Account key | Everything an account administers: create and delete apps, manage members, billing | account.key.create |
| App key | One app's data and functions, and that app's administration | account.app.create, or tenant.key.rotate |
| Agent token | Whatever the signed-in person's role allows, scoped to that person | MCP OAuth, or auth.agent_token.create |
| Publishable key | A narrow, public-safe slice of an account key: a browser request from a declared origin only | account.key.publish |
See accounts-and-apps for how account and app map to each other, and agents for agent tokens.
Least privilege
Give each caller the narrowest credential it needs:
- A backend that calls one app's API needs an app key. Do not give it an account key. A function running inside the app needs no key because
dbis already bound to the app. - A browser page needs a publishable key scoped to its web origin. Do not expose a secret key in the browser.
- An agent acting for a person needs an agent token scoped to that person's role.
An irreversible call from an agent session requires a person's approval. List pending calls with agent.approval.list, then use agent.approval.approve or agent.approval.deny for each one.
Rotation
tenant.key.rotate replaces an app's key and returns the new one once; pass overlap_seconds to keep the old key working for a window so a deploy can switch over without a failed request in between.
curl -s -X POST $SUBSTRIO_URL/v1/_tenant/key -H "Authorization: Bearer $SUBSTRIO_KEY" \
-H 'content-type: application/json' -d '{"overlap_seconds": 300}'
tenant.key.revoke ends the overlap early, by the old key's id, once every caller has switched. An account key is rotated the same way by revoking it (account.key.revoke) and creating a new one (account.key.create); there is no in-place overlap for an account key, so stage the new key everywhere it is used before revoking the old one.
Where to store keys
Never put a key in function source, a committed file, or a URL. Store keys a function needs as secrets with secret.write, and read them as env.<KEY>. Store deployment keys in your CI secret store and pass them to substrio deploy or the CLI as environment variables. Do not commit them.
What to give an agent
An agent that only calls your app's own API needs an account key limited to that app, with a narrow requests_per_minute or request_budget: pass apps and the limit to account.key.create. An agent acting for a signed-in person should use that person's agent token, not a key at all, so every call it makes is scoped to what that person's role already allows and every irreversible call asks that person before it runs.