Custom Tables
Structured content — posts, products, events, anything tabular. Create tables through the API or MCP, then query them from templates and handlers.
Creating tables
Table names must start with __. An id (TEXT PRIMARY KEY), created_at, and updated_at are added automatically:
POST /api/tables
{
"name": "__posts",
"columns": [
{ "name": "title", "type": "TEXT", "nullable": false },
{ "name": "slug", "type": "TEXT", "unique": true },
{ "name": "body", "type": "TEXT" },
{ "name": "published", "type": "BOOLEAN" },
{ "name": "views", "type": "INTEGER" },
{ "name": "rating", "type": "REAL" }
]
}
Supported column types: TEXT, INTEGER, REAL, NUMERIC, BLOB, BOOLEAN, DATETIME, DATE, JSON. Each column accepts nullable (default true) and unique.
SQL Guard applies: the db data source and ctx.db.query only run data statements. Schema lives here — in the custom tables API — not in templates.
Querying from templates
Use the db data source with the __-prefixed name:
{
"posts": {
"type": "db",
"query": "SELECT * FROM __posts ORDER BY created_at DESC LIMIT 10"
},
"post": {
"type": "db",
"queryOne": "SELECT * FROM __posts WHERE slug = ?",
"params": ["params.slug"]
}
}
Querying from handlers
const posts = await ctx.db.query("SELECT * FROM __posts WHERE published = 1");
return ctx.json(posts.rows);
Per-table CRUD
Creating a table automatically exposes 5 CRUD tools over MCP and the API — list, get, create, update (upsert), delete — scoped to that table. Your agent can manage rows without writing any SQL:
▸ "Add a blog with posts and tags"
✓ table __posts created (+ 5 CRUD tools)
✓ pages /blog and /blog/:slug wired to it
Dropping tables
drop_custom_table (or DELETE /api/tables/:name) removes the table and all its data — the CLI's snapshot tool gives you a restore point if you need one.