Stores
Memory, SQLite and Postgres, and what the conformance suite guarantees of all three.
Choosing one
| store | durable | use it when |
|---|---|---|
MemoryEntitlementStore | no | Tests, and reading the contract. Warns if used anywhere else. |
SqliteEntitlementStore | yes | The zero-config default. One machine, a file on disk. |
PostgresEntitlementStore | yes | Deployment. A container filesystem does not survive a redeploy; paid credits must. |
Memory
The reference implementation: correct, and deliberately not durable. consume is a real compare-and-swap on the record’s version — it snapshots, yields, and swaps only if nothing moved underneath. It exposes a test seam that forces the interleaving a naive read-then-write would lose to, and a negative control proves the seam has teeth. Construction warns unless NODE_ENV=test or acknowledgeEphemeral: true.
SQLite
better-sqlite3, WAL mode. consume runs in an IMMEDIATE transaction and keeps the version guard, so it holds both against concurrent calls in one process and against a second process on its own connection. claimSettlement is a primary-key insert, so exactly-once survives a restart.
One thing found in CI: several processes opening the same file all run the schema DDL, and under WAL that can return SQLITE_BUSY even with a busy timeout. Construction now retries briefly. Postgres handles the same case with an advisory lock.
Postgres
import { PostgresEntitlementStore } from '@tollbooth/store-postgres';
const store = new PostgresEntitlementStore({
connectionString: process.env.DATABASE_URL!, // Neon: use the *pooled* host
});
await store.ready(); // runs migrations under an advisory lock- Row lock plus version guard.
SELECT … FOR UPDATEserialises competing spenders; the version guard keeps the semantics identical to SQLite from either direction. - Migrations under an advisory lock, because two instances booting together after a deploy is the normal case.
- Neon idle suspension. Connection-level failures (
57P03,08006,ECONNRESET, “Connection terminated”) retry with jittered backoff. Errors the request itself caused never do. - A failed
COMMITis never retried. It may have applied before the acknowledgement was lost, and retrying would spend the same credit twice. It surfaces asAmbiguousCommitErrorinstead.
What the conformance suite guarantees
The contracts that lose money when they are wrong are written once, in @tollbooth/store-conformance, and run against every backend. A store that passes is safe to put in front of payments; one that does not is not, however well its own tests read. Seven groups:
- consume across the three pricing units, including all-or-nothing cost and the three failure reasons
- consume is atomic: 40 concurrent calls against 10 credits grant exactly 10; twelve separate processes against 4 credits grant exactly 4
- claimSettlement is exactly-once, including under 32 concurrent claimants
- charge bookkeeping: patches, pending lists, expiry sweeps
- subject handles round-trip and slide without rewriting creation time
- the three underpayment zones persist correctly
- survives a restart: balances, charges, handles, and the exactly-once claim
Memory declares no reopen, so its durability tests are skipped, not faked. When the suite was extracted, memory and SQLite passed it unchanged.measured
DATABASE_URL. Point it at a database on purpose with TOLLBOOTH_TEST_POSTGRES_URL, never at one holding paid entitlements.What the Neon run showed
All seven groups pass against a real Neon pooled endpoint, including the twelve-process contention test, with no behavioural difference from Postgres 15 or 16.measured · 2026-09-07 The only difference was latency from a laptop to us-east-1 — seconds per test rather than milliseconds — which is why deployment sits in the same region as the database. See troubleshooting for the errors each store raises.