@gj-kit/toss-payments-postgresql
Toss Payments stores on your own PostgreSQL, where forgetting encryption is a compile error.
- npm
0.5.2 - 0 runtime dependencies —
pgisn’t even a peer - 55 @ts-expect-error compile guards
- 250+ unit tests · 31 more against real PostgreSQL
- 7 tables, one explicit
migrate()
Keep learning
Section titled “Keep learning”Why this exists
Section titled “Why this 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.
What it does about it
Section titled “What it does about it”- 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.
Golden path
Section titled “Golden path”Outcome: PostgreSQL-backed Toss stores with one explicit migration step.
1. Install
Section titled “1. Install”pnpm add @gj-kit/toss-payments-postgresql2. Keep the app-owned boundary explicit
Section titled “2. Keep the app-owned boundary explicit”Adapt your pool with fromPgPool, use a real encrypted sensitiveValueProtector, and run migration during deployment.
3. Start with the smallest integration
Section titled “3. Start with the smallest integration”Copy this first, then replace only the app-owned values named above.
import { createTossPaymentsPostgres, fromPgPool, type PgPoolLike, type SensitiveValueProtector } from '@gj-kit/toss-payments-postgresql';
declare const pool: PgPoolLike;declare const sensitiveValueProtector: SensitiveValueProtector; // App KMS/encryption boundary.
export const stores = createTossPaymentsPostgres({ sql: fromPgPool(pool), sensitiveValueProtector,});
// Run await stores.migrate() once in deployment, never per request.What that looks like
Section titled “What that looks like”The protector is a required field and a raw identifier cannot stand in for a lock key — tsc rejects both, at TS2345, before anything reaches a database. withOpaqueMutationLock is also the only API that takes both locks, so the opaque → customer order is not a decision the caller gets to make.
import type { Pool } from 'pg';import type { BillingKeyRecord } from '@gj-kit/toss-payments/server';import { createAes256GcmSensitiveValueProtector, createOpaqueAdvisoryLockKey, createTossPaymentsPostgres, fromPgPool } from '@gj-kit/toss-payments-postgresql';
declare const pool: Pool; // app owns the pg Pooldeclare const keyHex: string; // app owns key custody and rotationdeclare const blindIndex: string; // app owns the blind index of the customer iddeclare const record: BillingKeyRecord; // app owns the freshly issued billing key
export const pg = createTossPaymentsPostgres({ sql: fromPgPool(pool), sensitiveValueProtector: createAes256GcmSensitiveValueProtector({ key: keyHex, keyId: '2026-08' }),});// Without sensitiveValueProtector: TS2345 '{ sql: SqlClient; }' is not assignable to 'TossPaymentsPostgresOptions'.
export const previous = await pg.billingKeys.withOpaqueMutationLock( createOpaqueAdvisoryLockKey(blindIndex), // raw blindIndex: TS2345 'string' is not assignable to 'OpaqueAdvisoryLockKey' record.customerKey, (mutation) => mutation.replaceAndGetPrevious(record),);Use it when
Section titled “Use it when”Use it when Toss payment stores need a proven PostgreSQL implementation while your app retains connection lifecycle and key custody.
Do not use it when
Section titled “Do not use it when”Do not run migrations on request or application startup, and do not use the plaintext protector in production.
Runtime and peers
Section titled “Runtime and peers”Engines: node >=20
| Peer | Supported range | Required for |
|---|---|---|
@gj-kit/toss-payments |
`^0.5.0 | |
@nestjs/common |
`^10 | |
reflect-metadata |
`^0.1.13 | |
rxjs |
^7 |
documented optional subpaths only |
Public subpaths
Section titled “Public subpaths”| Entry | Exported symbols |
|---|---|
Root entry |
55 |
./nestjs |
14 |
./testing |
30 |
Safety boundary
Section titled “Safety boundary”Use an app-owned KMS or key-management boundary for sensitive values, run explicit migrations once, and keep cleanup operations idempotent.
API reference
Section titled “API reference”complete API reference contains every public function, class, type, constant, and error contract generated from release declarations. Each item shows its import path, release signature, and package version.
Package relationships
Section titled “Package relationships”| Relationship | Package | Why combine them |
|---|---|---|
| Required | @gj-kit/toss-payments |
This package implements or consumes its public contract. |
| Optional companion | @gj-kit/toss-payments-nestjs |
Pass PostgreSQL-backed stores through a typed Nest payment provider. |