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.