Skip to content
sluice

Stores & migrations

Two implementations of the same SluiceStore interface ship today: MemoryStore (in @jamessuuu/sluice — tests, the browser playground, the CLI's ephemeral mode) and @jamessuuu/sluice-store-postgres (Drizzle + Neon/node-postgres, forward-only migrations). Both pass the identical runStoreConformance suite exported from @jamessuuu/sluice-testkit — that suite is the contract for any future adapter, not the interface types alone.

Postgres store

import { drizzle } from "drizzle-orm/neon-http";
import { neon } from "@neondatabase/serverless";
import { createSluice } from "@jamessuuu/sluice";
import { createPostgresStore } from "@jamessuuu/sluice-store-postgres";

const db = drizzle(neon(process.env.DATABASE_URL!));
const sluice = createSluice({ store: createPostgresStore(db) });

createPostgresStore takes any Drizzle PgDatabase instance — Neon HTTP, node-postgres, or anything else Drizzle supports — so the package never imports a concrete driver as a runtime dependency.

Every store method is one SQL statement, which is what lets the same store run over Neon's HTTP driver inside a stateless serverless function (no interactive transaction held across an await). The hash-chain append (sluice_event/sluice_cursor) is a two-CTE statement that reads and bumps the per-namespace cursor and inserts the row in one round trip.

Migrations

Forward-only, additive-only — no column drops, no type narrowing, no destructive backfills, ever. Numbered SQL files under packages/sluice-store-postgres/migrations/, generated by drizzle-kit and checked into the repo.

pnpm --filter @jamessuuu/sluice-store-postgres run db:migrate
pnpm --filter @jamessuuu/sluice-store-postgres run db:seed

db:migrate + db:seed give a fresh clone a working local instance without touching a shared environment. CI runs the same conformance suite against a postgres:17 service container on every push — Neon itself is only the demo/production store, never a CI dependency.

Scheduled work (gates, sweeps)

sluice ships no scheduler (non-goal #4). The reference deployment for gates.claimDecided() / sluice.sweep() is a GitHub Actions cron, not Vercel's cron: Vercel Hobby limits cron to once a day within a ±59-minute window, which cannot resolve a gate on any useful timeline. GitHub Actions on public repos is free and supports 5-minute granularity — mind the 60-day auto-disable on schedules in inactive repos.

Writing your own adapter

Implement SluiceStore (14 methods — see the API reference), then run it through the conformance suite before trusting it:

import { runStoreConformance } from "@jamessuuu/sluice-testkit";

const report = await runStoreConformance(() => new MyStore());
if (report.failed.length > 0) {
  throw new Error(report.failed.map((f) => `${f.name}: ${f.message}`).join("\n"));
}
console.log(`${String(report.passed)}/${String(report.total)} conformance cases passed`);