spec/ remain canonical. In particular, ledger entries are immutable and retries must preserve their original idempotency keys.
What the archive contains
lip state export creates a versioned, SHA-256 checksummed JSON archive with:
- the active program definition and fingerprint;
- members and privacy-preserving identity indexes;
- every account balance, expiration lot, and lot consumption;
- the immutable accrual, redemption, adjustment, and migration ledger;
- every reservation, including reservations still in
reservedstate; - issued rewards;
- request idempotency records; and
- business-identifier indexes for orders, adjustments, and redemptions.
The engine archive does not contain BFF customer accounts, sessions, consent, payment records, webhook secrets, pending webhook deliveries, Admin users, campaigns, memberships, or messaging connectors. Move those application and platform records separately. Recreate webhook configuration on the target and let the old host flush its webhook outbox before cutover.
Export and import commands
Use the exact active program JSON on both sides. Import rejects a different program fingerprint and refuses to replace existing engine state unless--force is explicitly supplied.
Export from SQLite:
The published container (available on npm at
0.1.0 under the @loyalty-interchange org) can run the same commands with node packages/cli/dist/cli.js in place of npm run lip --.Zero-loss cutover
1
Prepare the target
Provision its tenant, network path, API credential, program JSON, webhook destination, and webhook secret. Keep protocol writes disabled.
2
Freeze all source writes
Put the BFF’s signup and order mutation routes into maintenance mode, or stop/scale the BFF after draining requests. Freezing only accruals is insufficient: enrollment, reserve, capture, reverse, adjustment, and reward issuance also mutate loyalty state.
3
Drain in-flight checkout
Wait for payment callbacks and active BFF requests to finish. Flush the source webhook outbox. Record any reservations that remain open.
4
Export once
Run
lip state export against the source database. Save the printed member, balance, ledger, open-reservation, and idempotency counts with the archive checksum.5
Import once
Run
lip state import while the target is empty and frozen. The command verifies the checksum, program fingerprint, complete engine state, and target emptiness before saving.6
Verify before routing traffic
Compare archive counts, inspect several known member accounts and ledger histories, and run:
7
Repoint the BFF
Change
LIP_HOST and LIP_PORT (or LIP_URL) together with the target LIP_API_KEY, then restart the BFF. Do not expose that key to the mobile app.8
Canary while frozen
Read the program and a known account through the BFF. Confirm target logs receive the requests and that no request reaches the old host.
9
Unfreeze
Re-enable signup and order mutations. Place one controlled order, then confirm exactly one accrual ledger entry and any expected reserve/capture entries.
10
Retain the source read-only
Keep its database and archive until the rollback window closes. Securely delete temporary archive copies afterward.
Keep clocks synchronized. Open reservations retain their original expiration timestamps and can expire naturally during a long cutover. Finish within the reservation TTL or reverse those reservations before the freeze.
Why retries do not double-accrue
LIP requires a stable idempotency key per source system and operation. It also protects business identifiers such asorder_id, adjustment_id, redemption_id, and reservation_id.
The migration archive preserves both layers. If the source committed an accrual before export but the BFF lost the response, retrying that order against the target returns the imported result instead of posting another ledger entry. Derive mutation keys from stable order/cart identifiers, for example <order-id>-accrue, <order-id>-reserve, and <order-id>-capture.
Rollback
Before unfreezing the target, rollback is simply repointing the BFF to the still-frozen source. After the target accepts any write, do not repoint to the old snapshot: that would discard new transactions. Freeze again, export the target, import that state back to a clean source, verify it, and only then repoint traffic.Worked example: pilot cutover
This runbook has been exercised end to end on a production pilot: a full local rehearsal (seeded members, an open reservation surviving export/import, and byte-identical idempotency-key replay returning the original entry with no balance change), a read-only rehearsal against the live pilot database via aVACUUM INTO snapshot, and then a real zero-loss cutover from a self-hosted LIP to a public host using the exact sequence above:
- Freeze BFF mutations (
503+ a machine-readable freeze code on health) - Export from a consistent snapshot of the source database
- Import into an empty target tenant (no
--force); verify counts and a known member’s balance and ledger - Swap the BFF’s LIP base URL and API key together; restart
- Canary read, then one controlled write with a new, unused order key
- Unfreeze
order_id with different facts returned 409 conflict.
Operational records for the pilot (service topology, archive checksums, and member-level verification) live in the operator’s private runbook, not in this public repository.
Related guides
Postgres storage
Configure a Postgres-backed tenant as the migration target.
Reference platform
How the engine, storage adapters, and snapshots fit together.