Data Model

Database table schema for the payment module. Phase 0 artifact.

Phase 0SpecPostgreSQL
5 tables of the payment module. All tables are created in Phase 1 (even if some columns/features are used later). PostgreSQL. Monetary amounts -- always bigint in the smallest unit of the respective currency (cents for USD/EUR, yen for JPY, etc.). The currency is stored alongside in the currency column (ISO 4217). Multi-currency is supported: balances and transactions can be in different currencies.
brands and users tables are not described here -- they belong to the auth module. The tables below only reference them via FK.
1.

Tables

transactions
25 columnsPK3 FK5 IDX

Central table. Every deposit or withdrawal attempt creates one row. Never deleted -- audit trail.

ColumnTypeNullableDescription
PKid
uuidNoPrimary key, generated by backend (UUID v4)
FKbrand_id
uuidNoReferences brands.id -- which tenant owns this transaction
FKuser_id
uuidNoReferences users.id -- which player initiated it
FKpsp_config_id
uuidYesReferences psp_configs.id -- which PSP handled it. NULL until PSP is selected by orchestrator.
direction
enum('deposit','withdrawal')NoDirection of funds flow
IDXstatus
varchar(32)NoCurrent UnifiedStatus: INITIATED | PROCESSING | PENDING_CONFIRMATION | PENDING_PARTIAL | COMPLETED | FAILED | TIMED_OUT | CANCELLED
method
varchar(32)NoPayment method slug (e.g. "btc", "usdt_trc20")
amount_requested
bigintNoAmount requested by player, in the smallest unit of currency (e.g. cents for USD/EUR)
amount_credited
bigintYesFinal amount credited to player balance, in the smallest unit of currency. NULL until COMPLETED.
amount_crypto
varchar(64)YesRaw crypto amount as returned by PSP (string to preserve precision)
currency
varchar(8)NoISO 4217 currency code (e.g. "USD")
fee_total
bigintYesTotal fees in the smallest unit of currency (feeService + feeNetwork from PSP). NULL until settled.
exchange_rate
numeric(18,8)YesUSD/crypto rate used at time of conversion. Store for audit.
IDXpsp_payment_id
varchar(128)YesPSP-internal transaction ID (e.g. PassimPay transactionId). Used for idempotency and reconciliation.
IDXpsp_order_id
varchar(128)YesMerchant order ID sent to PSP (derived from id, max 64 chars, PSP-safe charset)
blockchain_tx_id
varchar(128)YesOn-chain transaction hash. Set on COMPLETED.
wallet_address
varchar(256)YesCrypto address shown to player (deposit) or sent to (withdrawal)
destination_tag
varchar(64)YesDestination tag / memo for XRP, TON networks. NULL for other networks.
IDXsession_id
varchar(128)YesPlayer session ID from JWT at time of creation. Used for fraud correlation.
geo
varchar(2)YesISO 3166-1 alpha-2 country code at time of transaction
metadata
jsonbYesArbitrary key-value pairs passed from frontend. Not queried -- store only.
expires_at
timestamptzYesInvoice / address expiry time. Used by TTL job to mark TIMED_OUT.
IDXcreated_at
timestamptzNoRow creation timestamp (UTC)
updated_at
timestamptzNoLast status change timestamp (UTC). Updated on every status transition.
completed_at
timestamptzYesSet when status becomes COMPLETED. Used for settlement reporting.

Indexes

UNIQUE (psp_payment_id) -- idempotency guard
UNIQUE (psp_order_id) -- no duplicate PSP orders
INDEX (user_id, created_at DESC) -- player history
INDEX (brand_id, status, created_at) -- ops dashboard
INDEX (status, expires_at) -- TTL job query
INDEX (psp_config_id, status) -- reconciliation job

All monetary amounts stored as bigint in the smallest unit of currency (cents for USD/EUR, yen for JPY, etc.). Never use float for money. Always pair amount with its currency column.

status column uses application-level enum (varchar), not DB enum -- easier to add new values without migrations.

Rows are never deleted. Hard deletes are prohibited. Use status=CANCELLED for cancellations.

transaction_events
8 columnsPK1 FK2 IDX

Immutable event log. Every status transition and webhook is appended here. Enables full audit trail and debugging.

ColumnTypeNullableDescription
PKid
uuidNoPrimary key
FKtransaction_id
uuidNoReferences transactions.id
IDXevent_type
varchar(64)Noe.g. status_changed | webhook_received | reconciliation_check | ttl_expired
status_from
varchar(32)YesPrevious status (NULL for the first event)
status_to
varchar(32)YesNew status after this event
payload
jsonbYesRaw webhook body or internal event data
psp_raw
jsonbYesRaw PSP response (for debugging). Strip sensitive fields before storing.
IDXcreated_at
timestamptzNoEvent timestamp (UTC)

Append-only. Never update or delete rows.

Every webhook received must be logged here before processing, even duplicates.

psp_configs
14 columnsPK1 FK3 IDX

One row per PSP per brand per geo. The orchestrator reads this table to select which PSP to use for a given request. Managed via Admin Panel (Phase 4) -- no code deploys needed to add or disable a PSP.

