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:
- Sign up → get a JWT token
- Create a project → note the
service_key - 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 / token | Use when | Policies |
|---|---|---|
| service_key | Trusted server-side code only | Bypassed entirely |
| (no header) | Public/anonymous frontend access | Enforced (anon role) |
| user JWT / PAT | Authenticated platform operators | Enforced (authenticated role) |
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.
| Body field | Type | Description |
|---|---|---|
| string | Valid email address | |
| password | string | Minimum 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).
curl -X POST "https://your-domain/api/v1/auth/login" \
-H "Content-Type: application/json" \
-d '{"email": "you@example.com", "password": "yourpassword"}'
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"
| Body field | Type | Description |
|---|---|---|
| current_password | string | Your current password |
| new_password | string | New 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"}'
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}
| Body field | Type | Description |
|---|---|---|
| token | string | The raw token from /auth/forgot |
| password | string | New 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..."}
user_reset_tokens table — see migration SQL below or in catalog_schema.sql.Projects
curl "https://your-domain/api/v1/projects" \ -H "Authorization: Bearer YOUR_TOKEN"
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.
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"
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
curl "https://your-domain/api/v1/projects/1/tables" \ -H "Authorization: Bearer YOUR_TOKEN"
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.
| Field | Type | Description |
|---|---|---|
| name | string | Column name |
| type | string | One of the supported data types |
| nullable | bool | Allow NULL values (default: true) |
| default | string | bool | Optional 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.
- anon — unauthenticated requests (no Authorization header)
- authenticated — requests with a valid user JWT, PAT, or project_user JWT
- service_role — service_key bypasses all policies entirely
- Project owner JWT also bypasses policies regardless of role
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.
| Query param | Default | Description |
|---|---|---|
| limit | 20 | Max rows to return (cap: 1000) |
| offset | 0 | Skip N rows (pagination) |
| col=value | — | Exact-match filter (shorthand for eq), e.g. ?status=active |
| col=op.value | — | Filter with operator: eq neq gt gte lt lte like — e.g. ?age=gte.18 or ?name=like.Alice%25 |
| order=col.dir | id DESC | Sort 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.
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.
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.
curl "https://your-domain/api/v1/data/1/users/42"
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"}'
curl -X DELETE "https://your-domain/api/v1/data/1/users/42" \ -H "Authorization: Bearer <service_key>"
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 field | Description |
|---|---|
| <identifier_col> | Any non-PASSWORD column to look up the row (e.g. email) |
| password | Plaintext 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.
.php .py .sh .exe .cgi .rb .pl .bat .cmd .htaccessSend 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"}
curl "https://your-domain/api/v1/projects/1/storage/avatars" \ -H "Authorization: Bearer YOUR_TOKEN"
Returns {"files": [{name, size, last_modified, url}, ...], "count": N}
curl -X DELETE "https://your-domain/api/v1/projects/1/storage/avatars/photo.jpg" \ -H "Authorization: Bearer YOUR_TOKEN"
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).
429 Too Many Requests with a Retry-After: 60 header. The file storage and auth endpoints are not rate-limited.Sites & Deploys
current/ is what end-users see.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.
curl "https://your-domain/api/v1/projects/1/sites/1" \ -H "Authorization: Bearer YOUR_TOKEN"
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.
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.
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.
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.
✓ 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.
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.
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"
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.
"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 }
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 }
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 }, ...] }
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 }
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.
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.
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.
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.
curl -X DELETE "https://your-domain/api/v1/auth/tokens/1" \ -H "Authorization: Bearer YOUR_TOKEN"