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
| Role | Can |
|---|---|
developer | Create 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). |
admin | Everything 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 key | Query key | |
|---|---|---|
| Prefix | zsk_ | zqk_ |
| Belongs to | a user | the account |
| Can do | whatever its user may do | read only: plan SQL, explain, explore, list fields and tables |
| Scope | every project the user can reach | only the projects and branches it is granted |
| Where it lives | .zsql on a developer machine, a CI secret | your application’s configuration |
| Rotation | revoke and create a new one | rotate 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 grant | Meaning |
|---|---|
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
| Task | Key |
|---|---|
zsql deploy, zsql check, zsql test | personal |
zsql sql, zsql explain, zsql fields on a developer machine | personal |
| CI pipeline that validates and deploys | personal (a dedicated user’s key) |
An application calling /sql | query |
| Granting access, changing project settings | personal, held by an owner |
| Adding members, reading the audit log | personal, 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:
| Setting | Values | Effect |
|---|---|---|
visibility | account (default) | restricted | account: every developer in the account has Read. restricted: only people granted a level. |
protected_production | true | false | When true, Write on the production branch needs Owner. Other branches are unaffected. |
production_branch | branch name, default main | The 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:
| Level | Verbs |
|---|---|
| Read | sql, explain, explore, fields, tables, branch status |
| Write | deploy, validate (zsql check), test, remove a branch |
| Owner | grant 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": "..."}}.
| Status | Class | Message | Fix |
|---|---|---|---|
| 401 | Unauthorized | an API key is required: Authorization: Bearer <key> | Send the header. The service does not read x-api-key. |
| 401 | Unauthorized | this API key is not valid | The secret is wrong, revoked or rotated. Create or copy a current one. |
| 403 | Forbidden | query keys are read only; deploy with your personal key | You deployed, validated or tested with a zqk_ key. |
| 403 | Forbidden | query key <name> is not granted <uid>/<branch> | Grant the key that project and branch, or *. |
| 403 | Forbidden | <email> has <level> access to <uid>; this needs <level> | Ask an owner to raise your level. |
| 403 | Forbidden | <email> has no access to <uid> | The project is restricted and you are not on it. |
| 403 | Forbidden | deploying to the protected production branch '<b>' of <uid> needs an owner | Deploy to another branch, or have an owner deploy or turn protected_production off. |
zsql prints these as <class>: <message>.