Authentication
Every request except /health carries a bearer token:
Authorization: Bearer dbk_live_key_01a00b50-b5a7-7c7b-b5eb-586f2ed8cf51_r-FkSowtS8WNuxni…The token is one opaque string. Its internal structure — dbk_live_<key id>_<secret> — exists so that
a key can be identified in an audit log without the secret, but you should treat the whole thing as
atomic and never parse it.
Getting a token
Section titled “Getting a token”There is no signup flow yet. An operator provisions your organization and hands you a founder token
once. That token carries org:admin and db:admin, which is everything.
The token is displayed once and cannot be recovered. Only a SHA-256 hash of the secret is stored, so a lost token is not a support ticket — it is a new key and a revoked old one. This is a design decision rather than a gap: a system that can show you your token again is a system that stores your token.
Ask the operator to issue narrower keys for anything that runs unattended, and keep the founder token somewhere a deploy script cannot reach.
Scopes
Section titled “Scopes”| Scope | Grants |
|---|---|
db:read |
Read-only SQL: SELECT, WITH, EXPLAIN, VALUES, and introspection PRAGMAs. |
db:write |
Everything db:read grants, plus SQL that modifies data or schema. |
db:admin |
Everything db:write grants. Reserved for future database-level operations. |
org:admin |
Creating, listing, inspecting, and deleting databases. |
db:admin implies db:write, which implies db:read. So a db:write key does not need db:read
alongside it.
org:admin does not imply db:read. Managing databases and reading their contents are different
powers, and an admin credential that could also read every row in every database would make that
distinction unavailable to you. If you want both, hold both — the founder token does.
The scope a data-plane request needs is derived from the SQL, not from the endpoint. POST /query needs
db:read for a SELECT and db:write for an INSERT. When the classifier cannot tell what a statement
does, it demands db:write, because the other default would let a read-only key mutate data.
Database-scoped keys
Section titled “Database-scoped keys”A key can be pinned to a single database when it is issued. Such a key:
- can only query that database; every other id in your own organization answers
404; - sees only that database in
GET /v1/databases, so the list matches what the key can actually use.
This is the right credential for an application server. Give each service the narrowest key that lets it work, and a leaked key is a bounded incident rather than an open-ended one.
Every failure looks the same
Section titled “Every failure looks the same”An expired key, a revoked key, a malformed token, a missing header, and a token that never existed all produce exactly this:
{ "type": "https://errors.sqlited.dev/unauthenticated", "title": "Unauthenticated", "status": 401, "detail": "invalid API key", "requestId": "58a878d5-5ea9-4a5a-9168-2e386ce37b59"}The detail is identical in every case on purpose. A message that distinguished “no such key” from
“revoked key” would tell someone probing tokens which of their guesses had once been real, and that is
worth more to an attacker than it is to you. If you need to know why your own key failed, the operator
can tell you from the requestId.
Similarly, a database belonging to another organization returns 404, never 403. A 403 would confirm
the database exists. See Errors.
Rotating a key
Section titled “Rotating a key”- Ask the operator to issue a new key with the same scopes.
- Deploy it.
- Ask the operator to revoke the old one.
Both keys are valid between steps 1 and 3, so there is no window where your application has no working credential. Revocation takes effect on the next request; there is no cache to wait out.