Skip to main content
The reference server supports a normalized PostgreSQL engine store in addition to the default SQLite sandbox. Postgres mode gives you transactional, multi-instance-safe persistence suitable for a production deployment.
The migrations and repositories described here ship in @loyalty-interchange/storage-postgres, available on npm.

Start the Postgres profile

The quickest way to try Postgres mode is the bundled Docker Compose profile.
Postgres runtimes must use a direct endpoint. withLease() holds session-scoped advisory locks, so Neon -pooler hostnames and URLs carrying pgbouncer=true are rejected without echoing credentials. Transaction pooling remains unsupported until a separately reviewed lease design passes concurrency and failover conformance.
The SQLite runtime remains on port 3210. The Postgres-backed runtime is available on port 3211 by default. Set LIP_POSTGRES_PORT to change it.
Startup applies numbered migrations from @loyalty-interchange/storage-postgres.
LIP_RESET=true explicitly deletes the selected tenant/program engine state before seeding. Only set it when you intend to wipe that tenant’s data.

Data model

Core engine state is split into tenant-scoped tables for:
  • members and identity indexes
  • account balances
  • redemption reservations
  • immutable ledger entries
  • expiring balance lots and lot consumption
  • idempotency records
  • order accrual and adjustment indexes
  • issued rewards
Every table includes tenant_id and program_id in its key. JSONB payloads preserve forward-compatible protocol fields while indexed columns keep common operator and reconciliation queries relational.

Multi-instance behavior

Each protocol request runs the same transactional sequence:
1

Check out a client

The request checks out one database client.
2

Begin a transaction

A transaction is started for the operation.
3

Acquire an advisory lock

It obtains a tenant/program advisory transaction lock.
4

Reload the latest revision

The latest engine revision is reloaded.
5

Perform the operation

The requested protocol operation runs against the reloaded state.
6

Replace rows atomically

All normalized rows are replaced atomically and the revision advances.
7

Commit before responding

The transaction commits before the HTTP response is returned.
This prevents lost updates across server instances. Webhook events generated inside the operation are buffered and released only after commit.
PostgresEngineRepository.withLease() exposes session advisory leases for single-run schedulers and background jobs. PostgresJsonStateStore provides optimistic revisions for tenant-scoped extension state. The bundled Admin in Postgres mode runs the complete service suite — program publishing, campaign scheduling, memberships, engagement, access directory, the location registry, and durable webhook journals — against tenant-scoped PostgresJsonStateStore rows sharing one pool. Engine-mutating admin operations (membership grants, campaign runs, program publishes) run inside executeEngineOperation, so they commit through the same transactional revision flow as protocol traffic.

Location-scoped admin access

Admin queries and reporting are location-aware and fail closed. Users and API keys created with allowed_location_ids see only their own locations in the registry and in /admin/api/v1/reports/locations, which aggregates accrued balances, order counts, and reservation outcomes per location_id from the location-stamped ledger, plus an unattributed_present flag telling scoped callers that data exists outside their view. Scoped callers are denied (403 location_scoped_forbidden) on tenant-wide admin reads such as /admin/api/v1/snapshot, /analytics, and /exports/members. Location-filtered variants of those tenant-wide views are follow-up work.

Lock-free location reports

GET /admin/api/v1/reports/locations is served from a lock-free read path: the platform loads the last committed engine state and hydrates a throwaway engine (readEngineSnapshot), so report polling never takes the tenant advisory lock and never queues behind accruals or redemptions.
Staleness contract. A report may lag in-flight mutations by one revision, and expiry side effects computed during a read (for example, lapsed reservations) are counted in the report but not persisted until the next real write commits.
Run at most one platform instance per tenant. The engine repository is multi-instance-safe, but the Admin extension services cache state with per-process revisions (a concurrent writer surfaces as StateRevisionConflictError), and the webhook journals persist last-writer-wins snapshots under a single-dispatcher assumption. Multi-instance Admin and webhook serving needs per-delivery revisioned rows or a withLease-guarded singleton dispatcher — tracked as follow-up work.
The managed Render blueprint enforces the singleton constraint with separate sandbox and production services, independent Neon databases and roles, environment-specific disks, and numInstances: 1. Deployment, backup/restore, and rollback procedures are recorded in the repository’s managed-environment release runbook.

Integration test

The default suite does not require a database. To run the live adapter test, point it at a reachable Postgres instance:

Next steps

Reference platform

See how the reference server, Admin, and storage adapters fit together.

Migration

Move engine state onto Postgres and manage schema revisions.