Skip to content

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.
plasma.config.ts
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,
},
})
src/shared/schema.ts
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:

Terminal window
plasma generate --name baseline

You get:

plasma/
migrations/
0000_baseline.sql
meta/
0000_snapshot.json
_journal.json
_manifest.json
schema-version.ts

Commit 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.

Terminal window
plasma generate --schema ./src/shared/schema.ts --out ./plasma --name add-priority

For 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.

Terminal window
plasma generate --check --schema ./src/shared/schema.ts --out ./plasma

Use 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.

Terminal window
plasma generate --watch

Watch 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.

Terminal window
# Local smoke test
plasma migrate deploy --local --local-db ./local.sqlite
# Production D1 REST API
plasma migrate deploy --wait 30

Deploy 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.

Terminal window
plasma migrate status --local --local-db ./local.sqlite
plasma migrate status --json

Status 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": [] }
Terminal window
plasma migrate baseline --local --local-db ./existing.sqlite
plasma migrate baseline --force

Baseline 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.

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.

The deploy runner uses a two-batch model because D1 cannot abort a batch once it is submitted:

  1. Bootstrap tracking and lock tables every run.
  2. Acquire _plasma_migrations_lock with a compare-and-swap lease.
  3. Insert a running row outside the migration SQL batch.
  4. Apply migration statements and the applied update in one batch.
  5. Mark verified or verified_mismatch after post-apply verification.
  6. On next startup, stale running rows are marked failed; --force allows 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.

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.

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"
},
})
  • plasma artifacts are missing — run plasma generate and commit the plasma/ directory.
  • generated artifacts differ from committed artifacts — run plasma generate, review the SQL/snapshot diff, and commit it.
  • migration generation refused — inspect JSON errors[].details; either make the schema additive or use --force after a manual data migration.
  • previous migration deploy crashed — inspect _plasma_migrations.logs; rerun with --force only if the SQL is idempotent for your database.
  • D1 credentials requiredgenerate --check does not need credentials; migrate deploy does unless you pass --local.