API Reference

All endpoints live under /api/v1. Responses are JSON. Authentication uses the Authorization: Bearer <token> header.

Quick Start

Three steps to your first API call:

  1. Sign up → get a JWT token
  2. Create a project → note the service_key
  3. Create a table → call the data API (no auth needed for public/anon access)
# 1. Sign up
curl -X POST "https://your-domain/api/v1/auth/signup" \
  -H "Content-Type: application/json" \
  -d '{"email": "you@example.com", "password": "yourpassword"}'
# → {"token": "eyJ..."}

# 2. Use the token to create a project
curl -X POST "https://your-domain/api/v1/projects" \
  -H "Authorization: Bearer eyJ..." \
  -H "Content-Type: application/json" \
  -d '{"name": "My App"}'
# → {"id": 1, "name": "My App", "service_key": "eyJ..."}

# 3. Query your table (no auth header = anon role; subject to table policies)
curl "https://your-domain/api/v1/data/1/users"

API Keys

Each project has a service_key, visible in the dashboard's API tab. Unauthenticated (anon) requests are made without any auth header at all — they are subject to the table's row-level security policies.

Key / tokenUse whenPolicies
service_keyTrusted server-side code onlyBypassed entirely
(no header)Public/anonymous frontend accessEnforced (anon role)
user JWT / PATAuthenticated platform operatorsEnforced (authenticated role)
⚠ Never expose the service_key in client-side or publicly accessible code. Rotate it via POST /v1/projects/:id/rotate-service-key if compromised.

A Personal Access Token (PAT) acts as your login credential for the control plane (managing projects, tables, etc.) — useful in CI/CD. Create them in Account.

Platform Authentication

These endpoints are for SupaBein operators (developers who manage projects, tables, and deployments). To authenticate your app's own end-users within a project table, use the data API login endpoint.

POST /v1/auth/signup Create an account
Body fieldTypeDescription
emailstringValid email address
passwordstringMinimum 8 characters
curl -X POST "https://your-domain/api/v1/auth/signup" \
  -H "Content-Type: application/json" \
  -d '{"email": "you@example.com", "password": "yourpassword"}'

Returns {"token": "eyJ..."} — a JWT valid for the duration configured in your secrets.php (JWT_TTL).

POST /v1/auth/login Sign in, get a JWT
curl -X POST "https://your-domain/api/v1/auth/login" \
  -H "Content-Type: application/json" \
  -d '{"email": "you@example.com", "password": "yourpassword"}'
GET /v1/auth/me Get current user profile

Requires a valid user JWT or PAT. Returns id, email, role, created_at.

curl "https://your-domain/api/v1/auth/me" \
  -H "Authorization: Bearer YOUR_TOKEN"
PATCH /v1/auth/password Change your password (requires auth)
Body fieldTypeDescription
current_passwordstringYour current password
new_passwordstringNew password (min 8 characters)
curl -X PATCH "https://your-domain/api/v1/auth/password" \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"current_password": "old", "new_password": "new-password"}'
POST /v1/auth/forgot Generate a password-reset token (no auth required)

Returns a raw token in the response. Deliver it to the user via your own email flow. Token expires in 1 hour. Always returns the same shape to prevent email enumeration.

curl -X POST "https://your-domain/api/v1/auth/forgot" \
  -H "Content-Type: application/json" \
  -d '{"email": "you@example.com"}'

Response: {"message": "...", "token": "abc123...", "expires_in": 3600}

POST /v1/auth/reset Reset password with a token
Body fieldTypeDescription
tokenstringThe raw token from /auth/forgot
passwordstringNew password (min 8 characters)
curl -X POST "https://your-domain/api/v1/auth/reset" \
  -H "Content-Type: application/json" \
  -d '{"token": "abc123...", "password": "new-password"}'

On success returns a fresh JWT: {"message": "Password updated successfully.", "token": "eyJ..."}

Requires the user_reset_tokens table — see migration SQL below or in catalog_schema.sql.

Projects

GET /v1/projects List your projects
curl "https://your-domain/api/v1/projects" \
  -H "Authorization: Bearer YOUR_TOKEN"
POST /v1/projects Create a project
curl -X POST "https://your-domain/api/v1/projects" \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"name": "My App"}'

Response includes the new project's id, name, and service_key.

DELETE /v1/projects/:id Delete a project and all its data

Drops all physical MySQL tables, removes all site directories and storage files from disk, then deletes the project row (cascades to all catalog rows).

