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_..." }