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 key that does its job:
- A backend that calls one app's API needs that app's key, never an account key. A function running inside the app needs no key at all:
dbis already bound to its app. - A page in a browser needs a publishable key, scoped to its web origin, never a secret key of any kind.
- An agent acting for a person needs an agent token (scoped to that person's role), not a raw account key handed to it.
An irreversible call from an agent session waits for a person's approval. List the waiting calls with agent.approval.list and answer each with agent.approval.approve or agent.approval.deny.
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. Keys you must read from a function go through a secret (secret.write, read back as env.<KEY>); keys your deploy pipeline needs go in your own CI secret store and are passed to substrio deploy or the CLI as an environment variable, never checked in.
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.