curl -X DELETE "https://your-domain/api/v1/projects/1" \
  -H "Authorization: Bearer YOUR_TOKEN"
POST /v1/projects/:id/rotate-service-key Issue a new service key (invalidates old one immediately)

Generates a new service_key JWT and stores it in the database. The previous service_key stops working as soon as this is called — update any consumers before rotating.

curl -X POST "https://your-domain/api/v1/projects/1/rotate-service-key" \
  -H "Authorization: Bearer YOUR_TOKEN"

Returns {"service_key": "eyJ..."}.

Tables

GET /v1/projects/:id/tables List tables in a project
curl "https://your-domain/api/v1/projects/1/tables" \
  -H "Authorization: Bearer YOUR_TOKEN"
POST /v1/projects/:id/tables Create a table
curl -X POST "https://your-domain/api/v1/projects/1/tables" \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"name": "users"}'

Table names must match ^[a-zA-Z_][a-zA-Z0-9_]{0,63}$. The physical MySQL table is named p{project_id}_{name}.

Columns

Supported data types: INT BIGINT VARCHAR(255) TEXT BOOLEAN DECIMAL(10,2) DATETIME DATE TIMESTAMP JSON FLOAT PASSWORD

PASSWORD columns are stored as bcrypt hashes. On read the value is always returned as null. Write a plaintext value and it is hashed automatically before saving. Use the data login endpoint to verify credentials.

POST /v1/projects/:id/tables/:name/columns Add a column
FieldTypeDescription
namestringColumn name
typestringOne of the supported data types
nullableboolAllow NULL values (default: true)
defaultstring | boolOptional default value. For BOOLEAN columns, true/false are accepted and coerced to 1/0 automatically.
curl -X POST "https://your-domain/api/v1/projects/1/tables/users/columns" \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"name": "email", "type": "VARCHAR(255)", "nullable": false}'

Policies (Row-Level Security)

Policies control which API roles can perform which operations on a table.

PUT /v1/projects/:id/tables/:name/policies Create or update a policy (single or batch)

Three formats accepted: a shorthand object (fewest keystrokes), a JSON array of explicit objects, or a single explicit object.

## Shorthand (recommended) — list allowed ops, the rest are auto-denied
curl -X PUT "https://your-domain/api/v1/projects/1/tables/posts/policies" \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -H "Content-Type: application/json" \
  -d '[
    { "api_role": "anon",          "allow": ["SELECT"] },
    { "api_role": "authenticated", "allow": ["SELECT", "INSERT", "UPDATE", "DELETE"] }
  ]'

## Explicit array — full control over each operation
curl -X PUT "https://your-domain/api/v1/projects/1/tables/posts/policies" \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -H "Content-Type: application/json" \
  -d '[
    { "api_role": "anon",          "operation": "SELECT", "allowed": true  },
    { "api_role": "anon",          "operation": "INSERT", "allowed": false },
    { "api_role": "authenticated", "operation": "SELECT", "allowed": true  },
    { "api_role": "authenticated", "operation": "INSERT", "allowed": true  }
  ]'

## Single object — one policy per call
curl -X PUT "https://your-domain/api/v1/projects/1/tables/posts/policies" \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{ "api_role": "anon", "operation": "SELECT", "allowed": true }'

constraint_sql is an optional WHERE clause appended to queries. Use :current_user_id as a placeholder for the authenticated user's ID (e.g. "user_id = :current_user_id"). Batch/shorthand response: {"updated": N, "policies": [...]}.

Data API

The data API lets you read and write rows in your tables. Requests with no Authorization header are treated as anon role. Operations are subject to the table's row-level security policies.

The project owner's JWT and the service_key both bypass all policies. Use the service_key only in trusted server-side code.
GET /v1/data/:project_id/:table List rows
Query paramDefaultDescription
limit20Max rows to return (cap: 1000)
offset0Skip N rows (pagination)
col=valueExact-match filter (shorthand for eq), e.g. ?status=active
col=op.valueFilter with operator: eq neq gt gte lt lte like — e.g. ?age=gte.18 or ?name=like.Alice%25
order=col.dirid DESCSort by column: ?order=name.asc or multiple: ?order=age.desc,name.asc

IDs in responses are numbers (integers), not strings.

The list response is a paginated envelope — always unwrap .data in client code:

# Anonymous (no auth header — subject to anon policy)
curl "https://your-domain/api/v1/data/1/users?limit=20&offset=0&status=active"

