PostgresStore

A PostgreSQL-based implementation of Stripe402Store with full transaction audit logging and SQL-level atomicity.

Constructor

import { Pool } from 'pg'

new PostgresStore(pool: Pool)
Parameter
Type
Description

pool

Pool

A pg.Pool instance for connection pooling.

Example

import { Pool } from 'pg'
import { PostgresStore } from '@stripe402/server'

const pool = new Pool({
  connectionString: 'postgresql://stripe402:stripe402@localhost:5433/stripe402',
})
const store = new PostgresStore(pool)

// Create tables on startup (idempotent)
await store.createTables()

Table Schema

createTables()

Call once on application startup. Creates tables if they don't exist (idempotent via CREATE TABLE IF NOT EXISTS).

stripe402_clients

Stores client records and credit balances.

Column
Type
Constraints
Description

client_id

TEXT

PRIMARY KEY

HMAC-derived client identifier (64-char hex).

stripe_customer_id

TEXT

NOT NULL

Stripe Customer ID (cus_...).

balance

INTEGER

NOT NULL DEFAULT 0

Current balance in units.

currency

TEXT

NOT NULL DEFAULT 'usd'

ISO 4217 currency code.

created_at

TIMESTAMPTZ

NOT NULL DEFAULT NOW()

Record creation timestamp.

updated_at

TIMESTAMPTZ

NOT NULL DEFAULT NOW()

Last modification timestamp.

stripe402_transactions

Stores transaction audit log entries.

Column
Type
Constraints
Description

id

TEXT

PRIMARY KEY

UUID transaction identifier.

client_id

TEXT

NOT NULL, REFERENCES stripe402_clients(client_id)

Foreign key to client.

type

TEXT

NOT NULL, CHECK (type IN ('topup', 'deduction'))

Transaction type.

amount

INTEGER

NOT NULL

Amount in units.

stripe_payment_intent_id

TEXT

(nullable)

Stripe PI ID (top-ups only).

resource

TEXT

(nullable)

Route key (deductions only), e.g., 'GET /api/joke'.

created_at

TIMESTAMPTZ

NOT NULL DEFAULT NOW()

Transaction timestamp.

Index: idx_stripe402_transactions_client_id on client_id for efficient lookups.

Method Implementations

getClient(clientId)

Returns null if no rows match. Maps snake_case columns to camelCase TypeScript fields.

createClient(record)

The ON CONFLICT DO NOTHING makes this idempotent — duplicate inserts are silently ignored.

deductBalance(clientId, amount)

Atomicity: The WHERE balance >= $1 clause ensures the deduction only happens if the balance is sufficient. If the balance is too low, no rows are updated and the RETURNING clause returns no rows — mapped to null.

This is atomic because PostgreSQL executes the entire UPDATE ... WHERE ... RETURNING statement as a single operation with row-level locking.

addBalance(clientId, amount)

Returns the new balance.

recordTransaction(transaction)

Nullable fields (stripePaymentIntentId, resource) are passed as null when not present.

Last updated