API keys
Give server-side services access to a realm's data with API keys.
API keys let server-side services (a backend, a worker, a script) call a realm’s data routes. A key belongs to one realm and gives full access to all of that realm’s data, in every database.
Roles
Each key has a role, set when it’s created:
| Role | Can |
|---|---|
data (default) | Data routes with full access, whatever the rules say; act on behalf of a user |
admin | Everything data can, plus the admin API: users, roles and invitations |
A data key on the admin API gets 403 forbidden.
Managing keys
Keys are managed by the realm’s administrators, with backd apikey (after backd login, or with an admin key in BACKD_API_KEY) or over HTTP with the admin API:
| Command | Effect |
|---|---|
backd apikey create --realm <realm> --name <name> | Create a data key that never expires, and print it (with a warning: prefer --expires) |
backd apikey create --realm <realm> --name <name> --role admin | Create an admin key, for the admin API |
backd apikey create --realm <realm> --name <name> --networks 10.0.0.0/8,192.0.2.7 | Create a key that works only from those networks |
backd apikey create --realm <realm> --name <name> --scopes read:blog/posts,call:main/contact | Create a key that reaches only what the scopes grant |
backd apikey create --realm <realm> --name <name> --expires 90d | Create a key that stops working after 90 days (days or Go durations such as 12h) |
backd apikey list --realm <realm> | List keys: name, role, first characters, scopes, networks, creation, last use and expiry |
backd apikey revoke --realm <realm> --name <name> | Delete the key; it stops working at once |
backd login --realm blog --url https://api.example.com
backd apikey create --realm blog --name publisher --expires 90d
# bdk_7VeQ1hN0m2k… ← standard output: the key
# API key "publisher" (role data) created. Store it now as a secret: it can't be shown again. It expires at …
backd apikey list --realm blog
# NAME ROLE KEY SCOPES NETWORKS CREATED LAST USED EXPIRES
# publisher data bdk_7VeQ1h… all - 2026-09-26T15:22:52Z - 2026-12-25T15:22:52Z
- The key is printed once, on standard output, so scripts can capture it.
- Names follow the same rules as realm names and are unique within the realm.
- Keys start with
bdk_(session tokens start withbds_), which helps secret scanners find leaked keys. - The last use is recorded at most once a minute.
- To rotate a key without downtime, create a new one, deploy it, then revoke the old one.
Expiry and rotation policy
A leaked key gives full access to the realm until it’s revoked or expires, so give every key an expiry and rotate it on a schedule:
- Create keys with
--expires. Use 90 days or less for production services, and hours (--expires 12h) for one-off scripts and migrations.backd apikey createwarns when a key has no expiry. - Name keys by service and period, such as
billing-2026q4, so the old and new keys can live side by side during a rotation. - Rotate before expiry: create the replacement, deploy it, check with
backd apikey listthat the old key’sLAST USEDhas stopped advancing, then revoke it. - Revoke at once when a key may have leaked, or when the service using it is retired or its operator leaves.
- Keep one key per service, never a shared one, so revoking one doesn’t break the others.
At startup, backd serve helps enforce this. For each realm it logs:
- a
WARNline listing keys that never expire; - a
WARNline listing keys that expire within 14 days; - an
INFOline listing expired keys that are still stored (they are already refused; revoke them to tidy up).
The lines include key names only, never keys or hashes. backd checks only at startup, so for long-running services, schedule backd apikey list (for example weekly) and alert on the EXPIRES column.
Scopes
A key without scopes reaches everything its role allows. Scopes narrow a data key to what it was made for, so a leaked key exposes that and nothing else. They are set when the key is created, with --scopes (comma-separated) or scopes in the admin API, and apikey list shows them (all when there are none).
A grant is read, write or call, optionally followed by a target:
| Grant | Gives |
|---|---|
read | reading (list and get) every collection |
read:blog | reading the collections of the database blog |
write:blog/posts | creating, updating and deleting in the collection blog/posts (including batches); write doesn’t include reading |
call | calling every function |
call:main | calling the functions of the database main |
call:main/export | calling that function, and reading the jobs of that function |
# A website's key: reads the published posts, receives the contact form, nothing else.
backd apikey create --realm blog --name website --expires 90d \
--scopes read:main/posts,call:main/contact_form
- Anything not granted answers
403 forbidden, naming the target. On the data routes, in batches, on function calls and on job reads, whatever the access rules say (keys bypass the rules, scopes don’t). - Functions are separate on purpose. A function can do more than the key that calls it (it has its own admin access), so a scoped key calls a function only with a
callgrant. While the function runs, the data calls it makes as the caller stay within the key’s scopes too. - Acting on behalf of a user doesn’t widen a key: both the user’s rules and the key’s scopes apply.
- Scopes are for
datakeys. Anadminkey reaches the admin API, which scopes don’t cover, so creating one with scopes is refused; finer administrator rights are a separate feature. - A grant must name something the realm has: a typo (
read:blogs) is refused when the key is created, not discovered when it fails. - Keys made before scopes existed have none, and keep working as before.
Network restrictions
A key created with --networks works only from those IP addresses or CIDR networks, IPv4 or IPv6. From anywhere else it answers 401 unauthenticated, as an invalid key would, and the attempt is logged as a WARN with the key’s name and the client address. Client addresses come from TRUSTED_PROXIES, so set it exactly. Use this for services with fixed addresses, so a leaked key is useless elsewhere.
Using a key
Send the key as a Bearer credential:
curl https://localhost:8443/v1/blog/main/posts -H "Authorization: Bearer $BACKD_API_KEY"
The access log records the request’s actor as key:<name>, never the key itself.
Acting on behalf of a user
A service can make a request as one of the realm’s users by adding their id:
curl https://localhost:8443/v1/blog/main/posts \
-H "Authorization: Bearer $BACKD_API_KEY" \
-H "X-Backd-On-Behalf-Of: dars2ql90v600434mf0g"
- The user’s access rules apply, exactly as if they had signed in. The key’s full access doesn’t.
- Documents it creates are owned by the user:
_meta.owneris their id, and_meta.created_by/updated_byareuser:<id>. - The access log records the actor as
key:<name> as user:<id>, so the service stays visible. - Only API keys may send the header. With a session or without credentials it answers
400 invalid_header, as does an id that isn’t a user of the realm. A disabled user answers403 forbidden.
Use it when a backend handles a request for a user and should be bound by the same rules, for example a server-rendered page or a webhook acting for an account.
Who can access data
In a realm with auth: enabled, data routes (/v1/{realm}/{database}/{collection}…) decide by caller:
| Caller | Result |
|---|---|
| Valid API key of the realm | Full access, whatever the rules say |
API key with X-Backd-On-Behalf-Of | What the access rules allow that user |
| Signed-in user (session token) | What the collection’s access rules allow |
| No credentials | What the access rules allow anonymous callers (user == nil) |
| Unknown, expired or revoked key or token, or one from another realm | 401 unauthenticated |
A collection without a rules: section is only reachable with API keys.
Realms with auth: disabled ignore credentials entirely: everyone has full access.