# Authenticated (service_key bypasses all policies)
curl "https://your-domain/api/v1/data/1/users?limit=20&offset=0&status=active" \
  -H "Authorization: Bearer <service_key>"

Returns: {"data": [{...}, ...], "count": N, "limit": N, "offset": N} — read rows from .data.

POST /v1/data/:project_id/:table Insert a row
curl -X POST "https://your-domain/api/v1/data/1/users" \
  -H "Content-Type: application/json" \
  -d '{"name": "Alice", "email": "alice@example.com"}'

Only fields that match defined columns are accepted. Extra fields are silently ignored.

POST /v1/data/:project_id/:table/batch Bulk insert rows (up to 500)

Send a JSON array of objects. Each row is inserted individually and subject to the same INSERT policy as a single insert. Returns all inserted rows.

curl -X POST "https://your-domain/api/v1/data/1/skills/batch" \
  -H "Authorization: Bearer <service_key>" \
  -H "Content-Type: application/json" \
  -d '[
    {"name": "PHP",        "category": "Backend",  "level": 5},
    {"name": "JavaScript", "category": "Frontend", "level": 4},
    {"name": "MySQL",      "category": "Database", "level": 5}
  ]'

Returns: {"inserted": 3, "rows": [{...}, {...}, {...}]}. Max 500 rows per request.

GET /v1/data/:project_id/:table/:id Get a single row by primary key
curl "https://your-domain/api/v1/data/1/users/42"
PATCH /v1/data/:project_id/:table/:id Update a row (partial update)
curl -X PATCH "https://your-domain/api/v1/data/1/users/42" \
  -H "Authorization: Bearer <service_key>" \
  -H "Content-Type: application/json" \
  -d '{"name": "Alice Smith"}'
DELETE /v1/data/:project_id/:table/:id Delete a row
curl -X DELETE "https://your-domain/api/v1/data/1/users/42" \
  -H "Authorization: Bearer <service_key>"
POST /v1/data/:project_id/:table/login Authenticate a row via a PASSWORD column

The table must have exactly one PASSWORD-type column. Provide a column that identifies the user (e.g. email) and the plaintext password. If the credentials match, a project_user JWT (role: authenticated) is returned and can be passed as Authorization: Bearer in subsequent data API calls.

Body fieldDescription
<identifier_col>Any non-PASSWORD column to look up the row (e.g. email)
passwordPlaintext password to verify against the stored hash
curl -X POST "https://your-domain/api/v1/data/1/users/login" \
  -H "Content-Type: application/json" \
  -d '{"email": "alice@example.com", "password": "secret123"}'

Returns {"token": "eyJ...", "row": {...}} — the JWT carries sub (row id), pid (project id), and type: "project_user". Use it as Bearer auth in subsequent data API calls to get the authenticated role.

File Storage

Store files (images, documents, assets) per project inside named buckets. Files are served publicly via a read-only URL — no auth needed to fetch them. Management endpoints (upload, list, delete) require operator auth.

Bucket names: 1–63 characters, lowercase letters/numbers/hyphens/underscores. Max file size: 50 MB. Blocked extensions: .php .py .sh .exe .cgi .rb .pl .bat .cmd .htaccess
POST /v1/projects/:project_id/storage/:bucket Upload a file to a bucket

Send as multipart/form-data with field name file. The bucket is created automatically if it doesn't exist.

curl -X POST "https://your-domain/api/v1/projects/1/storage/avatars" \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -F "file=@photo.jpg"

Returns {"name": "photo.jpg", "bucket": "avatars", "size": 24580, "url": "/api/v1/storage/1/avatars/photo.jpg"}

GET /v1/projects/:project_id/storage/:bucket List files in a bucket
curl "https://your-domain/api/v1/projects/1/storage/avatars" \
  -H "Authorization: Bearer YOUR_TOKEN"

Returns {"files": [{name, size, last_modified, url}, ...], "count": N}

DELETE /v1/projects/:project_id/storage/:bucket/:filename Delete a file
curl -X DELETE "https://your-domain/api/v1/projects/1/storage/avatars/photo.jpg" \
  -H "Authorization: Bearer YOUR_TOKEN"
GET /v1/storage/:project_id/:bucket/:filename Serve a file publicly (no auth)

Use the url value returned by upload or list. This URL is public — link to it directly from your frontend.

