OpenAPI spec
openapi: 3.0.3
info:
title: Unbase API
description: >
Unbase is a zero-config SQL (SQLite) database over HTTP, built on
Cloudflare Workers + Durable Objects. `POST /v1/projects` returns a
project and a bearer token in a single call; that token (the project's
secret key) is the permanent credential for the project's data plane.
Each project has two keys:
- **Secret key** (a.k.a. "service role"), format `<projectId>.<sig>` —
full SQL read/write plus admin. Send it as `Authorization: Bearer
<secretKey>`. Never expose it in a browser.
- **Anon key** (a.k.a. "publishable"), format `<projectId>.pk.<sig>` —
safe to embed in a client app. It unlocks the project's per-project
Auth endpoints (`/v1/projects/{id}/auth/*`) and read-only access to
Storage objects (`/v1/projects/{id}/storage/*`), and is sent in an
`apikey` header.
Project ids look like `unbase_xxxxxxxxxxxx`, account ids like `acct_...`,
and Auth end-user ids like `user_...`.
version: "2.0.0"
contact:
url: https://api.unbase.dev
servers:
- url: https://api.unbase.dev
description: Production
tags:
- name: projects
description: Create, claim, and delete projects
- name: sql
description: Run SQL against a project
- name: usage
description: Usage and export
- name: account
description: Log in and manage projects
- name: auth
description: >
Per-project Auth service — authenticate the end users of an app built
on a project. Callers authenticate with the project's anon key in an
`apikey` header (the secret key works too). Access tokens are HS256 JWTs
signed with the project's own JWT secret.
- name: storage
description: >
Per-project file storage, backed by Cloudflare R2. Uploads and deletes
require the secret key; reads (download and list) accept the anon key
too, sent as `Authorization: Bearer <key>`. Objects are private — there
is no public/anonymous URL.
paths:
/v1/projects:
post:
operationId: createProject
tags: [projects]
summary: Create a new project
description: >
Creates a new anonymous project and returns its id, URL, a bearer
token (the secret key), and an anon key. The token is shown exactly
once in this response and is not retrievable afterwards. No
authentication required.
requestBody:
required: false
content:
application/json:
schema:
type: object
properties:
turnstileToken:
type: string
description: >
Cloudflare Turnstile token from the browser creation
flow. Only relevant to web-based abuse prevention;
server-to-server/API callers can omit this field.
additionalProperties: false
responses:
"201":
description: Project created
content:
application/json:
schema:
$ref: "#/components/schemas/CreateProjectResponse"
"429":
description: Too many projects created from this IP this hour
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorResponse"
"503":
description: Shared free-tier capacity temporarily saturated
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorResponse"
/v1/claim:
post:
operationId: initiateClaim
tags: [projects]
summary: Initiate a magic-link claim (step 1 of 2)
description: >
Step 1 of the two-step claim flow. Confirms the project exists,
then mints a signed, 30-minute magic-link token and emails a claim
link ({SITE_URL}/claim/verify?token=...) to the given address via
Resend. This does NOT claim the project — the recipient must
open the link, which drives POST /v1/claim/verify. Requiring the
email owner to open the link proves control of the address before
the claim is committed. No authentication required (the caller must
supply the projectId). When no Resend API key is configured (keyless
dev mode) no email is sent and the full claim URL is returned inline
as `devLink`; do not run keyless in production.
requestBody:
required: true
content:
application/json:
schema:
type: object
required: [id, email]
properties:
id:
type: string
description: The projectId to claim
example: unbase_7f3k9q2m1x8a
email:
type: string
format: email
additionalProperties: false
responses:
"200":
description: >
Claim link sent (or, in keyless dev mode, returned inline as
devLink)
content:
application/json:
schema:
$ref: "#/components/schemas/InitiateClaimResponse"
"400":
description: Missing id or email
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorResponse"
"404":
description: Project not found
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorResponse"
/v1/claim/verify:
post:
operationId: verifyClaim
tags: [projects]
summary: Complete a magic-link claim (step 2 of 2)
description: >
Step 2 of the two-step claim flow. Verifies the magic-link token's
HMAC signature and 30-minute expiry, then permanently attaches the
anonymous project to the email-based account, upgrading its plan
from "anonymous" to "free" and removing the 7-day expiry. Creates
the account if it doesn't already exist (idempotent by email). The
project's existing bearer token continues to work unchanged. No
authentication required — the token itself is the proof.
requestBody:
required: true
content:
application/json:
schema:
type: object
required: [token]
properties:
token:
type: string
description: The token from the magic link.
additionalProperties: false
responses:
"200":
description: Claim completed
content:
application/json:
schema:
$ref: "#/components/schemas/ClaimResponse"
"400":
description: >
Missing token, or the token is malformed, tampered, or expired
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorResponse"
/v1/auth/login:
post:
operationId: requestLogin
tags: [account]
summary: Request an account login magic link
description: >
Sends a magic link to the given email for the Unbase dashboard /
account. Verifying it (see /v1/auth/verify) logs the user in,
creating their account on first login — so this doubles as sign-up.
This is the account/console login, distinct from the per-project
Auth service under /v1/projects/{id}/auth. No authentication
required. In keyless dev mode (no Resend key) the link is returned
inline as `devLink` instead of being emailed.
security: []
requestBody:
required: true
content:
application/json:
schema:
type: object
required: [email]
properties:
email:
type: string
format: email
additionalProperties: false
responses:
"200":
description: Link sent (or returned inline in dev mode)
content:
application/json:
schema:
$ref: "#/components/schemas/MagicLinkResponse"
"400":
$ref: "#/components/responses/BadRequest"
/v1/auth/verify:
post:
operationId: verifyLogin
tags: [account]
summary: Complete account login and receive a session token
description: >
Verifies an account login magic link's HMAC signature and 30-minute
expiry, gets-or-creates the account for that email, and returns a
long-lived (30-day) account session token. Present that token as a
bearer credential on the /v1/account/* endpoints.
security: []
requestBody:
required: true
content:
application/json:
schema:
type: object
required: [token]
properties:
token:
type: string
additionalProperties: false
responses:
"200":
description: Logged in
content:
application/json:
schema:
$ref: "#/components/schemas/SessionResponse"
"400":
$ref: "#/components/responses/BadRequest"
/v1/account:
get:
operationId: getAccount
tags: [account]
summary: Get the authenticated account
security:
- sessionAuth: []
responses:
"200":
description: Account details
content:
application/json:
schema:
$ref: "#/components/schemas/AccountResponse"
"401":
$ref: "#/components/responses/Unauthorized"
/v1/account/projects:
get:
operationId: listAccountProjects
tags: [account]
summary: List the account's projects
description: >
Returns every project owned by the authenticated account, each with
a freshly minted working bearer token and anon key (keys are
deterministic, so this is safe — the session already proves
ownership).
security:
- sessionAuth: []
responses:
"200":
description: The account's projects
content:
application/json:
schema:
$ref: "#/components/schemas/AccountProjectList"
"401":
$ref: "#/components/responses/Unauthorized"
post:
operationId: createAccountProject
tags: [account]
summary: Create a new project
description: >
Creates a new project owned by the account, on the account's plan,
and returns its endpoint, secret token, and anon key. Enforces the
plan's max-projects cap.
security:
- sessionAuth: []
requestBody:
required: false
content:
application/json:
schema:
type: object
properties:
name:
type: string
description: Optional human-friendly project name (max 64 chars).
additionalProperties: false
responses:
"201":
description: Project created
content:
application/json:
schema:
$ref: "#/components/schemas/CreatedProjectResponse"
"401":
$ref: "#/components/responses/Unauthorized"
"403":
description: Plan project limit reached
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorResponse"
/v1/account/projects/{projectId}:
patch:
operationId: renameAccountProject
tags: [account]
summary: Rename a project
description: >
Updates the display name of a project the authenticated account owns.
The name is trimmed and capped at 64 chars; an empty (or whitespace)
name clears it back to null, and the dashboard then falls back to the
project id. Scoped to the account — renaming a project the account
does not own returns 404.
security:
- sessionAuth: []
parameters:
- name: projectId
in: path
required: true
schema:
type: string
description: The project's id (e.g. `unbase_...`).
requestBody:
required: true
content:
application/json:
schema:
type: object
properties:
name:
type: string
description: New project name (max 64 chars; empty clears it).
additionalProperties: false
responses:
"200":
description: The updated project id and name
content:
application/json:
schema:
type: object
required: [id, name]
properties:
id:
type: string
name:
type: string
nullable: true
"401":
$ref: "#/components/responses/Unauthorized"
"404":
description: No such project owned by this account
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorResponse"
/v1/stripe/webhook:
post:
operationId: stripeWebhook
tags: [projects]
summary: Apply paid-plan upgrades/downgrades from Stripe
description: >
Receives Stripe webhook events and applies paid-plan changes.
Authenticated by the Stripe signature (NOT a bearer token): the
Stripe-Signature header is verified via HMAC-SHA256 against
STRIPE_WEBHOOK_SECRET with a 5-minute timestamp tolerance. The
request body is the raw Stripe event JSON. On
checkout.session.completed the customer email (customer_email or
customer_details.email) and target plan (metadata.plan, "founder"
or "pro", set on the Stripe Payment Link) are read and the account
plus all its projects are upgraded to that plan. On
customer.subscription.deleted the account is downgraded to "free",
but only if metadata.email is present on the event.
security: []
parameters:
- name: Stripe-Signature
in: header
required: true
schema:
type: string
description: >
Stripe's `t=...,v1=...` signature over the raw request body.
requestBody:
required: true
description: Raw Stripe event JSON.
content:
application/json:
schema:
type: object
additionalProperties: true
responses:
"200":
description: >
Event received. `applied` is true when a plan change was made,
false for acknowledged-but-no-op events.
content:
application/json:
schema:
$ref: "#/components/schemas/StripeWebhookResponse"
"400":
description: Bad or missing Stripe signature
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorResponse"
"503":
description: STRIPE_WEBHOOK_SECRET is not configured
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorResponse"
/v1/projects/{id}/query:
post:
operationId: runQuery
tags: [sql]
summary: Run a single SQL statement
security:
- secretKey: []
parameters:
- $ref: "#/components/parameters/ProjectId"
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/Statement"
responses:
"200":
description: Statement executed
headers:
X-Unbase-Rows-Read:
schema:
type: string
description: Mirrors rowsRead in the response body
X-Unbase-Rows-Written:
schema:
type: string
description: Mirrors rowsWritten in the response body
X-Unbase-Limit-Warning:
schema:
type: string
enum: ["true"]
description: >
Present only when the account is at or over 100% of its
plan's monthly quota (read, write, or storage).
content:
application/json:
schema:
$ref: "#/components/schemas/QueryResult"
"400":
$ref: "#/components/responses/BadRequest"
"401":
$ref: "#/components/responses/Unauthorized"
"403":
$ref: "#/components/responses/Forbidden"
"413":
$ref: "#/components/responses/PayloadTooLarge"
"503":
$ref: "#/components/responses/ServiceUnavailable"
/v1/projects/{id}/batch:
post:
operationId: runBatch
tags: [sql]
summary: Run multiple SQL statements as one atomic transaction
security:
- secretKey: []
parameters:
- $ref: "#/components/parameters/ProjectId"
requestBody:
required: true
content:
application/json:
schema:
type: object
required: [statements]
properties:
statements:
type: array
minItems: 1
items:
$ref: "#/components/schemas/Statement"
additionalProperties: false
responses:
"200":
description: All statements executed (all-or-nothing)
headers:
X-Unbase-Rows-Read:
schema:
type: string
X-Unbase-Rows-Written:
schema:
type: string
X-Unbase-Limit-Warning:
schema:
type: string
enum: ["true"]
content:
application/json:
schema:
$ref: "#/components/schemas/BatchResult"
"400":
$ref: "#/components/responses/BadRequest"
"401":
$ref: "#/components/responses/Unauthorized"
"403":
$ref: "#/components/responses/Forbidden"
"413":
$ref: "#/components/responses/PayloadTooLarge"
"503":
$ref: "#/components/responses/ServiceUnavailable"
/v1/projects/{id}/usage:
get:
operationId: getUsage
tags: [usage]
summary: Get plan, status, and current-month usage
security:
- secretKey: []
parameters:
- $ref: "#/components/parameters/ProjectId"
responses:
"200":
description: Usage snapshot
content:
application/json:
schema:
$ref: "#/components/schemas/UsageResponse"
"401":
$ref: "#/components/responses/Unauthorized"
/v1/projects/{id}/export:
get:
operationId: exportProject
tags: [usage]
summary: Download a plain-SQL logical dump of the project
description: >
Returns a plain-text SQL dump (CREATE TABLE + INSERT INTO
statements wrapped in a transaction) — not a binary .sqlite file.
Fully replayable with `sqlite3 restored.db < dump.sql`.
security:
- secretKey: []
parameters:
- $ref: "#/components/parameters/ProjectId"
responses:
"200":
description: SQL dump
headers:
Content-Disposition:
schema:
type: string
example: 'attachment; filename="unbase_7f3k9q2m1x8a.sql"'
content:
application/sql:
schema:
type: string
format: binary
"401":
$ref: "#/components/responses/Unauthorized"
"404":
$ref: "#/components/responses/NotFound"
/v1/projects/{id}/tables:
get:
operationId: listTables
tags: [sql]
summary: List the project's tables, columns, and row counts
description: >
Schema introspection for building a table browser. Returns user
tables only — internal bookkeeping (_unbase_*), SQLite (sqlite_*),
and Cloudflare (_cf_*) tables are excluded.
security:
- secretKey: []
parameters:
- $ref: "#/components/parameters/ProjectId"
responses:
"200":
description: The project's tables
content:
application/json:
schema:
$ref: "#/components/schemas/TablesResponse"
"401":
$ref: "#/components/responses/Unauthorized"
/v1/projects/{id}:
delete:
operationId: deleteProject
tags: [projects]
summary: Permanently delete a project and its exports
security:
- secretKey: []
parameters:
- $ref: "#/components/parameters/ProjectId"
responses:
"204":
description: Deleted (no content)
"401":
$ref: "#/components/responses/Unauthorized"
/v1/projects/{id}/storage/objects:
get:
operationId: listStorageObjects
tags: [storage]
summary: List a project's storage objects
security:
- secretKey: []
- anonApiKey: []
parameters:
- $ref: "#/components/parameters/ProjectId"
responses:
"200":
description: The project's storage objects
content:
application/json:
schema:
$ref: "#/components/schemas/StorageObjectList"
"401":
$ref: "#/components/responses/Unauthorized"
/v1/projects/{id}/storage/objects/{key}:
put:
operationId: uploadStorageObject
tags: [storage]
summary: Upload (or overwrite) an object
description: >
The request body is the raw file bytes — not multipart/form-data.
`Content-Type` is stored and returned on download; it defaults to
`application/octet-stream` if omitted. The key may contain `/` to
namespace objects into folders, e.g. `avatars/user.png`. Capped at
100 MB per object, subject to the plan's file-storage quota.
security:
- secretKey: []
parameters:
- $ref: "#/components/parameters/ProjectId"
- $ref: "#/components/parameters/StorageKey"
requestBody:
required: true
content:
"*/*":
schema:
type: string
format: binary
responses:
"201":
description: Object stored
content:
application/json:
schema:
$ref: "#/components/schemas/StorageObject"
"400":
$ref: "#/components/responses/BadRequest"
"401":
$ref: "#/components/responses/Unauthorized"
"403":
$ref: "#/components/responses/Forbidden"
"413":
$ref: "#/components/responses/PayloadTooLarge"
get:
operationId: downloadStorageObject
tags: [storage]
summary: Download an object
security:
- secretKey: []
- anonApiKey: []
parameters:
- $ref: "#/components/parameters/ProjectId"
- $ref: "#/components/parameters/StorageKey"
responses:
"200":
description: Object bytes, with the Content-Type stored at upload time
content:
"*/*":
schema:
type: string
format: binary
"401":
$ref: "#/components/responses/Unauthorized"
"404":
$ref: "#/components/responses/NotFound"
delete:
operationId: deleteStorageObject
tags: [storage]
summary: Delete an object
security:
- secretKey: []
parameters:
- $ref: "#/components/parameters/ProjectId"
- $ref: "#/components/parameters/StorageKey"
responses:
"204":
description: Deleted (no content)
"401":
$ref: "#/components/responses/Unauthorized"
"404":
$ref: "#/components/responses/NotFound"
/v1/projects/{id}/auth/signup:
post:
operationId: authSignup
tags: [auth]
summary: Sign up an end user with email + password
description: >
Registers a new end user of the project and returns a Session.
Authenticate the request with the project's anon key in the `apikey`
header (the secret key also works). Password must be at least 8
characters.
security:
- anonApiKey: []
parameters:
- $ref: "#/components/parameters/ProjectId"
requestBody:
required: true
content:
application/json:
schema:
type: object
required: [email, password]
properties:
email:
type: string
format: email
password:
type: string
minLength: 8
additionalProperties: false
responses:
"201":
description: User created; Session returned
content:
application/json:
schema:
$ref: "#/components/schemas/Session"
"400":
$ref: "#/components/responses/BadRequest"
"401":
$ref: "#/components/responses/Unauthorized"
"409":
description: A user with that email already exists
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorResponse"
/v1/projects/{id}/auth/signin:
post:
operationId: authSignin
tags: [auth]
summary: Sign in an end user with email + password
description: >
Verifies an end user's email + password and returns a Session.
Authenticate the request with the project's anon key in the `apikey`
header (the secret key also works).
security:
- anonApiKey: []
parameters:
- $ref: "#/components/parameters/ProjectId"
requestBody:
required: true
content:
application/json:
schema:
type: object
required: [email, password]
properties:
email:
type: string
format: email
password:
type: string
additionalProperties: false
responses:
"200":
description: Signed in; Session returned
content:
application/json:
schema:
$ref: "#/components/schemas/Session"
"400":
$ref: "#/components/responses/BadRequest"
"401":
description: Invalid credentials or missing/invalid apikey
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorResponse"
/v1/projects/{id}/auth/magiclink:
post:
operationId: authMagicLink
tags: [auth]
summary: Send an end user a passwordless magic link
description: >
Emails the end user a passwordless sign-in link pointing at
`redirectTo?token=...` (falls back to
`<SITE_URL>/auth/callback?token=...` when redirectTo is omitted). The
project owner's app then calls .../auth/verify with the token to
obtain a Session. Authenticate the request with the project's anon
key in the `apikey` header. In keyless dev mode the link is returned
inline as `devLink`.
security:
- anonApiKey: []
parameters:
- $ref: "#/components/parameters/ProjectId"
requestBody:
required: true
content:
application/json:
schema:
type: object
required: [email]
properties:
email:
type: string
format: email
redirectTo:
type: string
format: uri
description: >
URL the emailed link points at; the single-use token is
appended as a `token` query parameter. Defaults to
`<SITE_URL>/auth/callback`.
additionalProperties: false
responses:
"200":
description: Link sent (or returned inline in dev mode)
content:
application/json:
schema:
$ref: "#/components/schemas/MagicLinkResponse"
"400":
$ref: "#/components/responses/BadRequest"
"401":
$ref: "#/components/responses/Unauthorized"
/v1/projects/{id}/auth/verify:
post:
operationId: authVerify
tags: [auth]
summary: Verify a magic-link token and receive a Session
description: >
Completes a passwordless sign-in by verifying the token from a magic
link and returning a Session. Creates the end user on first verify.
Authenticate the request with the project's anon key in the `apikey`
header.
security:
- anonApiKey: []
parameters:
- $ref: "#/components/parameters/ProjectId"
requestBody:
required: true
content:
application/json:
schema:
type: object
required: [token]
properties:
token:
type: string
additionalProperties: false
responses:
"200":
description: Verified; Session returned
content:
application/json:
schema:
$ref: "#/components/schemas/Session"
"400":
description: Missing, malformed, or expired token
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorResponse"
"401":
$ref: "#/components/responses/Unauthorized"
/v1/projects/{id}/auth/token:
post:
operationId: authRefreshToken
tags: [auth]
summary: Exchange a refresh token for a new Session
description: >
Rotates a refresh token for a fresh Session (new access + refresh
token). Refresh tokens are single-use — each call invalidates the
one presented. Authenticate the request with the project's anon key
in the `apikey` header.
security:
- anonApiKey: []
parameters:
- $ref: "#/components/parameters/ProjectId"
requestBody:
required: true
content:
application/json:
schema:
type: object
required: [refreshToken]
properties:
refreshToken:
type: string
additionalProperties: false
responses:
"200":
description: New Session issued
content:
application/json:
schema:
$ref: "#/components/schemas/Session"
"400":
$ref: "#/components/responses/BadRequest"
"401":
description: Invalid, expired, or already-used refresh token
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorResponse"
/v1/projects/{id}/auth/password:
post:
operationId: authChangePassword
tags: [auth]
summary: Change the signed-in end user's password
description: >
Changes the password of the end user identified by their access token.
Present the access token as `Authorization: Bearer <accessToken>`
together with the project's anon key in the `apikey` header. A user who
already has a password must supply the correct `currentPassword`;
passwordless (magic-link-only) users omit it to set one for the first
time. The new password must be at least 8 characters. The change revokes
the user's other sessions and returns a fresh Session.
security:
- userAccessToken: []
anonApiKey: []
parameters:
- $ref: "#/components/parameters/ProjectId"
requestBody:
required: true
content:
application/json:
schema:
type: object
required: [newPassword]
properties:
currentPassword:
type: string
description: >
Required when the user already has a password; omitted by
passwordless users setting one for the first time.
newPassword:
type: string
minLength: 8
additionalProperties: false
responses:
"200":
description: Password changed; a fresh Session is returned
content:
application/json:
schema:
$ref: "#/components/schemas/Session"
"400":
$ref: "#/components/responses/BadRequest"
"401":
description: Missing/invalid access token or incorrect current password
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorResponse"
/v1/projects/{id}/auth/recover:
post:
operationId: authRecover
tags: [auth]
summary: Send an end user a password-reset link
description: >
Starts a forgot-password flow: emails the end user a reset link
pointing at `redirectTo?token=...` (falls back to
`<SITE_URL>/auth/reset?token=...` when redirectTo is omitted). The
project owner's app then calls .../auth/reset with the token and a new
password. Authenticate the request with the project's anon key in the
`apikey` header. Always reports success without revealing whether an
account exists. In keyless dev mode the link is returned inline as
`devLink`.
security:
- anonApiKey: []
parameters:
- $ref: "#/components/parameters/ProjectId"
requestBody:
required: true
content:
application/json:
schema:
type: object
required: [email]
properties:
email:
type: string
format: email
redirectTo:
type: string
format: uri
description: >
URL the emailed link points at; the single-use reset token
is appended as a `token` query parameter. Defaults to
`<SITE_URL>/auth/reset`.
additionalProperties: false
responses:
"200":
description: Reset link sent (or returned inline in dev mode)
content:
application/json:
schema:
$ref: "#/components/schemas/MagicLinkResponse"
"400":
$ref: "#/components/responses/BadRequest"
"401":
$ref: "#/components/responses/Unauthorized"
/v1/projects/{id}/auth/reset:
post:
operationId: authResetPassword
tags: [auth]
summary: Complete a password reset with a reset token
description: >
Completes a forgot-password flow: verifies the token from a reset link,
sets the new password on the matching account, revokes all prior
sessions, and returns a fresh Session. The new password must be at
least 8 characters. Authenticate the request with the project's anon
key in the `apikey` header.
security:
- anonApiKey: []
parameters:
- $ref: "#/components/parameters/ProjectId"
requestBody:
required: true
content:
application/json:
schema:
type: object
required: [token, newPassword]
properties:
token:
type: string
newPassword:
type: string
minLength: 8
additionalProperties: false
responses:
"200":
description: Password reset; Session returned
content:
application/json:
schema:
$ref: "#/components/schemas/Session"
"400":
description: >
Missing/short new password, or a missing, malformed, expired, or
unknown-account reset token
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorResponse"
"401":
$ref: "#/components/responses/Unauthorized"
/v1/projects/{id}/auth/user:
post:
operationId: authGetUser
tags: [auth]
summary: Get the current end user
description: >
Returns the end user identified by the access token. Present the
access token as `Authorization: Bearer <accessToken>` together with
the project's anon key in the `apikey` header. The request body is an
empty JSON object.
security:
- userAccessToken: []
anonApiKey: []
parameters:
- $ref: "#/components/parameters/ProjectId"
requestBody:
required: false
content:
application/json:
schema:
type: object
additionalProperties: false
responses:
"200":
description: The current end user
content:
application/json:
schema:
type: object
required: [user]
properties:
user:
$ref: "#/components/schemas/AuthUser"
"401":
$ref: "#/components/responses/Unauthorized"
/v1/projects/{id}/auth/logout:
post:
operationId: authLogout
tags: [auth]
summary: Revoke a refresh token (sign out)
description: >
Invalidates the given refresh token so it can no longer be exchanged.
Authenticate the request with the project's anon key in the `apikey`
header.
security:
- anonApiKey: []
parameters:
- $ref: "#/components/parameters/ProjectId"
requestBody:
required: true
content:
application/json:
schema:
type: object
required: [refreshToken]
properties:
refreshToken:
type: string
additionalProperties: false
responses:
"204":
description: Logged out (no content)
"400":
$ref: "#/components/responses/BadRequest"
"401":
$ref: "#/components/responses/Unauthorized"
/v1/projects/{id}/auth/users:
get:
operationId: authListUsers
tags: [auth]
summary: List the project's end users (admin)
description: >
Admin-only. Returns every end user registered with the project.
Requires the project's secret key as `Authorization: Bearer
<secretKey>` — the anon key is rejected here.
security:
- secretKey: []
parameters:
- $ref: "#/components/parameters/ProjectId"
responses:
"200":
description: The project's end users
content:
application/json:
schema:
type: object
required: [users]
properties:
users:
type: array
items:
$ref: "#/components/schemas/AuthUser"
"401":
$ref: "#/components/responses/Unauthorized"
/v1/projects/{id}/auth/settings:
get:
operationId: authGetSettings
tags: [auth]
summary: Get the project's Auth settings (admin)
description: >
Admin-only. Returns the project's Auth configuration, including the
JWT secret used to sign end-user access tokens — so the project owner
can verify those tokens in their own backend. Requires the project's
secret key as `Authorization: Bearer <secretKey>`.
security:
- secretKey: []
parameters:
- $ref: "#/components/parameters/ProjectId"
responses:
"200":
description: Auth settings
content:
application/json:
schema:
$ref: "#/components/schemas/AuthSettings"
"401":
$ref: "#/components/responses/Unauthorized"
components:
securitySchemes:
secretKey:
type: http
scheme: bearer
description: >
The project's secret key (service role), of the form
`${projectId}.${signature}`, returned once by POST /v1/projects. It
is the permanent full-access credential for that project's data
plane and admin Auth endpoints; a key only authorizes the projectId
embedded in it. Never expose it in a browser.
anonApiKey:
type: apiKey
in: header
name: apikey
description: >
The project's anon (publishable) key, of the form
`${projectId}.pk.${signature}`. Safe to embed in a client app. Sent
in the `apikey` header, it unlocks the per-project Auth endpoints
only. The secret key is also accepted in this header.
userAccessToken:
type: http
scheme: bearer
description: >
An end user's access token — an HS256 JWT signed with the project's
own JWT secret, returned in a Session by the Auth endpoints. Used on
POST /v1/projects/{id}/auth/user. Claims:
{ sub: userId, iss: projectId, role: "authenticated", email, iat, exp }.
sessionAuth:
type: http
scheme: bearer
description: >
Account session token returned by POST /v1/auth/verify. Long-lived
(30 days) and scoped to the account, not a single project. Used on
the /v1/account/* endpoints.
parameters:
ProjectId:
name: id
in: path
required: true
schema:
type: string
example: unbase_7f3k9q2m1x8a
description: The projectId to operate on.
StorageKey:
name: key
in: path
required: true
schema:
type: string
example: avatars/user.png
description: >
The object's key within the project. May contain `/` to namespace
objects into folders; each path segment must be percent-encoded.
responses:
BadRequest:
description: Malformed request
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorResponse"
Unauthorized:
description: Missing, invalid, or mismatched credential
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorResponse"
Forbidden:
description: >
Quota hard-limit exceeded (writes only), account suspended, or the
statement referenced a reserved _unbase_* table
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorResponse"
NotFound:
description: Unknown route or project
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorResponse"
PayloadTooLarge:
description: Response would exceed the 2 MB response cap
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorResponse"
ServiceUnavailable:
description: >
Shared free-tier capacity temporarily saturated (circuit
breaker); writes only, reads unaffected
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorResponse"
schemas:
CreateProjectResponse:
type: object
required: [projectId, url, token, anonKey]
properties:
projectId:
type: string
example: unbase_7f3k9q2m1x8a
url:
type: string
format: uri
example: https://api.unbase.dev/v1/projects/unbase_7f3k9q2m1x8a
token:
type: string
description: >
The project's secret key. Shown exactly once. Store it
immediately.
example: unbase_7f3k9q2m1x8a.AbCdEf123...
anonKey:
type: string
description: >
The project's anon (publishable) key. Safe to embed in a client
app; unlocks the project's Auth endpoints only.
example: unbase_7f3k9q2m1x8a.pk.AbCdEf123...
InitiateClaimResponse:
type: object
required: [sent]
properties:
sent:
type: boolean
description: >
True when a claim email was sent (Resend key configured);
false in keyless dev mode.
devLink:
type: string
format: uri
description: >
Present only in keyless dev mode — the full claim URL, returned
inline because no email was sent.
example: https://unbase.dev/claim/verify?token=eyJ...
ClaimResponse:
type: object
required: [projectId, accountId, plan]
properties:
projectId:
type: string
example: unbase_7f3k9q2m1x8a
accountId:
type: string
example: acct_9k2m1x8a7f3k
plan:
type: string
enum: [free]
MagicLinkResponse:
type: object
required: [sent]
properties:
sent:
type: boolean
description: True when the link was emailed.
devLink:
type: string
description: >
Present only in keyless dev mode — the link that would have been
emailed, returned inline so the flow stays usable.
SessionResponse:
type: object
required: [token, accountId, email, plan]
properties:
token:
type: string
description: The account session bearer token (30-day expiry).
accountId:
type: string
example: acct_9k2m1x8a7f3k
email:
type: string
format: email
plan:
type: string
enum: [free, founder, pro]
AccountResponse:
type: object
required: [accountId, email, plan]
properties:
accountId:
type: string
example: acct_9k2m1x8a7f3k
email:
type: string
format: email
plan:
type: string
enum: [free, founder, pro]
AccountProjectSummary:
type: object
required: [id, plan, status, sizeBytes, url, token, anonKey]
properties:
id:
type: string
example: unbase_7f3k9q2m1x8a
name:
type: string
nullable: true
plan:
type: string
enum: [anonymous, free, founder, pro]
status:
type: string
enum: [active, limited, suspended]
createdAt:
type: integer
format: int64
nullable: true
lastUsedAt:
type: integer
format: int64
nullable: true
sizeBytes:
type: integer
url:
type: string
format: uri
example: https://api.unbase.dev/v1/projects/unbase_7f3k9q2m1x8a
token:
type: string
description: The project's secret key.
anonKey:
type: string
description: The project's anon (publishable) key.
AccountProjectList:
type: object
required: [projects]
properties:
projects:
type: array
items:
$ref: "#/components/schemas/AccountProjectSummary"
CreatedProjectResponse:
type: object
required: [projectId, url, token, anonKey, plan]
properties:
projectId:
type: string
example: unbase_7f3k9q2m1x8a
name:
type: string
nullable: true
url:
type: string
format: uri
example: https://api.unbase.dev/v1/projects/unbase_7f3k9q2m1x8a
token:
type: string
description: The project's secret key.
anonKey:
type: string
description: The project's anon (publishable) key.
plan:
type: string
enum: [free, founder, pro]
TablesResponse:
type: object
required: [tables]
properties:
tables:
type: array
items:
type: object
required: [name, rowCount, columns]
properties:
name:
type: string
rowCount:
type: integer
columns:
type: array
items:
type: object
required: [name, type, notnull, pk]
properties:
name:
type: string
type:
type: string
notnull:
type: boolean
pk:
type: boolean
StorageObject:
type: object
required: [key, size, contentType, createdAt, updatedAt]
properties:
key:
type: string
example: avatars/user.png
size:
type: integer
description: Size in bytes.
contentType:
type: string
example: image/png
createdAt:
type: integer
description: Unix ms timestamp of first upload.
updatedAt:
type: integer
description: Unix ms timestamp of the most recent upload (same as createdAt if never overwritten).
StorageObjectList:
type: object
required: [objects, totalBytes, count]
properties:
objects:
type: array
items:
$ref: "#/components/schemas/StorageObject"
totalBytes:
type: integer
description: Sum of size across all of the project's objects.
count:
type: integer
StripeWebhookResponse:
type: object
required: [received, applied]
properties:
received:
type: boolean
example: true
applied:
type: boolean
description: >
True when a plan change was applied; false for
acknowledged-but-no-op events.
plan:
type: string
description: The plan applied (present only when applied is true).
enum: [free, founder, pro]
projects:
type: integer
description: >
Number of projects updated (present only when applied is true).
Statement:
type: object
required: [sql]
properties:
sql:
type: string
example: "SELECT * FROM todos WHERE id = ?"
params:
type: array
description: Positional values bound to `?` placeholders.
items: {}
additionalProperties: false
QueryResult:
type: object
required: [rows, rowsRead, rowsWritten]
properties:
rows:
type: array
items:
type: object
additionalProperties: true
rowsRead:
type: integer
rowsWritten:
type: integer
BatchResult:
type: object
required: [results, rowsRead, rowsWritten]
properties:
results:
type: array
items:
$ref: "#/components/schemas/QueryResult"
rowsRead:
type: integer
description: Sum of rowsRead across all statements
rowsWritten:
type: integer
description: Sum of rowsWritten across all statements
UsageResponse:
type: object
required:
[plan, status, sizeBytes, usageMonth, rowsRead, rowsWritten]
properties:
plan:
type: string
enum: [anonymous, free, founder, pro]
status:
type: string
enum: [active, limited, suspended]
sizeBytes:
type: integer
usageMonth:
type: string
description: Calendar month these counters apply to, YYYY-MM.
example: "2026-07"
rowsRead:
type: integer
description: Row reads so far this calendar month.
rowsWritten:
type: integer
description: Row writes so far this calendar month.
AuthUser:
type: object
required: [id, email, createdAt]
properties:
id:
type: string
example: user_3m1x8a7f3k9q
email:
type: string
format: email
emailConfirmedAt:
type: string
format: date-time
nullable: true
description: >
When the email was confirmed (e.g. via magic link). Null until
confirmed.
createdAt:
type: string
format: date-time
lastSignInAt:
type: string
format: date-time
nullable: true
Session:
type: object
required:
[accessToken, tokenType, expiresIn, expiresAt, refreshToken, user]
description: >
Returned by the Auth signup/signin/verify/token endpoints. The
accessToken is an HS256 JWT signed with the project's own JWT secret
(retrievable via .../auth/settings), so the project owner can verify
end-user tokens in their own backend. Claims:
{ sub: userId, iss: projectId, role: "authenticated", email, iat, exp }.
properties:
accessToken:
type: string
description: HS256 JWT signed with the project's JWT secret.
tokenType:
type: string
enum: [bearer]
expiresIn:
type: integer
description: Access-token lifetime in seconds.
example: 3600
expiresAt:
type: integer
format: int64
description: Unix epoch (seconds) at which the access token expires.
refreshToken:
type: string
description: Single-use refresh token; rotated on each /auth/token call.
user:
$ref: "#/components/schemas/AuthUser"
AuthSettings:
type: object
required: [jwtSecret, userCount, anonKey, authUrl]
properties:
jwtSecret:
type: string
description: >
The project's JWT secret used to sign end-user access tokens.
Use it to verify HS256 access-token signatures in your backend.
userCount:
type: integer
description: Number of end users registered with the project.
anonKey:
type: string
description: The project's anon (publishable) key.
authUrl:
type: string
format: uri
description: Base URL of the project's Auth service.
example: https://api.unbase.dev/v1/projects/unbase_7f3k9q2m1x8a/auth
ErrorResponse:
type: object
required: [error]
properties:
error:
type: object
required: [code, message]
properties:
code:
type: string
enum:
- bad_request
- unauthorized
- forbidden
- not_found
- conflict
- payload_too_large
- rate_limited
- service_unavailable
- internal_error
message:
type: string