@gj-kit/toss-payments-postgresql core concepts
Toss Payments stores on your own PostgreSQL, where forgetting encryption is a compile error.
Why this boundary exists
Section titled “Why this boundary exists”Wiring Toss Payments to your own database means hand-designing seven tables, a webhook dedupe transition that has to be atomic, and at-rest encryption for billing keys, virtual-account secrets, and cancel-retry tickets. Do it yourself and a read-then-insert dedupe lets two concurrent redeliveries of the same event both win the claim, and a store wired without a protector writes billing keys to disk in plaintext — neither one tells you until production.
Sensitive values require an application protector
Section titled “Sensitive values require an application protector”Pool lifecycle and key custody remain in the host application. Required protection prevents omitted encryption from persisting billing data in plaintext.
Migrations are an explicit deployment action
Section titled “Migrations are an explicit deployment action”The store aggregate exposes one migration step for deployment automation. Request handling and application startup can repeat under normal traffic.
What the package enforces
Section titled “What the package enforces”- Assembly without a protector won’t compile —
createTossPaymentsPostgres({ sql })is a type error —sensitiveValueProtectoris a required field on the aggregate and on all three sensitive store factories. - Raw strings are not lock keys — opaqueLocks.withLock accepts only a branded OpaqueAdvisoryLockKey, so passing a customer id as a raw string is rejected by tsc with TS2345.
- Types encode the transaction requirement — createPgBillingKeyStore rejects a SqlExecutor: SELECT … FOR UPDATE → decrypt → constant-time compare → DELETE has to run on one pinned connection inside one transaction, so only SqlClient — the interface that adds withConnection — typechecks.
- Webhook claim is one statement — In src/stores/webhook-dedupe.ts the claim transitions through a single CTE with INSERT … ON CONFLICT DO UPDATE, so exactly one of N concurrent redeliveries receives ‘claimed’.
- Deadlocks fail loudly in tests — The ./testing in-memory double throws MemoryLockContractError(‘nested-lock-api’ | ‘reentrant-lock’) exactly where PostgreSQL would silently hang.
What remains application-owned
Section titled “What remains application-owned”Use an app-owned KMS or key-management boundary for sensitive values, run explicit migrations once, and keep cleanup operations idempotent.
Continue with the two verified recipes.