curl "https://your-domain/api/v1/storage/1/avatars/photo.jpg"

Returns the file with correct Content-Type and cache headers (Cache-Control: public, max-age=86400).

Rate limiting: The data API enforces a limit of 600 requests per minute per project. Excess requests receive 429 Too Many Requests with a Retry-After: 60 header. The file storage and auth endpoints are not rate-limited.

Sites & Deploys

Staging-first rule: Every deploy (zip upload, file-by-file finalize) lands in staging — never directly live. Only call Publish to live when explicitly instructed to do so. Staging is safe to overwrite at any time; current/ is what end-users see.
GET /v1/projects/:id/sites List sites for a project
curl "https://your-domain/api/v1/projects/1/sites" \
  -H "Authorization: Bearer YOUR_TOKEN"

Returns an array of site objects including id, subdomain, spa_mode, current_deploy_id, and created_at. Each project supports one site.

GET /v1/projects/:id/sites/:site_id Get a single site
curl "https://your-domain/api/v1/projects/1/sites/1" \
  -H "Authorization: Bearer YOUR_TOKEN"
DELETE /v1/projects/:id/sites/:site_id Delete a site and all its deploys
curl -X DELETE "https://your-domain/api/v1/projects/1/sites/1" \
  -H "Authorization: Bearer YOUR_TOKEN"

Permanently deletes the site record and all associated deploy records. The deployed files on disk are also removed.

POST /v1/projects/:id/sites Create a site (one per project)
curl -X POST "https://your-domain/api/v1/projects/1/sites" \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"subdomain": "myapp", "spa_mode": false}'

subdomain must be 2–63 lowercase alphanumeric characters or hyphens (e.g. "my-app"). name is accepted as an alias for subdomain. Set spa_mode: true for single-page apps — unknown paths will serve index.html instead of 404. Each project supports one site.

GET /v1/projects/:id/sites/:site_id/deploys List deploy history
curl "https://your-domain/api/v1/projects/1/sites/1/deploys" \
  -H "Authorization: Bearer YOUR_TOKEN"

Returns deploys newest-first with id, version_label, status (pending / processing / ready / failed), size_bytes, and uploaded_at.

POST /v1/projects/:id/sites/:site_id/deploys Deploy a zip file

Upload a zip archive (multipart/form-data, field name zipfile, max 50 MB). The zip must not contain .php, .sh, or other server-executable files. If the zip has a single wrapper folder, it's automatically unwrapped.

Zip structure: files must be at the root of the zip, not inside a subfolder.
✓ correct: cd dist && zip -r ../deploy.zip .
✗ wrong: zip -r deploy.zip dist/ — creates a dist/ subfolder inside the zip and the site will 404.
cd dist && zip -r ../deploy.zip . && cd ..
curl -X POST "https://your-domain/api/v1/projects/1/sites/1/deploys" \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -F "zipfile=@./deploy.zip" \
  -F "label=v1.0.0"

After a successful upload, the deploy lands in staging (not live yet).
Preview at: https://your-domain/sites/s{site_id}/staging/
Then call Publish to live to make it the active version.

POST /v1/projects/:id/sites/:site_id/deploys/:deploy_id/publish Promote staged deploy to live

Copies staging/ to current/, sets current_deploy_id, and clears staging_deploy_id on the site. The deploy must be the current staging deploy (its ID must match site.staging_deploy_id). Returns the updated site object including live_url and staging_url.

curl -X POST "https://your-domain/api/v1/projects/1/sites/1/deploys/5/publish" \
  -H "Authorization: Bearer YOUR_TOKEN"

Response includes live_url (the public URL) and staging_url for immediate use — no need to construct URLs manually.

POST /v1/projects/:id/sites/:site_id/deploys/:deploy_id/rollback Roll back to a previous deploy
curl -X POST "https://your-domain/api/v1/projects/1/sites/1/deploys/3/rollback" \
  -H "Authorization: Bearer YOUR_TOKEN"

File-by-File Deploy API

An alternative to zip uploads. Open a staging deploy, push individual files over HTTP, then finalize. Ideal for CI/CD pipelines and build tools.

# 1. Open a deploy (returns deploy_id)
DID=$(curl -sX POST "https://your-domain/api/v1/projects/1/sites/1/deploys/open" \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"label":"v2.1.0"}' | python3 -c "import sys,json; print(json.load(sys.stdin)['id'])")

