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`);