Skip to content

@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 — pg isn’t even a peer
  • 55 @ts-expect-error compile guards
  • 250+ unit tests · 31 more against real PostgreSQL
  • 7 tables, one explicit migrate()

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.

  • Assembly without a protector won’t compile — createTossPaymentsPostgres({ sql }) is a type error — sensitiveValueProtector is 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.

Outcome: PostgreSQL-backed Toss stores with one explicit migration step.

Terminal window
pnpm add @gj-kit/toss-payments-postgresql

Adapt your pool with fromPgPool, use a real encrypted sensitiveValueProtector, and run migration during deployment.

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.

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 Pool
declare const keyHex: string; // app owns key custody and rotation
declare const blindIndex: string; // app owns the blind index of the customer id
declare 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 Toss payment stores need a proven PostgreSQL implementation while your app retains connection lifecycle and key custody.

Do not run migrations on request or application startup, and do not use the plaintext protector in production.

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
Entry Exported symbols
Root entry 55
./nestjs 14
./testing 30

Use an app-owned KMS or key-management boundary for sensitive values, run explicit migrations once, and keep cleanup operations idempotent.

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.

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.

npm · GitHub