ajo

Application

Data that stays in sync

SQLite migrations, transactions and topic-based live updates.

SQLite through Kysely

ajo-kit/database exposes connect(), db(), close() and Kysely’s SQL tools. Keep reads explicit and bounded. Use a transaction for a logical write that changes several rows.

src/data.ts
import { connect, db as database } from 'ajo-kit/database'

type Data = { notes: { id: number; text: string } }
connect(process.env.DATABASE_PATH ?? 'app.sqlite')
export const db = () => database<Data>()

Evolve the schema deliberately

Every migration exports up() and down(). Each project or plugin owns a contiguous sequence starting at 0001. Qualified identities share one history, lock and transaction boundary.

Terminal
pnpm exec kit migrate create notes
pnpm exec kit migrate up
pnpm exec kit migrate status
# Revert the most recently executed migration:
pnpm exec kit migrate down

Track what a loader reads

A loader tracks each topic it depends on. After a durable write commits, an action emits the changed topics. Active routes revalidate over SSE and receive a complete route payload. For API handlers and other server work, use emit() from ajo-kit/server.

The read and write boundary
// In a loader, before reading notes:
req.track?.('notes')

// In an action, after the write commits:
action.emit('notes')

Give on-disk data a home

Declare a filesystem root in package.json, create that writable directory, and set AJO_DATA=/ajo/data. DATABASE_PATH is relative to it. An in-memory SQLite database does not need a filesystem grant.

Topics and SSE fanout live in one process. Multiple processes require shared coordination. Persistent SQLite also needs an operational backup and restore plan.

package.json
{
  "kit": {
    "engine": {
      "fs": { "roots": ["/ajo/data"] }
    }
  }
}