API reference
All endpoints are scoped to a single project.
Base URL
{SMOLDB_URL}/api/projects/{projectId}
SMOLDB_URL is the deployment origin (http://localhost:3000 in development). projectId identifies a project owned by the caller.
Authentication
Every request must include one of:
| Credential | Used by |
|------------|---------|
| x-api-key: smol_... header | External applications |
| smoldb_session cookie | Dashboard |
Requests without credentials return 401. Route handlers verify project ownership after authentication.
Rate limits
Requests to /api/projects/* are rate-limited per credential using a token bucket.
| Plan | Limit | |------|-------| | free (x-api-key) | 60 req/min | | pro (x-api-key) | 600 req/min | | Dashboard (cookie) | 300 req/min |
Over-limit requests return 429 Too Many Requests with a Retry-After: 60 header and a JSON body:
{ "error": "Rate limit exceeded" }Limits are per-process; clients hitting multiple instances may briefly exceed the stated rate before buckets converge.
Plan limits
| Plan | Database | File storage | Projects | |------|----------|--------------|----------| | free | 50 MB | 200 MB | 2 | | pro | 500 MB | 2 GB | 10 |
Project
GET /
Returns project metadata.
{
"id": "...",
"name": "...",
"plan": "free",
"db_size": 16384,
"storage_used": 0
}DELETE /
Deletes the project, its SQLite database, and all file blobs.
Returns { "ok": true }.
Tables
GET /tables
Returns an array of table names.
["todos", "notes"]POST /tables
Creates a table. An id INTEGER PRIMARY KEY AUTOINCREMENT column is added automatically.
Request:
{
"name": "todos",
"columns": [{ "name": "title", "type": "TEXT" }]
}Response 201:
{ "ok": true }Returns 400 if name or columns is missing.
POST /tables/:table/columns
Adds a column to an existing table.
Request:
{ "name": "done", "type": "INTEGER" }Response 201:
{ "ok": true }Returns 400 with the SQLite error message on conflict, for example duplicate column name: done.
Rows
GET /tables/:table/rows
Returns the current schema, a page of rows, and the total row count.
Query params:
| Param | Range | Default |
|-------|-------|---------|
| limit | 1–500 | 100 |
| offset | ≥ 0 | 0 |
{
"schema": [{ "cid": 0, "name": "id", "type": "INTEGER", "pk": 1 }],
"rows": [{ "id": 1, "title": "buy milk" }],
"total": 1
}POST /tables/:table/rows
Inserts a row. The request body is the row itself.
{ "title": "buy milk" }Response 201:
{ "ok": true }A plan-based database size check runs before the insert.
PUT /tables/:table/rows
Updates a row by id.
Request:
{ "rowId": 1, "data": { "title": "new title" } }Response:
{ "ok": true }DELETE /tables/:table/rows
Deletes a row by id.
Request:
{ "rowId": 1 }Response:
{ "ok": true }Query
POST /query
Executes arbitrary SQL against the project database. Supports parameter binding via ? placeholders.
Request:
{ "sql": "SELECT * FROM todos WHERE id = ?", "params": [1] }The params field is optional. When omitted, the SQL runs verbatim.
Response depends on the statement.
For SELECT, WITH, EXPLAIN, or PRAGMA:
{ "type": "query", "rows": [...] }For INSERT, UPDATE, DELETE, CREATE, or any other non-query statement:
{ "type": "exec", "result": { "changes": 1, "lastInsertRowid": 42 } }(When params is omitted on non-query statements, result is the raw return of better-sqlite3's .exec(), which supports multi-statement scripts but does not report row counts.)
Returns 400 with the SQLite error message on failure, including parameter-count mismatches.
Use parameter binding for all user-derived values. Raw interpolation into SQL is still possible but opens you to injection. The typed row endpoints are also safe.
Files
GET /files
Returns an array of file metadata.
[
{ "id": "...", "name": "photo.jpg", "size": 12345, "created_at": "..." }
]POST /files
Uploads a file using multipart/form-data with a single file field.
Warning. Do not set Content-Type manually. The runtime must pick the multipart boundary. When using fetch, pass the FormData as the body and omit headers.
Response 201:
{ "id": "<fileId>" }| Status | Condition |
|--------|-----------|
| 413 | Incoming Content-Length exceeds the plan limit. Returned before the body is read. |
| 403 | Existing storage plus the upload exceeds the plan limit. Returned after the body is read. |
GET /files/:fileId
Streams the file bytes with Content-Disposition: attachment and X-Content-Type-Options: nosniff. The response is always a download — the server never renders uploaded bytes inline, since a user-supplied text/html file served inline would be a stored-XSS primitive. Clients that need to preview files must render them from a Blob (for example via URL.createObjectURL). Returns 400 if :fileId is not a UUID.
DELETE /files/:fileId
Deletes the blob and metadata row.
Response:
{ "ok": true }MCP
The smoldb-mcp package runs a Model Context Protocol server that exposes the endpoints above as tools. Add it to Claude Desktop by editing ~/Library/Application Support/Claude/claude_desktop_config.json:
{
"mcpServers": {
"smoldb": {
"command": "npx",
"args": ["-y", "smoldb-mcp"],
"env": {
"SMOLDB_URL": "https://your-smoldb.com",
"SMOLDB_API_KEY": "smol_...",
"SMOLDB_PROJECT_ID": "<projectId>"
}
}
}
}See MCP setup for the full tool listing.
API keys
Dashboard only. API-key callers cannot manage keys. Keys are stored hashed (scrypt) — the raw value is only returned once, at creation time. Losing a key requires regenerating it.
GET /key
Returns metadata about the current key. The raw value is never returned.
{ "key": null, "hashed": true, "prefix": "smol_abc123" }When no key exists: { "key": null, "hashed": false, "prefix": null }.
POST /key
Generates a new key, invalidates the previous one, and returns the raw value. Copy it immediately — it cannot be retrieved later.
{ "key": "smol_..." }