@sh1n4ps/plasma-server
v0.2.0
Published
Cloudflare Workers adapter for plasma. AST→SQL compiler (SQLite for D1 + Postgres for Hyperdrive), createSyncHandler with declarative row-level auth, SyncCoordinator Durable Object for realtime poke.
Downloads
74
Maintainers
Readme
@sh1n4ps/plasma-server
Cloudflare Workers adapter for plasma. Compiles the
query AST to SQL, ships the /sync/* handler, enforces declarative table
auth, and generates the AFTER-write triggers that keep the change log
authoritative — including when someone bypasses createServerDb with raw
drizzle or wrangler d1 execute.
Install
pnpm add @sh1n4ps/plasma-server @sh1n4ps/plasma-coreQuick shape
import {
createServerDb, createSyncHandler, ensureSchema, fromD1,
} from "@sh1n4ps/plasma-server"
import { SyncCoordinator, pokeCoordinator } from "@sh1n4ps/plasma-server/coordinator"
import { mutators, schema, SCHEMA_VERSION } from "./schema"
export { SyncCoordinator }
export default {
async fetch(req, env, ctx) {
await ensureSchema({ schema, executor: fromD1(env.DB) })
return createSyncHandler({
schema,
executor: fromD1(env.DB),
schemaVersion: SCHEMA_VERSION,
mutators,
auth: async (req) => resolveAuth(req),
onPushed: () => pokeCoordinator(env.SYNC_COORDINATOR),
})(req)
},
async scheduled(_, env) {
// Same isomorphic drizzle-flavored DSL as the mutators.
const db = createServerDb({ schema, executor: fromD1(env.DB) })
// ... cron work; all writes flow through the change log automatically.
},
} satisfies ExportedHandler<{ DB: D1Database, SYNC_COORDINATOR: DurableObjectNamespace }>What lives here
- AST → SQL compiler (
compileSelect / compileInsert / compileUpdate / compileDelete) targeting SQLite (sqliteDialect) and Postgres (postgresDialect). - Executor adapters:
fromBetterSqlite3(for tests) andfromD1(for production Workers).SqlExecutoris the porcelain interface; other backends (Hyperdrive via postgres.js, libsql, …) drop in the same way. ensureSchema— idempotent CREATE TABLE / INDEX / TRIGGER for the user schema plus the sync-side bookkeeping tables.createServerDb— aDb<S>bound to the driver, no auth, no optimistic layer. Use it forscheduled()cron jobs and admin tools.createSyncHandler— thefetch-style handler that servicesPOST /sync/pushandGET /sync/pull. Applies per-tableauth.read / auth.writerules; firesonPushedafter every accepted push so a Durable Object can broadcast a poke.SyncCoordinatorDO (from@sh1n4ps/plasma-server/coordinator) — a hibernation-aware WebSocket fan-out keyed by room.pokeCoordinatoris a two-line helper for the sync handler'sonPushedhook.
Design notes
- Trigger fallback: every user table has
_plasma_trg_<name>_insert / _update / _deletetriggers. Origin (client group / client / mutation id) is read from a scratch table_plasma_originthatcreateSyncHandlerpopulates inside the mutation's tx. Server-side writes done viacreateServerDbor raw drizzle are captured too, but without origin attribution — the client still receives them because the change log entry exists. - Declarative auth:
table("name", cols, { auth: { read, write } })attaches per-row rules. Push validates writes againstwrite(ctx, row)(both pre-image and post-image for updates). Pull filters put change records byread(ctx, row). Deletes always flow — the client can safely receive adelfor a row it never received. - Idempotent push:
last_mutation_idper(clientGroupID, clientID)turns a retried push into a no-op. A mutator that throws still advances the id in a separate transaction so the client isn't stuck retrying.
Runtime footprint
ESM only. Cloudflare Workers first — the workerd export condition
points at the same entry as import so bundlers pick it up unchanged.
cloudflare:sockets and cloudflare:workers are always externalized;
@sh1n4ps/plasma-server/coordinator is a separate entrypoint so importing the
main package doesn't drag cloudflare:workers into non-Workers builds.
