Tables and rows
All row endpoints live under /api/projects/:id/tables/.... Every request must authenticate with the x-api-key header or the dashboard session cookie. The JavaScript examples below use the smoldb-js SDK. The curl examples hit the raw HTTP API.
JavaScript SDK
Import smoldb from the smoldb-js package. It provides a chainable client over the HTTP API.
import { smoldb } from "smoldb-js";
const db = smoldb({
url: "https://your-smoldb.com",
projectId: "<projectId>",
key: "smol_...",
});All row CRUD flows through db.from(tableName).
Creating a table
POST /tables with a name and a list of columns. smoldb adds id INTEGER PRIMARY KEY AUTOINCREMENT automatically. Do not declare it in the request.
curl -X POST $SMOLDB_URL/tables \
-H "x-api-key: $KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "todos",
"columns": [{ "name": "title", "type": "TEXT" }]
}'await db.tables.create("todos", [{ name: "title", type: "TEXT" }]);
// 201 { ok: true }Returns 400 if name or columns is missing.
Adding a column
POST /tables/:table/columns adds one column per request.
await db.from("todos").addColumn("done", "INTEGER");
// 201 { ok: true }On conflict, the response is 400 with the raw SQLite message (for example, duplicate column name: done). Surface that message to the caller.
Reading rows
Every SDK call returns { data, error }. No try/catch is required.
// all columns, all rows
const { data, error } = await db.from("todos").select("*");
// specific columns
const { data } = await db.from("todos").select("title, done");
// with a filter
const { data } = await db.from("todos").select("*").eq("id", 1);For the raw schema (PRAGMA table_info), call the HTTP API directly or use db.query("PRAGMA table_info('todos')").
Inserting
const { error } = await db.from("todos").insert({ title: "buy milk", done: 0 });Updating
Chain .update({...}).eq(column, value).
await db.from("todos").update({ done: 1 }).eq("id", 1);Deleting
Chain .delete().eq(column, value).
await db.from("todos").delete().eq("id", 1);Raw query
For operations the typed endpoints do not cover (aggregates, joins, PRAGMA), use db.query(). It supports ? placeholder binding:
const { data } = await db.query(
"SELECT COUNT(*) AS n FROM todos WHERE done = ?",
[1],
);
await db.query(
"INSERT INTO todos (title, done) VALUES (?, ?)",
["buy milk", 0],
);SELECT, WITH, EXPLAIN, and PRAGMA return { type: "query", rows: [...] }. Non-query statements return { type: "exec", result: { changes, lastInsertRowid } } when bound with params, or the raw better-sqlite3 result when called without params (which allows multi-statement scripts).
Always prefer ? placeholders for user-derived input. Raw string interpolation into SQL is still possible but opens SQL-injection risk.