Accounts, keys and access

An account holds users, projects and keys. You deploy and administer with a personal key; your application queries with a read-only query key that is granted specific projects and branches. Nothing you send to plan a query is stored.

Signing up

Sign up at https://app.0sql.io with an account name, your name, email and a password of at least 8 characters. That creates the account, makes you its first user with the admin role, and shows your first personal key once:

Run `zsql auth --api-key <key>` inside a project to store it. You can make more keys under My keys.

Copy it. The console lists keys by their first 12 characters afterwards and never shows the secret again. Store it in the project with zsql auth --api-key zsk_..., which writes .zsql (mode 0600, git-ignored), or export ZSQL_API_KEY, which wins over any file. See Authentication.

Roles

RoleCan
developerCreate personal and query keys, read every project with visibility: account, deploy where they hold Write, and create new projects (the first deploy of a new uid makes them its owner).
adminEverything a developer can, plus add and remove members, issue invites, revoke or rotate any query key, and read the audit log.

Two kinds of key

Personal keyQuery key
Prefixzsk_zqk_
Belongs toa userthe account
Can dowhatever its user may doread only: plan SQL, explain, explore, list fields and tables
Scopeevery project the user can reachonly the projects and branches it is granted
Where it lives.zsql on a developer machine, a CI secretyour application’s configuration
Rotationrevoke and create a new onerotate issues a new secret and keeps the grants

Both go in the same header on every request:

Authorization: Bearer zsk_...

A key answers for one account. Secrets are stored hashed (SHA-256); the service cannot show them again.

Personal keys

Named, several per user, listed under My keys in the console with name, 12-character prefix, created and last-used times. zsql and CI use them to deploy, check, test and query.

curl -s https://app.0sql.io/account/keys \
  -H "Authorization: Bearer zsk_..." \
  -H "Content-Type: application/json" \
  -d '{"name": "ci"}'

Response: {"key": {...}, "secret": "zsk_..."}. Revoke with DELETE /account/keys/{id}.

Query keys

Always read only, even for an admin. A query key starts with no access; a project owner grants it a project, and the grant names a branch:

branch in the grantMeaning
omitted (null)the project’s production branch
"*"every branch
"staging"that one branch

Create one:

curl -s https://app.0sql.io/account/query-keys \
  -H "Authorization: Bearer zsk_..." \
  -H "Content-Type: application/json" \
  -d '{"name": "checkout-service"}'
{"key": {"id": "<key id>", "name": "checkout-service", "prefix": "zqk_abcdefgh", "created_by": "<user id>", "created_at": "...", "last_used_at": null},
 "secret": "zqk_..."}

Grant it the tpcds project on every branch (you must own tpcds):

curl -s https://app.0sql.io/account/query-keys/<key id>/grants \
  -H "Authorization: Bearer zsk_..." \
  -H "Content-Type: application/json" \
  -d '{"project": "tpcds", "branch": "*"}'
{"key": {...}, "grant": {"project_id": "<project id>", "project_uid": "tpcds", "branch": "*"}}

Ship the secret with your application and plan with it:

curl -s https://app.0sql.io/projects/tpcds/branches/main/sql \
  -H "Authorization: Bearer zqk_..." \
  -H "Content-Type: application/json" \
  -d '{"expr": "item category, store net paid, year = 2002",
       "context": {"email": "tank@matrix.com", "system_admin": false, "project_admin": false,
                   "tags": [], "groups": [{"name": "CC-TMNT", "tags": ["call_center_id:TMNT"]}]}}'

The context is the security context of the person your application is answering for. It is required on a branch that has policies and ignored on one that does not. See Security.

GET /me tells a key what it is:

{"kind": "query_key",
 "key": {"id": "<key id>", "name": "checkout-service", "prefix": "zqk_abcdefgh", "created_by": "<user id>", "created_at": "...", "last_used_at": "..."},
 "account": {"uid": "acme", "name": "Acme"},
 "grants": [{"project_id": "<project id>", "project_uid": "tpcds", "branch": "*"}]}

A personal key gets {"kind": "user", "user", "account", "role", "key_id"} instead.

Revoke a grant with DELETE /account/query-keys/{id}/grants/{project}. Rotate with POST /account/query-keys/{id}/rotate, which returns a new secret and keeps every grant; the old secret stops working. Revoke the key with DELETE /account/query-keys/{id}. Rotation and revocation are open to the key’s creator and to admins.

