This page exists so you never have to grep the source to find out
“can plasma do X?”. Every capability plasma exposes in v1.0 is
listed once, categorised, and linked to the guide that explains it.
| Capability |
How |
| Declare a table |
table("name", { id: id(), ... }) — Schema |
| Group tables into a schema |
defineSchema({ users, todos, ... }) |
| Nullable column |
.nullable() |
| Column default value |
.default(value) |
| Unique constraint |
.unique() |
| Encrypted at rest |
.encrypted() — Encryption |
| Foreign key |
ref(() => users.id, { onDelete }) — cascade / restrict / setNull / noAction |
| Auto UUID primary key |
id() — must be named id, one per table |
| String column |
text() |
| Integer column |
int(), bigint() |
| Boolean |
boolean() |
| Binary inline |
blob() (small bytes stored in the row) |
| JSON |
json() |
| Content-addressable file (R2) |
file({ maxSize?, mimeAllowList?, immutable?, upload? }) — Files |
| Grow-only counter |
crdtCounter() — CRDT |
| Signed counter |
crdtPnCounter() |
| Last-writer-wins register |
crdtLwwRegister<T>() |
| Observed-remove set |
crdtOrSet<T>() |
| Row-level auth (read/write) |
TableOptions.auth: { read, write } — Auth |
| Custom conflict merge |
TableOptions.resolveConflict — Conflict resolution |
| Local-only cache table |
TableOptions.changeLogSuppressed: true — Offline |
| Per-table storage adapter (schema-side declaration) |
TableOptions.blobs: storageRef("name") — v1.0 note: the sync handler currently only honours default; non-default names throw at startup |
The read side is drizzle-shaped. Every operator on the Schema
guide table above is composable in
db.select().from(...).where(...).orderBy(...).limit(...).offset(...).
| Capability |
How |
| Comparison |
eq, ne, gt, gte, lt, lte |
| Boolean logic |
and, or, not |
| Set membership |
inArray |
| Null checks |
isNull, isNotNull |
| Pattern match |
like |
| Ordering |
asc, desc |
| Aggregates |
count, sum, avg, max, min |
| Grouping |
.groupBy(col) — Query builder |
| Group filter |
.having(expr) — post-aggregate filter |
| Pagination — limit |
.limit(n) |
| Pagination — offset |
.offset(n) |
| Inner join |
.innerJoin(table, on) |
| Left join |
.leftJoin(table, on) |
| Subquery as FROM |
.fromSubquery(inner, "alias") + colRef("alias", "col") |
| Live subscription |
.live() returns LiveQuery<T> — Live queries |
| React binding |
useLiveQuery(factory, deps) |
Column projection (select({ x: todos.title })) is supported;
row types flow through the projected shape.
| Capability |
How |
| Define mutators |
defineMutators<S, Ctx>()({ name: async ({db, args, ctx}) => ... }) — Mutators |
| Args validation |
Standard Schema — Zod / Valibot / ArkType / Effect all work |
| Cross-table writes in one transaction |
Multi-table db.insert/update/delete inside one mutator |
| Access the mutation origin |
clientID, mutationID on the destructure |
| React binding |
useMutation<M, K>(name) returns { mutate, isPending, error, reset } |
| Server-only mutator run |
invokeMutator(mutators, name, {db, ctx, args}) — for scheduled jobs |
| Client-side one-shot mutate |
client.mutate("name", args) — instant IDB apply, enqueues outbox |
| Discard a queued mutation |
client.discardMutation(id) |
| Capability |
How |
| Start the sync loop |
client.start() — opens poll timer + WebSocket + upload worker |
| Stop the sync loop |
client.stop() |
| Wait for outbox drain |
await client.flush() |
| Manual push |
client.pushOnce() |
| Manual pull |
client.pullOnce() |
| Poll interval |
PlasmaClientOptions.pollIntervalMs |
| Retry policy |
PlasmaClientOptions.retry: { maxAttempts, initialDelayMs, maxDelayMs } |
| WebSocket subscription |
subscribe: createWebSocketSubscription({ url }) |
| Offline mode |
PlasmaClientOptions.offline: true — Offline |
| Auth headers |
authHeaders: async () => ({ authorization: ... }) |
| Custom fetch |
fetcher: async (input, init) => Response |
| Context provider |
getContext: async () => ({ ...ctx }) |
| Schema mismatch handler |
onSchemaMismatch: async ({ phase }) => "reset" | "stay" |
| Error hook |
onError: (err: SyncClientError) => void |
| Reset local state |
client.resetLocalState() — wipes IDB + rotates clientID |
| Capability |
How |
Upload from File / Blob |
Just pass it as the arg — desugared before outbox. Uint8Array / ArrayBuffer are not accepted (would clash with blob() column); wrap in a Blob first |
Read from a FileRef |
client.readFile(ref) or the usePlasmaFile(ref) hook |
| Blob upload retry policy |
blobUploadRetry: { maxAttempts, initialDelayMs, maxDelayMs } |
| Retry a failed upload |
client.retryBlobUpload(hash) |
| GC orphaned blobs |
gcOrphanedBlobs({ executor, storage, minOrphanAgeMs, limit }) — run from scheduled |
Rebuild _plasma_blob_refs after raw driver writes |
reconcileBlobRefs({ schema, executor, dialect }) |
| Per-table storage adapter (schema-side declaration) |
TableOptions.blobs: storageRef("name") — v1.0 note: the sync handler currently only honours default; non-default names throw at startup |
| Server-side blob adapter |
blobs: { default: r2Storage({ bucket }) } on SyncHandlerOptions |
| Blob read auth cap |
SyncHandlerOptions.readAuthMaxRefs (default 128) |
| Capability |
How |
| Subscribe to snapshot |
live.subscribe((rows) => ...) |
| Subscribe to diffs |
live.subscribeDelta?.((delta) => ...) — { added, removed, changed } |
| Await initial delivery |
await live.whenReady?.() |
| IVM eligibility inspection |
classifyIvm(ast) returns "select" | "aggregate" | "none" |
| Server-side live |
serverLiveSelect({ executor, fetch, onChange, tables?, pollIntervalMs? }) — Server-side operations |
Read helpers (call on the row’s stored value):
| Column type |
Read |
Merge |
crdtCounter |
sumCrdtCounter(map) |
mergeCrdtCounter(a, b) |
crdtPnCounter |
pnRead(map) |
mergePnCounter(a, b) |
crdtLwwRegister<T> |
lwwRead(reg, fallback) |
mergeLwwRegister(a, b) |
crdtOrSet<T> |
orSetValues(set) / orSetHas(set, v) |
mergeOrSet(a, b) |
Write helpers (build the new stored value inside a mutator):
| Column |
Helper |
crdtCounter |
crdtIncrement(clientID, delta, current) |
crdtPnCounter |
pnIncrement(clientID, delta, current), pnDecrement(clientID, delta, current) |
crdtLwwRegister |
lwwSet(clientID, value, ts, current) |
crdtOrSet |
orSetAdd(clientID, seq, value, current), orSetRemove(value, current) |
| Capability |
How |
| Column-level marker |
.encrypted() on a column |
| At-rest client-local wrapping |
PlasmaClientOptions.encryption: { dek, keyId } |
| Manual encryption inside a mutator (E2EE) |
encryptField(dek, aad, value) / decryptField(dek, aad, envelope) |
| Envelope wire format |
Envelope — AES-GCM-256, keyId-tagged, AAD from (table, rowId, column, keyId) |
| Envelope validation on incoming push |
validateEnvelope(env, { maxCiphertextBytes, allowedKeyIds }) |
| PQ hybrid wrapping |
encryptFieldPq(provider, aad, value) / decryptFieldPq(provider, aad, env) |
| PQ envelope |
PqEnvelope, isPqEnvelope |
| Provider interface for KEM |
PqHybridProvider |
| Insecure staging provider |
insecurePlaceholderProvider({ acceptInsecure: true }) — throws without opt-in |
| Capability |
How |
| Mount the handler |
createSyncHandler({ schema, mutators, executor, schemaVersion, auth, ... }) — Deployment |
| Custom path prefix |
basePath: "/api/plasma" (default "/sync") |
| Request auth |
auth: async (req) => ({ ok, clientGroupID, clientID, ctx }) |
| Post-push hook (poke fan-out) |
onPushed: async ({ clientGroupID, ... }) => pokeCoordinator(...) |
| Server-side error hook |
onError: (err: SyncServerError) => void — 11 kinds |
| Non-blocking Cache API put |
waitUntil: ctx.waitUntil.bind(ctx) |
| Cap mutations per push |
maxMutationsPerPush: 200 (default) |
| Cap payload size |
maxPayloadBytes: 1_048_576 (default 1 MiB) |
| Encryption envelope validation |
envelopeValidation: { maxCiphertextBytes, allowedKeyIds, maxKemCiphertextBytes } |
| Attach a blob storage |
blobs: { default: r2Storage({ bucket }) } |
| Cap blob read auth fan-out |
readAuthMaxRefs: 128 |
| Server-side db access outside handler |
createServerDb({ schema, executor, ctx }) |
| Authorization policy |
AuthorizePolicy — bind read / write from ctx to TableAuth predicates |
| Raise mutator authz errors |
throw new PlasmaAuthorizationError(...) |
| Capability |
How |
| First-run DDL (idempotent) |
ensureSchema({ schema, executor }) — call every request |
| Reconcile diff at deploy |
runMigrations({ schema, executor }) — Migrations |
| Refuse destructive migrations |
MigrationRefused — thrown on drops / renames / kind changes |
| Introspect the diff |
Returned SchemaDiff structure |
| Bump schema version |
SCHEMA_VERSION string in the shared schema file |
| Handshake behaviour |
409 on mismatch; client onSchemaMismatch chooses "reset" or "stay" |
| Capability |
How |
| Durable Object for WebSocket fan-out |
Export SyncCoordinator from your Worker |
| Trigger a WebSocket poke |
pokeCoordinator(env.COORDINATOR, clientGroupID) from @sh1n4ps/plasma-server/coordinator |
| Client-side WebSocket transport |
createWebSocketSubscription({ url, protocols? }) |
| Capability |
How |
Broadcast client userInfo payload |
createWebSocketSubscription({ userInfo, onPresence }) — Presence |
| Receive presence changes |
onPresence: (entries) => ... — full snapshot every time |
| Presence entry shape |
{ clientID, userInfo } — userInfo is any JSON |
| Room scoping |
clientGroupID by default; override with ?room= on the WebSocket URL |
| Update your own userInfo |
Reconnect with new payload (or throttle for cursor-like data) |
| Capability |
How |
| Per-region monotonic id source |
SequencerDO |
| Reserve id range |
stub.reserve(count) returns { start, end } as strings (bigint safe) |
| Storage backend for the DO |
SequencerStorage interface, MemorySequencerStorage for tests |
| Test-time simulator |
SequencerDoSimulator |
| Server-side version reservation |
reserveRegionVersions({ executor, regionId, count }) |
DDL for _plasma_region_versions |
ensureRegionVersionsTable({ executor }) |
| Causal cookie codec |
encodeCookie, decodeCookie, mergeCookie, cookieCovers |
| Advance a cookie from a change batch |
advanceCookieFromChanges(cookie, rows) |
| Pull predicate from cookie |
pullPredicate(cookie) — the SQL WHERE fragment |
| Pack / unpack region+version |
packRegionVersion, unpackRegionVersion |
| Constants |
REGION_VERSIONS_TABLE, CHANGES_TABLE |
| Capability |
How |
| Fold intermediate puts / del pairs |
compactChangeLog(executor, safeUpToVersion) — scheduled worker |
| Result stats |
CompactionResult |
| Capability |
How |
| Client engine against fake IDB |
createIdbEngine({ schema, dbName }) + fake-indexeddb/auto — Testing |
| SQLite executor for tests |
fromBetterSqlite3(sqlite) |
| Worker-side tests |
@cloudflare/vitest-pool-workers + Miniflare |
| Await first live delivery |
await live.whenReady?.() |
| Sequencer stub for tests |
SequencerDoSimulator + MemorySequencerStorage |
| Server-side db for isolated writes |
createServerDb({ schema, executor, ctx }) |
| Capability |
How |
| Wrap the tree |
<PlasmaProvider client={plasma}> |
| Read the current client |
usePlasma() |
| Live query |
useLiveQuery(factory, deps) |
| Mutation with state |
useMutation<M, K>(name) → { mutate, isPending, error, reset } |
| File resolution + Blob URL |
usePlasmaFile(ref) → FileHandle union (pending / local / uploading / ready / missing / error) |
| Capability |
How |
| In-page panel |
<PlasmaDevtools client={plasma} dbName="..." schema={schema} /> |
| Snapshot hook |
useDevtoolsSnapshot(client, options) |
| postMessage bridge |
attachDevtoolsBridge(client, { dbName, schema, targetOrigin, allowedOrigins, ... }) |
| Message discriminant |
PLASMA_DEVTOOLS_SOURCE = "plasma-devtools" |
| Type guard |
isDevtoolsMessage(evt.data) |
| Command kinds |
flush, pull, reset-local-state, ping |
| Capability |
How |
| Cloudflare D1 |
fromD1(env.DB) |
| better-sqlite3 (Node) |
fromBetterSqlite3(sqlite) |
| SQLite dialect |
sqliteDialect |
| Postgres dialect |
postgresDialect |
| SQL executor interface |
SqlExecutor — bring your own driver |
| SQL dialect interface |
SqlDialect — bring your own dialect |
| R2 storage adapter |
r2Storage({ bucket }) |
| Storage interface |
Storage — bring your own backend |
- Nested-relational query sugar (
db.query.x.findMany({ with })).
db.increment(col, delta) high-level CRDT counter API.
- Args-boundary envelope walker (so
.encrypted() E2EE stops
requiring encryptField() inside the mutator body).
- Runtime
client.setOffline(bool) toggle.
- Per-table blob storage adapter (declared today, refused at
runtime because handler only wires
default).
- Full per-region pull predicate (v1.0 handler simplifies to
hi = max(cursors); correct for single-region, has a
drop-window for heterogeneous multi-region).
See the Roadmap for the tracking status.