S
Store

Adapter Migration

Migrate data between adapters using the export/import pipeline without downtime.

Why migrate?

As your application grows, you may need to move from one adapter to another. Common migrations include SQLite to Postgres (scaling beyond a single machine), local SQLite to a remote Postgres sync server, or consolidating multiple SQLite files into a single Postgres database.

Export/import pipeline

Store exposes migration planning on the client and portable archive helpers for backup and restore workflows. Use a dry-run plan before changing routes or moving state.

import { createStore } from "@cuitty/store/client";

const store = await createStore({
  module: "app",
  profile: {
    id: "migration",
    name: "Migration",
    local: { adapter: "local.sqlite", path: "./data/app.db" },
    remotes: [{ id: "postgres", adapter: "postgres.record", dsnRef: "secret://pg" }],
    routes: [{ namespace: "users", class: "record", localAdapter: "local.sqlite", remoteAdapters: ["postgres"] }],
    encryption: { atRest: "preferred", remote: "required", p2p: "disabled" },
    sync: { mode: "manual" },
  },
  schema: { users: { class: "record" } },
});

const plan = await store.migrate.dryRun({
  from: "local.sqlite",
  to: "postgres.record",
  namespace: "users",
});

console.log(plan.steps.map((step) => step.label));

CLI migration

The CLI provides a simpler interface for common migrations:

# Export to a portable format
cui store export my-app --format jsonl --output ./backup.jsonl

# Import into a new store
cui store import my-app --from ./backup.jsonl \
  --adapter postgres \
  --connection "postgresql://user:pass@host:5432/store"

The JSONL format stores one record per line, making it easy to inspect, filter, or transform with standard Unix tools.

Zero-downtime migration

For production workloads, use a route-based dual-write pattern:

  1. Set up the destination store with the new adapter.
  2. Add the destination remote to the profile route while local writes still commit first.
  3. Run the backfill to copy existing data from source to destination.
  4. Verify that both stores have identical data.
  5. Switch reads to the destination.
  6. Disable dual-write and decommission the source.
const route = {
  namespace: "users",
  class: "record",
  localAdapter: "local.sqlite",
  remoteAdapters: ["postgres"],
};

await store.sync.flush();

Data integrity

The migration pipeline checksums each batch and verifies that the destination matches the source. If a batch fails, the pipeline retries with exponential backoff. After migration, run cui store verify to compare record counts and checksums between source and destination.

cui store verify my-app \
  --source sqlite:./data/app.db \
  --target postgres:postgresql://user:pass@host/store