Quick Start
In plasma:
- the browser runs your mutators optimistically against IndexedDB
- the Worker runs the same mutators canonically against D1
- plasma owns sync, push, pull, rebase, live queries, and retries
You write one schema and one set of mutation functions. Both runtimes use them.
-
Define schema and mutators once
Section titled “Define schema and mutators once”Create a shared file both the browser and the Worker will import.
src/schema.ts import {defineMutators,defineSchema,eq,id,int,table,text,} from "@sh1n4ps/plasma-core"export const todos = table("todos", {id: id(),title: text(),done: int().default(0),updatedAt: int(),})export const schema = defineSchema({ todos })export const SCHEMA_VERSION = "todos-v1"interface Ctx { userId: string }export const mutators = defineMutators<typeof schema, Ctx>()({createTodo: async ({ db, args }) => {await db.insert(todos).values({id: args.id,title: args.title,updatedAt: args.updatedAt,})},markDone: async ({ db, args }) => {await db.update(todos).set({ done: 1, updatedAt: args.updatedAt }).where(eq(todos.id, args.id))},})The
defineMutators<typeof schema, Ctx>()shape captures both the schema (sodb.insert(todos)is type-checked) and the ctx type (soctx.userIdis available inside every mutator). -
Mount the sync endpoint on a Worker
Section titled “Mount the sync endpoint on a Worker”worker.ts import { createSyncHandler, ensureSchema, fromD1 } from "@sh1n4ps/plasma-server"import { schema, mutators, SCHEMA_VERSION } from "./schema"interface Env {DB: D1Database}export default {async fetch(req: Request, env: Env): Promise<Response> {const executor = fromD1(env.DB)await ensureSchema({ schema, executor })const handler = createSyncHandler({schema,mutators,executor,schemaVersion: SCHEMA_VERSION,auth: async (req) => ({ok: true,clientGroupID: req.headers.get("x-group") ?? "demo",clientID: req.headers.get("x-client") ?? "anonymous",ctx: { userId: req.headers.get("x-user") ?? "demo-user" },}),})return handler(req)},}ensureSchemaruns additively-safe DDL: it creates the tables, the change-log trigger set, and plasma’s internal bookkeeping tables.authis called on every request; return{ ok: false, reason }to reject. -
Point the browser at the same schema
Section titled “Point the browser at the same schema”src/main.tsx import { createPlasmaClient } from "@sh1n4ps/plasma-client"import { PlasmaProvider } from "@sh1n4ps/plasma-react"import { createRoot } from "react-dom/client"import { schema, mutators, SCHEMA_VERSION } from "./schema"import { TodoList } from "./TodoList"export const plasma = createPlasmaClient({schema,mutators,dbName: "todos",endpoint: "/sync",clientGroupID: "demo",schemaVersion: SCHEMA_VERSION,getContext: async () => ({ userId: "demo-user" }),})plasma.start()createRoot(document.getElementById("root")!).render(<PlasmaProvider client={plasma}><TodoList /></PlasmaProvider>,)plasma.start()opens the pull polling loop. Realtime pokes over WebSocket are opt-in — passsubscribe: createWebSocketSubscription({...})tocreatePlasmaClientwhen you’re ready. Theendpointvalue is resolved againstlocation.originin the browser; on Node / Workers you must pass an absolute URL.Note that
plasmaisexported here soTodoList.tsxcan import it below. -
React to writes with useLiveQuery
Section titled “React to writes with useLiveQuery”src/TodoList.tsx import { useLiveQuery, useMutation } from "@sh1n4ps/plasma-react"import { asc, eq } from "@sh1n4ps/plasma-core"import { plasma } from "./main"import { mutators, todos } from "./schema"export function TodoList() {const rows = useLiveQuery(() => plasma.db.select().from(todos).where(eq(todos.done, 0)).orderBy(asc(todos.updatedAt)),[],)const create = useMutation<typeof mutators, "createTodo">("createTodo")const markDone = useMutation<typeof mutators, "markDone">("markDone")return (<div><buttononClick={() => create.mutate({id: plasma.newId(),title: "Try plasma",updatedAt: Date.now(),})}>Add todo</button><ul>{rows.map((row) => (<li key={row.id}>{row.title}<button onClick={() => markDone.mutate({id: row.id,updatedAt: Date.now(),})}>done</button></li>))}</ul></div>)}The
useLiveQueryresult updates the moment either:- the local mutation fires (optimistically), or
- the pull loop delivers a change from the server (canonically).
You never subscribe explicitly. plasma’s IVM (Incremental View Maintenance) tracks which rows are in the query’s window and pushes diffs to your component.
What just happened
Section titled “What just happened”You wrote schema.ts once.
- The browser used it for typed local queries and optimistic writes.
- The Worker used it for canonical writes to D1.
- The same
createTodomutator ran in both places. - Live queries updated the moment the local mutation landed, then the sync loop reconciled with the server in the background.
- Sync, retry, rebase, and the change log were handled by plasma.
Actually run it
Section titled “Actually run it”The four files above are the shape of a plasma app but not a
complete runnable project — you still need wrangler.jsonc, a
Vite proxy for the browser, and the two-terminal dev loop.
Head to the Todo App recipe for a copy-paste runnable version with the full project layout, D1 setup commands, and a note on local vs deployed.
- Todo App recipe — the runnable version of what’s above (start here to actually see it work)
- Next Steps — where to go once the todo app runs
- Mental model — the three layers that make one schema serve two runtimes
- Guides / Deployment — deploying the Worker + D1 setup to Cloudflare production