A query key can call only GET /me, GET /projects and the read routes of its granted branches: sql, explain, explore, fields, tables and the branch summary. Anything else is a 403.

Which key for what

TaskKey
zsql deploy, zsql check, zsql testpersonal
zsql sql, zsql explain, zsql fields on a developer machinepersonal
CI pipeline that validates and deployspersonal (a dedicated user’s key)
An application calling /sqlquery
Granting access, changing project settingspersonal, held by an owner
Adding members, reading the audit logpersonal, held by an admin

Projects

A project is created by its first deploy: zsql deploy from a directory whose project.yml names a uid the account has not seen creates the project with that uid, name and production_branch, and makes the deploying user its owner. The uid is a slug: lower-case letters, digits, - and _.

Each project has:

SettingValuesEffect
visibilityaccount (default) | restrictedaccount: every developer in the account has Read. restricted: only people granted a level.
protected_productiontrue | falseWhen true, Write on the production branch needs Owner. Other branches are unaffected.
production_branchbranch name, default mainThe branch a query key grant with no branch reads.

Owners change these with PATCH /projects/{uid} or the project’s Settings tab, and delete a project with DELETE /projects/{uid}.

Access levels

Levels are ordered: read < write < owner. Each verb needs one:

LevelVerbs
Readsql, explain, explore, fields, tables, branch status
Writedeploy, validate (zsql check), test, remove a branch
Ownergrant and revoke access, grant query keys, change settings, delete the project

Owners manage people with GET /projects/{uid}/access (anyone with Read may look), PUT /projects/{uid}/access with {"email": "...", "level": "write"}, and DELETE /projects/{uid}/access/{email}. The same list is the People tab in the console.

Members and invites

Admins add a member with POST /account/members and {"email": "...", "name": "...", "role": "developer"}. The response carries a one-time invite token (zin_...); send the person https://app.0sql.io/invite?token=zin_.... Accepting it sets their name and password and shows their first personal key once, the same way sign-up does. POST /account/members/{email}/invite issues a fresh token for a member who has not accepted yet; DELETE /account/members/{email} removes a member. GET /account/members lists members with their roles.

Audit log

Admins read GET /account/audit?limit=50 (up to 1000) or the Audit tab:

{"events": [{"actor": "<email>", "action": "deploy", "subject": "<project/branch>", "at": "<timestamp>"}]}

Each deploy writes one row. Planning does not.

Playground

Every project in the console has a playground at /projects/{uid}/play?branch=<branch>. Type a shorthand expression or a spec as JSON, add a context if the branch has policies, and press SQL, Explain or Explore (or Cmd-Enter). It has a field search and a cheat sheet, and remembers your last 20 queries in your browser only. It is a developer tool, not an end-user UI: it returns SQL and never runs it.

What is stored

Stored, per deployed branch:

  • the compiled model (tables, fields, joins, universes, policies)
  • the branch’s tests
  • one audit row per deploy

Never stored:

  • query specs and expressions
  • security contexts
  • the SQL that comes back

sql, explain and explore write nothing: no log line, no database row, no file. Their only side effect is updating the key’s last_used_at, at most once a minute. There is no request logging middleware in front of them.

Never received at all:

  • the rows in your warehouse
  • the results of any statement
  • your warehouse credentials

0sql has no connection to your database. The compiled model describes your warehouse (names, SQL expressions, join conditions, policies) and that description is what the planner reads. Your datasources.yml can carry connection details as metadata for your own application, but zsql deploy strips every secret key from it before upload, and the service never opens a socket to a warehouse. Your data stays inside your network, and the statement you get back is executed there by you.

Authentication errors

Every error is {"error": {"class": "...", "message": "..."}}.

StatusClassMessageFix
401Unauthorizedan API key is required: Authorization: Bearer <key>Send the header. The service does not read x-api-key.
401Unauthorizedthis API key is not validThe secret is wrong, revoked or rotated. Create or copy a current one.
403Forbiddenquery keys are read only; deploy with your personal keyYou deployed, validated or tested with a zqk_ key.
403Forbiddenquery key <name> is not granted <uid>/<branch>Grant the key that project and branch, or *.
403Forbidden<email> has <level> access to <uid>; this needs <level>Ask an owner to raise your level.
403Forbidden<email> has no access to <uid>The project is restricted and you are not on it.
403Forbiddendeploying to the protected production branch '<b>' of <uid> needs an ownerDeploy to another branch, or have an owner deploy or turn protected_production off.

zsql prints these as <class>: <message>.

Next steps