# 2. Upload changed files
for f in dist/**/*; do
  [[ -f "$f" ]] || continue
  REL="${f#dist/}"
  curl -sX POST "https://your-domain/api/v1/projects/1/sites/1/deploys/$DID/files?path=$REL" \
    -H "Authorization: Bearer YOUR_TOKEN" \
    --data-binary "@$f"
done

# 3. Finalize (go live)
curl -sX POST "https://your-domain/api/v1/projects/1/sites/1/deploys/$DID/finalize" \
  -H "Authorization: Bearer YOUR_TOKEN"
POST /v1/projects/:id/sites/:site_id/deploys/open Open a new pending deploy

Creates an empty staging directory and returns a deploy record with status: "pending". Optionally accepts a label for version tracking. Use the returned id in subsequent file upload calls.

labelOptional version label (e.g. "v2.1.0"). Defaults to current timestamp.
curl -X POST "https://your-domain/api/v1/projects/1/sites/1/deploys/open" \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"label":"v2.1.0"}'

Returns: { id, site_id, version_label, status: "pending", staged_dir, uploaded_at }

POST /v1/projects/:id/sites/:site_id/deploys/:deploy_id/files?path= Upload a single file into the staging deploy

Send the raw file bytes as the request body. The ?path= query parameter specifies where the file goes relative to the site root. Subdirectories are created automatically. Blocked extensions (.php, .sh, etc.) return 403.

curl -X POST "https://your-domain/api/v1/projects/1/sites/1/deploys/7/files?path=assets/app.js" \
  -H "Authorization: Bearer YOUR_TOKEN" \
  --data-binary "@./dist/assets/app.js"

Returns: { path, size }

GET /v1/projects/:id/sites/:site_id/deploys/:deploy_id/files List all staged files in a deploy
curl "https://your-domain/api/v1/projects/1/sites/1/deploys/7/files" \
  -H "Authorization: Bearer YOUR_TOKEN"

Returns: { deploy_id, status, files: [{ path, size }, ...] }

DELETE /v1/projects/:id/sites/:site_id/deploys/:deploy_id/files?path= Remove a staged file from a pending deploy

Only works while the deploy is still in pending status.

curl -X DELETE "https://your-domain/api/v1/projects/1/sites/1/deploys/7/files?path=old-page.html" \
  -H "Authorization: Bearer YOUR_TOKEN"

Returns: { deleted: true, path }

POST /v1/projects/:id/sites/:site_id/deploys/:deploy_id/finalize Finalize a pending deploy (moves to staging)

Writes the hardening .htaccess, moves files to the staging directory, and marks the deploy ready. Returns 422 if no files were uploaded (empty deploy). The site is not live yet — call Publish to live to promote it.

curl -X POST "https://your-domain/api/v1/projects/1/sites/1/deploys/7/finalize" \
  -H "Authorization: Bearer YOUR_TOKEN"

Returns the updated deploy record with status: "ready" and staging_url.

GET /v1/projects/:id/sites/:site_id/deploys/:deploy_id/diff?vs=:other_id Compare two deploy snapshots

Compares the file tree of :deploy_id against ?vs=:other_id using SHA-256 hashes. Both deploys must be in ready status. Files in added are present in :deploy_id but not in vs; removed is the inverse.

curl "https://your-domain/api/v1/projects/1/sites/1/deploys/7/diff?vs=5" \
  -H "Authorization: Bearer YOUR_TOKEN"
{
  "deploy_id": 7,
  "vs": 5,
  "added":    ["assets/new-logo.svg"],
  "removed":  ["old-page.html"],
  "modified": ["index.html", "assets/app.js"],
  "unchanged": 12
}

Personal Access Tokens

PATs authenticate as your user account across all projects. Use them in scripts or CI/CD pipelines instead of storing your password.

GET /v1/auth/tokens List your PATs
curl "https://your-domain/api/v1/auth/tokens" \
  -H "Authorization: Bearer YOUR_TOKEN"

Token values are never returned after creation. Only id, name, created_at, and last_used_at are shown.

POST /v1/auth/tokens Create a PAT
curl -X POST "https://your-domain/api/v1/auth/tokens" \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"name": "CI deploy"}'

Returns {"token": "sb_pat_..."}. The raw token value is shown only once — store it immediately.

DELETE /v1/auth/tokens/:id Revoke a PAT
curl -X DELETE "https://your-domain/api/v1/auth/tokens/1" \
  -H "Authorization: Bearer YOUR_TOKEN"