ColumnTypeNullableDescription
PKid
uuidNoPrimary key
FKbrand_id
uuidNoReferences brands.id
IDXpsp_name
varchar(64)NoAdapter identifier (e.g. "passimpay"). Must match the adapter registry key.
IDXis_active
booleanNoToggle to enable/disable PSP without deleting config. Checked by orchestrator.
priority
smallintNoCascade priority order. Lower number = tried first. Must be unique per brand+geo+direction.
IDXgeo
varchar(2)YesISO 3166-1 alpha-2 country filter. NULL = all geos.
direction
enum('deposit','withdrawal','both')NoWhich direction this config applies to
methods
varchar[]NoArray of supported method slugs for this config (e.g. ["btc","usdt_trc20"])
credentials
jsonbNoEncrypted PSP credentials (platform_id, api_secret etc.). Encrypted at rest -- never returned in API responses.
min_amount
bigintYesOverride minimum amount in the smallest unit of the config currency. NULL = use PSP default.
max_amount
bigintYesOverride maximum amount in the smallest unit of the config currency. NULL = use PSP default.
health_score
smallintNoPSP health 0--100. Updated by reconciliation job based on success rate. Used for smart routing in Phase 6.
created_at
timestamptzNoRow creation timestamp
updated_at
timestamptzNoLast update timestamp

Indexes

INDEX (brand_id, is_active, geo, direction) -- orchestrator routing query
UNIQUE (brand_id, psp_name, geo, direction) -- no duplicate configs

credentials column must be encrypted at rest (e.g. pgcrypto or application-level AES-256). Never log credentials.

health_score starts at 100 and decays on failures. Phase 6 feature -- populated but not used in routing until Phase 6.

routing_rules
11 columnsPK2 FK1 IDX

Explicit overrides on top of psp_configs priority. Allows ops to pin specific PSPs for specific conditions without changing priority order. Evaluated by orchestrator after base PSP selection.

ColumnTypeNullableDescription
PKid
uuidNoPrimary key
FKbrand_id
uuidNoReferences brands.id. NULL = applies to all brands.
FKpsp_config_id
uuidNoReferences psp_configs.id -- which PSP this rule forces
condition_geo
varchar(2)YesISO 3166-1 alpha-2. NULL = match any geo.
condition_method
varchar(32)YesMethod slug filter. NULL = match any method.
condition_currency
varchar(8)YesCurrency code filter. NULL = match any currency.
condition_min_amount
bigintYesRule applies only when transaction amount >= this value (smallest unit of transaction currency)
condition_max_amount
bigintYesRule applies only when transaction amount <= this value (smallest unit of transaction currency)
IDXis_active
booleanNoToggle rule on/off without deleting
priority
smallintNoRule evaluation order. Lower = evaluated first. First matching rule wins.
created_at
timestamptzNoRow creation timestamp

Phase 2 feature -- table is created in Phase 1 but the orchestrator only reads it from Phase 2 onwards.

All NULL conditions are treated as wildcards (match anything). A rule with all NULLs matches every transaction.

payment_methods
12 columnsPK1 FK1 IDX

Cache of available payment methods per PSP config. Populated by the getSupportedMethods() adapter call. Refreshed every 5 minutes. Frontend reads this to build the method selector.

ColumnTypeNullableDescription
PKid
uuidNoPrimary key
FKpsp_config_id
uuidNoReferences psp_configs.id
IDXslug
varchar(32)NoMethod identifier (e.g. "btc", "usdt_trc20"). Used in transaction.method column.
name
varchar(64)NoDisplay name for UI (e.g. "Bitcoin", "USDT TRC20")
network
varchar(64)YesBlockchain network name (e.g. "TRC20", "ERC20")
min_amount
bigintNoMinimum amount in the smallest unit of the method currency
max_amount
bigintNoMaximum amount in the smallest unit of the method currency
fee_pct
numeric(5,2)YesEstimated fee percentage 0.00--100.00
logo_url
varchar(512)YesURL to method logo image
requires_tag
booleanNoTrue for XRP, TON and other networks requiring destination tag / memo
is_active
booleanNoSoft toggle. False = hide from player UI without deleting cache row.
cached_at
timestamptzNoWhen this row was last refreshed from PSP

Indexes

UNIQUE (psp_config_id, slug) -- one row per method per PSP config
INDEX (psp_config_id, is_active) -- frontend method list query
2.

Table Relationships

TableColumnReferencesDescription
transactionsbrand_idbrandsEach transaction belongs to a brand (tenant)
transactionsuser_idusersEach transaction belongs to a player
transactionspsp_config_idpsp_configsWhich PSP handled this transaction (set by orchestrator)
transaction_eventstransaction_idtransactionsEvent log entries belong to a transaction
psp_configsbrand_idbrandsPSP configs are scoped to a brand
routing_rulesbrand_idbrandsRouting rules are scoped to a brand (nullable = all brands)
routing_rulespsp_config_idpsp_configsRule forces a specific PSP config
payment_methodspsp_config_idpsp_configsMethods are cached per PSP config
DEPO44 | DATA MODEL v1 | PHASE 0 ARCHITECTURE