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.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.@loyalty-interchange/storage-postgres.
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
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 withallowed_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.
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.