@playfast/reform-db
v1.0.1
Published
Type-safe, PGlite-backed reactive SQL state for Reform — define tables once, read them as reactive Sources and write through Procedures, all typed via Kysely.
Maintainers
Readme
@playfast/reform-db
Type-safe, engine-neutral reactive SQL state for Reform.
Define tables once, read them reactively as Reform Sources, and write through
ordinary Procedures — all with column-level type-safety inferred through
Kysely. Built for on-device / local-first apps
(Electron, Electrobun, browser) where the database lives next to the UI and writes
are instant.
This package is the engine-neutral core: the schema/query/migration primitives
and the Db driver contract. It carries queries as engine-neutral Kysely operation
nodes and lets a storage engine recompile them to its own SQL dialect. Pick an
engine package to actually open a database:
@playfast/reform-db-pglite— Postgres-in-WASM (native live queries, dump/restore snapshots,idb://browser persistence).@playfast/reform-db-sqlite—bun:sqlite(Bun's built-in SQLite; emulated reactivity,serialize()snapshots).
Install
npm install @playfast/reform-db kysely
# plus one engine:
npm install @playfast/reform-db-pglite @electric-sql/pglite # PGlite
# or
npm install @playfast/reform-db-sqlite # bun:sqlite (Bun only)effect, kysely, and @playfast/reform are peer dependencies.
Quick Start
import { Db, DbColumn, DbQuery, DbSchema, DbTable, Migration } from '@playfast/reform-db'
import { Pglite } from '@playfast/reform-db-pglite'
import { Procedure } from '@playfast/reform'
import { Layer } from 'effect'
// 1. Define tables — each column carries its SQL type + a runtime Schema.
const Environments = DbTable.make('environments', {
columns: {
id: DbColumn.text({ primaryKey: true }),
name: DbColumn.text(),
status: DbColumn.literal(['idle', 'running', 'done']),
createdAt: DbColumn.timestamp(),
},
primaryKey: 'id',
})
// 2. One schema → one Kysely Database type, shared by reads AND writes.
const AppDb = DbSchema.make('app', { tables: [Environments] })
// 3. A reactive read — `yield* ActiveEnvironments` yields AsyncData<Row[]>.
const ActiveEnvironments = DbQuery.make('activeEnvironments', { output: Environments.Row })
const ActiveEnvironmentsLive = DbQuery.live(ActiveEnvironments, {
schema: AppDb,
inputs: [],
build: (_inputs, db) =>
db.selectFrom('environments').selectAll().where('status', '=', 'running').orderBy('createdAt'),
})
// 4. A write — a Procedure using Db.exec, typed against the SAME schema.
const CreateEnvironmentLive = Procedure.live(CreateEnvironment, {
run: ({ name }) =>
Db.exec(
AppDb.kysely
.insertInto('environments')
.values({ id: ulid(), name, status: 'idle', createdAt: Date.now() }),
),
})
// 5. Wire an engine at the root (renderer-side, instant writes).
const DataLayer = Layer.mergeAll(ActiveEnvironmentsLive, CreateEnvironmentLive).pipe(
Layer.provideMerge(Pglite.layer({ dataDir: 'idb://app', migrations: Migration.fromSchema(AppDb) })),
)Use Pglite.memory() (ephemeral) — or Sqlite.memory() — in tests and
@playfast/reform-proof. The Db driver is an interface, so swapping engines (or
using an in-memory one for tests) changes nothing else.
There is no DbMutation primitive: a write is a side effect inside an ordinary
Reform Procedure, reusing the existing mutation path rather than adding a parallel
concept.
Docs
- API reference: playbook/api.doc.md — full enumeration of
DbColumn/DbTable/DbSchema/DbQuery/Db/Migration, theDbEngineSPI (makeDriver,statementOf), and theSyncedStorebridge into Reform. - Engines:
@playfast/reform-db-pglite,@playfast/reform-db-sqlite. - Pairs with
@playfast/reform— the core render host andSyncedStore.
License
MIT
