For AI agents
These docs are published in forms meant for LLMs and coding agents:
| URL | Contents |
|---|---|
/llms.txt | Index of every page, with one-line summaries and links to Markdown |
/llms-full.txt | Every page as a single Markdown file |
/docs/<page>.md | Any single page as raw Markdown, e.g. /docs/sync.md |
To give an agent the whole library in one go, point it at
https://declarative-sqlite.linden.no/llms-full.txt.
The package also ships TypeScript declarations (dist/*.d.ts). When these
docs and the types disagree, the types are right.
Rules for writing code with this library
These are the mistakes that compile but break at runtime or corrupt sync state. Follow them in generated code.
- Install v3:
npm install declarative-sqlite.latestis v3; if you need the old API, pin a2.xversion explicitly. - Never write a
.synced()table directly. Not withdb.tables, and not withdb.executeortx.execute. Usesync.outbox.record({ table, systemId, changes }). outbox.recordonly changes existing rows. It can't create or delete synced rows; those come from the server by pulling. Don't write workarounds that insert into synced tables.- Supply
system_idon insert into a plain table (crypto.randomUUID()). It isn't generated. - Give every
.notNull()a default of the column's type:t.integer('n').notNull(0),t.text('s').notNull(''). - Declare every table a live query reads in
reads, joined tables included, and setkeyto the row id column (usuallysystem_id). - Close what you open:
query.close()for live queries you created,sync.close()beforeawait db.close(). - Create one database and one sync runtime per app, outside React
components, and hand them to
<SyncProvider>. - Use
useDraftFieldfor editable inputs bound to synced columns, notuseStateplusrecord. Convert numbers inonChange; the event value is a string. - Use lowercase names locally, uppercase on the wire.
sync.pull.pull('task', { project_id: 42 })andsync.ticks.notify({ table: 'task', … })take local names.FakeTransport.seed('TASK', …)and everything aSyncTransportsends or receives use wire names. - Don't declare tables named
outboxorsync_cursor, or recreate thesystem_id,system_removedorsync_seqcolumns with other types. - Throw from the transport on network failure. Don't return an empty
result: a thrown
pushis retried with the same batch id, while an invented answer is filed as the server's verdict. - Test with
MemoryAdapterandFakeTransport, not mocks of the library.
Minimal complete example
import { SchemaBuilder, openAdapter, Database, createSyncRuntime, type SyncTransport } from 'declarative-sqlite';
interface Rows {
task: { system_id: string; project_id: number; title: string; hours: number | null };
}
const schema = new SchemaBuilder();
schema
.table('task', (t) => {
t.integer('project_id').notNull(0);
t.text('title').notNull('');
t.real('hours');
})
.synced({ key: 'system_id', scope: ['project_id'] });
export async function start(transport: SyncTransport, deviceId: string) {
const { adapter, warnings } = await openAdapter({ name: 'app.db' });
warnings.forEach((w) => console.warn(w));
const db = await Database.open<Rows, 'task'>({ schema: schema.build(), adapter });
const sync = await createSyncRuntime({ db, transport, deviceId });
await sync.pull.pull('task', { project_id: 42 });
const tasks = db.live<Rows['task']>({
sql: 'SELECT * FROM task WHERE project_id = ? ORDER BY title',
params: [42],
reads: [{ table: 'task', scope: { project_id: 42 } }],
key: 'system_id',
});
tasks.subscribe((rows) => console.log(rows));
const first = (await db.queryOne<Rows['task']>('SELECT * FROM task LIMIT 1'))!;
await sync.outbox.record({ table: 'task', systemId: first.system_id, changes: { hours: 2 } });
return async () => {
tasks.close();
sync.close();
await db.close();
};
}