Payment safety depends on honest storage for idempotency (reserve / complete with lease fencing), webhook inbox (claim, lease, dual-mode processing), and reconciliation stores.
Source of truth: each package’s StorageAdapterManifest + shared testkit conformance suites — not marketing copy. Frozen matrix: ADAPTER_SELECTION_MATRIX in @paykernel/testkit and docs/adapter-capability-matrix.json. Live cross-check: scripts/check-adapter-selection-honesty.test.ts.
Domain reconciliation primitives live in @paykernel/reconciliation and inject any conforming ReconciliationStore — no mandatory queue product.
Core honesty
| Fact | Implication |
|---|---|
| Redis is optional | You do not need Redis to use @paykernel/core or the webhook engine. |
| Local SQLite is single-host only | Bun / Node / better-sqlite3 file DBs must not be used for multi-host coordination. :memory: is one process. |
| Memory is NON-PRODUCTION | MEMORY_STORAGE_ADAPTER_MANIFEST is test-only (single-process + ephemeral). |
| Matrix cells must match manifests | If a cell would overclaim, this guide uses the weaker honest value and names the limitation. |
No published adapter declares coordinationScope: "multi-region" |
Do not invent multi-region strong consistency. |
Turso has no /sync export |
Remote multi-host ≠ local SQLite. D1 ≠ Durable Objects ≠ Turso. |
@paykernel/internal-sql-store is private |
Do not install it. Relational adapters share schemas via publishable @paykernel/sql-foundation. |
Manifests are validated by assertStorageAdapterManifest from @paykernel/store-contracts (re-exported by @paykernel/testkit). Store interfaces are dual-owned across domain packages, @paykernel/store-contracts, and testkit re-exports — inject production adapters at the app layer.
Importing a store package does not apply DDL. Migrate explicitly (migratePostgresAdapter, migrateSqliteAdapter, migrateTursoAdapter, migrateD1Adapter, migrateDoAdapter).
Capability matrix
Distributed ≈ safe multi-worker coordination scope from coordinationScope (honest wording, not “multi-region” unless tested). Durable audit ≈ durability (rows/keys survive process restart under declared assumptions). Atomic claim ≈ consistency.claims: "strong" via engine-level ops (conditional SQL / Lua) — never get-then-set.
Cells match docs/adapter-capability-matrix.json.
| Adapter | Package | Distributed | Durable audit | Atomic claim | Best use | Important limitation |
|---|---|---|---|---|---|---|
| PostgreSQL | @paykernel/store-postgres |
Yes (multi-host) |
Yes (durable) |
Yes (strong) |
General production default when you already have (or will run) PostgreSQL | Needs managed/self-hosted DB; multi-primary without consensus is out of scope |
| Redis/Valkey (Bun, ioredis, node-redis) | @paykernel/store-redis /bun, /ioredis, /node-redis |
Yes (multi-host), except Bun: no Cluster/Sentinel |
Configuration-dependent (AOF/RDB / managed persistence) | Yes (atomic Lua) | Low-latency coordination, TTLs, lease claims | Optional infra; not automatic long-term audit alone; Bun rejects Cluster/Sentinel/clusterKeys |
| Upstash Redis | @paykernel/store-redis/upstash |
Yes (multi-host) |
Configuration-dependent | Yes (server-side EVAL) | Serverless coordination / idempotency over HTTP REST | HTTP/network model + platform persistence policy; same hybrid-audit caveats as other Redis |
| Bun SQLite | @paykernel/store-sqlite/bun |
No — single host (single-host) |
Yes with durable disk file (durable; :memory: is process-local) |
Yes (BEGIN IMMEDIATE + conditional SQL) |
Bun local / single-server apps | Not cross-host; no network FS sharing of the file |
| Node SQLite | @paykernel/store-sqlite/node |
No — single host | Yes (file-backed) | Yes | Node local / single server | node:sqlite stability varies by Node line (experimental); optional subpath only |
| better-sqlite3 | @paykernel/store-sqlite/better-sqlite3 |
No — single host | Yes (file-backed) | Yes | Mature Node SQLite deployments | Native dependency; synchronous API |
| Turso serverless | @paykernel/store-turso/serverless |
Yes (multi-host remote) |
Yes (durable) |
Yes after conformance (strong claims) | Shared remote SQLite-compatible store | Remote/async txn semantics; not local store-sqlite; no /sync export |
| libSQL | @paykernel/store-turso/libsql |
Yes remote multi-host; local file: is single-host testing only |
Yes remote; local file follows SQLite file rules | Yes after conformance | Existing Turso / @libsql/client projects |
Embedded-replica / offline multi-writer not advertised; no /sync; clients not interchangeable with /serverless |
| Cloudflare D1 | @paykernel/store-d1 |
Yes (multi-host, shared D1) |
Yes (durable) |
Yes (strong claims) |
Worker-native shared relational store | Not local SQLite, Turso, or DO; readAfterWrite: "session"; staleReadsPossible: true without Sessions under read replication |
| Cloudflare Durable Objects | @paykernel/store-durable-objects |
Yes, partitioned (multi-host + per-DO strong coordination) |
Yes (SQLite-backed DO) | Yes within a partition | Strong per-key / per-partition coordination and retries | Requires sharding; never one global DO; no global total order across partitions; not D1/shared multi-primary SQL |
| Memory (testkit) | @paykernel/testkit |
No — single process (single-process) |
No (ephemeral) |
Strong only in one isolate | Unit tests, local examples, conformance self-proof | NON-PRODUCTION. Never on a production payment path. Restart loses all state. |
Manifest field cheat sheet
| Adapter | coordinationScope |
durability |
consistency.claims |
readAfterWrite |
staleReadsPossible |
|---|---|---|---|---|---|
| postgres | multi-host |
durable |
strong |
strong |
false |
| redis (all bindings) | multi-host |
configuration-dependent |
strong |
strong |
false |
| sqlite (all bindings) | single-host |
durable |
strong |
strong |
false |
| turso (both bindings) | multi-host |
durable |
strong |
strong |
false |
| cloudflare-d1 | multi-host |
durable |
strong |
session |
true |
| cloudflare-do | multi-host (partitioned) |
durable |
strong |
strong (per DO) |
false (per partition) |
| memory | single-process |
ephemeral |
strong (isolate only) |
strong |
false |
Decision tree (answer in order; stop at the first clear fit)
| # | Question | Choice |
|---|---|---|
| 1 | Do you already run PostgreSQL that all workers will share? | @paykernel/store-postgres (general production default). Subpaths: . /pg /postgres-js /bun-sql /drizzle. |
| 2 | Cloudflare Workers? (including greenfield; do not jump to Turso; never @paykernel/store-sqlite — Workers are multi-isolate with no durable local FS) |
Need strong per-key / per-partition serialization (including if you already use D1)? → @paykernel/store-durable-objects with an explicit sharding strategy. Never route all payment work through one global Durable Object. Shared relational / greenfield (no existing D1)? → @paykernel/store-d1 as the Worker-native default. Prefer Sessions (first-primary / bookmarks) when read replication is on. |
| 3 | Ephemeral serverless FS / no durable local disk (not Workers)? | Do not use @paykernel/store-sqlite. Continue to remote SQLite (Turso), optional Redis, or STOP. |
| 4 | Bun app, single host, durable local file is acceptable? | @paykernel/store-sqlite/bun. |
| 5 | Node app, single host, local file OK? | /node or /better-sqlite3 on @paykernel/store-sqlite (prefer better-sqlite3 until node:sqlite is stable for your line). |
| 6 | Need multi-host remote SQLite-compatible storage (not a local file)? | @paykernel/store-turso (/serverless or /libsql). Not interchangeable with local SQLite or D1. No /sync. |
| 7 | Already operate Redis, Valkey, or Upstash for coordination? | Matching Redis binding; optionally hybrid with SQL for long-term audit. Bun + plain Redis URL → prefer /bun. Bun + Cluster or Sentinel → do not use /bun; use /ioredis or /node-redis. Upstash → /upstash. |
| 8 | No Redis today and moderate load? | Pick the primary relational / D1 / DO adapter from above. Do not add Redis only because PayKernel exists. |
Tests / examples only: createMemoryStores() from @paykernel/testkit — NON-PRODUCTION.
Same logic as a compact If / Then table:
| If | Then |
|---|---|
| Shared PostgreSQL already | @paykernel/store-postgres |
| Cloudflare + per-key serialization | @paykernel/store-durable-objects (sharded; never one global DO) |
| Cloudflare + shared relational / greenfield | @paykernel/store-d1 |
| Bun single host + durable local file | @paykernel/store-sqlite/bun |
| Node single host + durable local file | @paykernel/store-sqlite/node or /better-sqlite3 |
| Need remote SQLite-compatible multi-host | @paykernel/store-turso /serverless or /libsql (no /sync) |
| Already run Redis / Valkey / Upstash | Matching @paykernel/store-redis binding (optional hybrid with SQL) |
| Multi-host needed but only local SQLite | Refuse — Postgres, Turso, D1, or DO |
| Unit tests / in-process examples | @paykernel/testkit memory — NON-PRODUCTION |
Recommended defaults
- Existing PostgreSQL application →
@paykernel/store-postgres. Factories:createPostgresStores/ binding helpers (/pg,/postgres-js,/bun-sql,/drizzle). Example:createPostgresStoresFromPg({ client })+migratePostgresAdapter(executor). - Cloudflare application already using D1 →
@paykernel/store-d1. Factory:createD1PaymentStores({ db })+ explicitmigrateD1Adapter. - Cloudflare application needing strong per-key coordination →
@paykernel/store-durable-objects. Factory:createDoPaymentStores({ namespace, sharding })— sharding required; never global/singleton default. - Bun single-server application →
@paykernel/store-sqlite/bun. Factory:createBunSqliteStores({ db })+migrateSqliteAdapter. - Globally deployed app wanting remote SQLite compatibility →
@paykernel/store-turso. Prefer/serverless(createTursoServerlessStores) or/libsql(createLibsqlStores) against a shared remote primary (not local file as multi-host) +migrateTursoAdapter. - Existing Redis or Valkey needing fast coordination →
@paykernel/store-redismatching binding (createRedisStoresFromBun/FromIoredis/FromNodeRedis/FromUpstash), optionally paired with SQL/D1/Turso for durable audit. - Bun application already using Redis or Valkey → prefer
@paykernel/store-redis/bununless Cluster or Sentinel is required (then ioredis/node-redis). - No existing Redis and moderate workload → prefer the primary relational / D1 / DO adapter above; avoid adding Redis infrastructure solely for this SDK.
Real subpaths (do not invent others):
| Package | Subpaths |
|---|---|
@paykernel/store-postgres |
. /bun-sql /postgres-js /pg /drizzle |
@paykernel/store-redis |
. /bun /upstash /ioredis /node-redis |
@paykernel/store-sqlite |
. /bun /node /better-sqlite3 |
@paykernel/store-turso |
. /serverless /libsql — no /sync |
@paykernel/store-d1 |
. only |
@paykernel/store-durable-objects |
. only — never one global DO |
Honesty / anti-marketing
| Do not market… | Because |
|---|---|
| Local SQLite (Bun / Node / better-sqlite3) as multi-host or multi-region | Manifest: coordinationScope: "single-host" only |
| Redis as required for PayKernel | Redis adapter is optional; many apps use SQL/D1/DO alone |
| D1 as interchangeable with local SQLite, Turso, or Durable Objects | Separate packages, APIs, and consistency models |
| Durable Objects as one global object or shared multi-primary SQL | Partitioned strong coordination only; sharding required |
| Multi-region strong consistency for any adapter | No adapter manifest uses multi-region; most are multi-host at most |
Turso embedded-replica /sync |
Package has no ./sync export; offline multi-writer not advertised |
| Bun Redis Cluster / Sentinel support | /bun rejects Cluster/Sentinel; use ioredis/node-redis |
| Memory stores as production-safe | single-process + ephemeral — NON-PRODUCTION only |
| D1 strong read-after-write without sessions under replication | Manifest: readAfterWrite: "session", staleReadsPossible: true |
| Redis as blindly durable audit storage | durability: "configuration-dependent"; hybrid SQL preferred for long-term audit |
| libSQL and Turso serverless clients as drop-in interchangeable | Different subpaths; test independently |
When uncertain: fail closed (weaker guarantee, or refuse the deployment shape) rather than invent capability.
Package quick reference
| npm package | coordinationScope |
Primary factory example |
|---|---|---|
@paykernel/store-postgres |
multi-host |
createPostgresStores({ executor }) or createPostgresStoresFromPg({ client }) |
@paykernel/store-redis |
multi-host |
createRedisStoresFromBun / FromIoredis / FromNodeRedis / FromUpstash |
@paykernel/store-sqlite |
single-host |
createBunSqliteStores({ db }) / Node / better-sqlite3 counterparts |
@paykernel/store-turso |
multi-host |
createTursoServerlessStores / createLibsqlStores (+ migrateTursoAdapter) |
@paykernel/store-d1 |
multi-host |
createD1PaymentStores({ db: env.PAYMENTS_DB }) |
@paykernel/store-durable-objects |
multi-host (partitioned) |
createDoPaymentStores({ namespace, sharding }) |
@paykernel/testkit (memory) |
single-process |
createMemoryStores() |
Manifest constants:
| Constant | Package |
|---|---|
POSTGRES_STORAGE_ADAPTER_MANIFEST |
@paykernel/store-postgres |
REDIS_STORAGE_ADAPTER_MANIFEST |
@paykernel/store-redis |
SQLITE_STORAGE_ADAPTER_MANIFEST |
@paykernel/store-sqlite |
TURSO_STORAGE_ADAPTER_MANIFEST |
@paykernel/store-turso |
D1_STORAGE_ADAPTER_MANIFEST |
@paykernel/store-d1 |
DO_STORAGE_ADAPTER_MANIFEST |
@paykernel/store-durable-objects |
MEMORY_STORAGE_ADAPTER_MANIFEST |
@paykernel/testkit |
Helpers: assertStorageAdapterManifest, isProductionSafeCoordination, isStrongClaimAdapter from @paykernel/store-contracts (re-exported by @paykernel/testkit).
Agent checklist
When recommending an adapter:
- State package name + subpath (if any).
- State
coordinationScopeanddurabilityfrom the manifest, not assumptions. - For multi-host or multi-isolate need + local SQLite only → refuse and offer Postgres / Turso / D1 / DO. For Cloudflare Workers → D1 or DO, never
store-sqlite. - For Redis → say optional; mention Bun Cluster/Sentinel rejection when relevant.
- For D1 → mention session RAW / stale-read caveat under replication.
- For DO → require sharding; forbid global singleton.
- For Turso → no
/sync; remote multi-host ≠ local SQLite. - Never recommend memory for production paths.
See Getting started for Postgres composition, Store contracts, and SQL foundation.