Migrations
plasma schema changes are generated ahead of deploy. The Worker runtime no longer creates or reconciles tables on the request path; it serves sync traffic against artifacts that were generated, reviewed, and applied by the CLI.
The six commands are:
| Command | Purpose |
|---|---|
plasma generate |
Build the next SQL migration, snapshot, journal, manifest, and schema version. |
plasma generate --check |
Rebuild artifacts and compare them byte-for-byte with what is committed. |
plasma generate --watch |
Watch schema.ts and run generate in a child process on change. |
plasma migrate deploy |
Apply unapplied SQL artifacts with a crash-safe lock and tracking table. |
plasma migrate status |
Print applied rows, current lock, and artifact/database drift. |
plasma migrate baseline |
Adopt an existing database as 0000_baseline after drift checks. |
Minimal project layout
Section titled “Minimal project layout”import { defineConfig } from "@sh1n4ps/plasma-cli/config"
export default defineConfig({ schema: "./src/shared/schema.ts", out: "./plasma", localDbPath: "./local.sqlite", d1: { accountId: process.env.CLOUDFLARE_ACCOUNT_ID, databaseId: process.env.CLOUDFLARE_D1_DATABASE_ID, apiToken: process.env.CLOUDFLARE_API_TOKEN, },})import { defineMutators, defineSchema, id, table, text } from "@sh1n4ps/plasma-core"
export const todos = table("todos", { id: id(), title: text() })export const schema = defineSchema({ todos })export const mutators = defineMutators<typeof schema, { userId: string }>()({})Run the first generation:
plasma generate --name baselineYou get:
plasma/ migrations/ 0000_baseline.sql meta/ 0000_snapshot.json _journal.json _manifest.json schema-version.tsCommit the whole directory. The generated schema-version.ts exports the
SCHEMA_VERSION value used by both the client and the Worker; do not hand-edit
or manually bump it.
Command examples
Section titled “Command examples”plasma generate
Section titled “plasma generate”plasma generate --schema ./src/shared/schema.ts --out ./plasma --name add-priorityFor an initial schema this emits a baseline SQL file. For later changes it reads
plasma/meta/_journal.json, loads the newest snapshot, builds a new snapshot
from your declared schema and mutator manifests, diffs them, refuses unsafe
changes, estimates migration size, and atomically swaps the artifact directory.
plasma generate --check
Section titled “plasma generate --check”plasma generate --check --schema ./src/shared/schema.ts --out ./plasmaUse this in CI. It does static validation and artifact drift detection only; it
does not require D1 credentials. Exit codes are stable: 0 means committed
artifacts match, 1 means validation/error, and 2 means drift.
--check --probe is opt-in for environments that can reach D1 and want live
batch-limit measurements.
plasma generate --watch
Section titled “plasma generate --watch”plasma generate --watchWatch mode forks a child process for each generate run so TypeScript module cache state from a previous schema import cannot leak into the next one.
plasma migrate deploy
Section titled “plasma migrate deploy”# Local smoke testplasma migrate deploy --local --local-db ./local.sqlite
# Production D1 REST APIplasma migrate deploy --wait 30Deploy first bootstraps _plasma_migrations and _plasma_migrations_lock, then
acquires a lease, records a running row before the SQL batch, applies the SQL
and completion update in a second batch, verifies when a verifier is configured,
and releases the lease. Failed or crashed runs are recorded in the tracking
table instead of being forgotten.
plasma migrate status
Section titled “plasma migrate status”plasma migrate status --local --local-db ./local.sqliteplasma migrate status --jsonStatus compares the artifact journal with _plasma_migrations and shows the
current lock holder. JSON output always has this envelope:
{ "version": "1", "exitCode": 0, "results": {}, "warnings": [], "errors": [] }plasma migrate baseline
Section titled “plasma migrate baseline”plasma migrate baseline --local --local-db ./existing.sqliteplasma migrate baseline --forceBaseline introspects an existing database, compares it with the declared schema,
and refuses drift unless --force is present. It writes 0000_baseline.sql, a
snapshot, journal, manifest, and a self-recording _plasma_migrations insert so
future deploys know the baseline was already adopted.
What is considered unsafe
Section titled “What is considered unsafe”Generation refuses changes that would lose data, break IndexedDB semantics, or
make the sync protocol ambiguous: table/column drops, kind changes, nullable to
NOT NULL, adding uniqueness, mutator required-arg additions, missing mutator
manifests, encryption key changes, and ref().onDelete semantic changes. Use
--force only when you have separately migrated data; it downgrades refuses to
warnings in the JSON/stderr output.
Crash-safe apply model
Section titled “Crash-safe apply model”The deploy runner uses a two-batch model because D1 cannot abort a batch once it is submitted:
- Bootstrap tracking and lock tables every run.
- Acquire
_plasma_migrations_lockwith a compare-and-swap lease. - Insert a
runningrow outside the migration SQL batch. - Apply migration statements and the
appliedupdate in one batch. - Mark
verifiedorverified_mismatchafter post-apply verification. - On next startup, stale
runningrows are markedfailed;--forceallows an idempotent retry.
The heartbeat stops issuing new migrations after lost lock, but lets an in-flight D1 batch finish so the database is never left with two active runners.
Hash breakdown and handshake
Section titled “Hash breakdown and handshake”Each snapshot produces five independent SHA-256 hashes: shape, mutators,
auth, protocol, and conflict. Their JCS-canonical composite hash becomes
the first 16 hex characters of SCHEMA_VERSION. The journal stores the full
breakdown so drift reports can say which dimension changed.
Servers can set minClientVersion; new projects should require clients at least
1.0.0, the first client line that understands generated schema versions,
"keep" mismatch handling, hash breakdowns, and IDB migration diagnostics.
Client IDB migration outcomes
Section titled “Client IDB migration outcomes”When a pulled schema version differs from local IndexedDB metadata, the client classifies the situation into four branches:
| Branch | Result |
|---|---|
| Empty outbox + compatible local cache | migrate metadata and continue. |
| Empty outbox + incompatible cache | reset local state automatically. |
Pending outbox + app returns "reset" |
discard local state and retry from server truth. |
Pending outbox + app returns "keep" |
keep IDB, mark user action required, and surface the mismatch. |
createPlasmaClient({ schema, mutators, endpoint: "/sync", schemaVersion: SCHEMA_VERSION, onSchemaMismatch: async (info) => { if (info.outboxCount === 0) return "reset" return "keep" },})Troubleshooting
Section titled “Troubleshooting”plasma artifacts are missing— runplasma generateand commit theplasma/directory.generated artifacts differ from committed artifacts— runplasma generate, review the SQL/snapshot diff, and commit it.migration generation refused— inspect JSONerrors[].details; either make the schema additive or use--forceafter a manual data migration.previous migration deploy crashed— inspect_plasma_migrations.logs; rerun with--forceonly if the SQL is idempotent for your database.- D1 credentials required —
generate --checkdoes not need credentials;migrate deploydoes unless you pass--local.