Skip to main content

3. Backend DB Schema

The backend uses a relational database (PostgreSQL) to store the authoritative server-side record of cards, users, terminals, audit events, and reconciliation history.

This schema is the server-side projection of card state. It is not the source of truth for offline balance — the card is. The backend schema becomes authoritative only after reconciliation.


Entity overview

TablePurpose
usersCooperative members; one user may have one card at a time
terminalsRegistered devices with a role and credentials
cardsBackend record of each issued card
audit_logImmutable log of every reconciled card event
reconciliation_batchesMetadata about each submitted reconciliation batch
key_versionsRecord of active and retired key sets

users

ColumnTypeConstraintsDescription
user_iduint32 / INTEGERPK, NOT NULLBackend-assigned member ID; matches card userId field
nameVARCHAR(100)NOT NULLFull member name
statusVARCHAR(20)NOT NULL, default 'active'Account status: active, suspended, closed
created_atTIMESTAMPTZNOT NULLAccount creation timestamp
updated_atTIMESTAMPTZNOT NULLLast modification timestamp

Notes:

  • A suspended or closed user account prevents new card issuance but does not automatically block existing cards.
  • user_id values are never reused.

terminals

ColumnTypeConstraintsDescription
terminal_idSMALLINTPK, NOT NULLBackend-assigned terminal ID (uint16); matches card session.terminalId
device_idVARCHAR(128)UNIQUE, NOT NULLHardware or installation identifier used for auth
roleVARCHAR(20)NOT NULLOne of: terminal, gate, station, scout
nameVARCHAR(100)Human-readable label for the device
statusVARCHAR(20)NOT NULL, default 'active'active, suspended
last_token_issued_atTIMESTAMPTZTimestamp of most recent token issuance
created_atTIMESTAMPTZNOT NULLRegistration timestamp

Notes:

  • device_id + role are verified at token issuance. The commissioning secret is one-time-use and not stored after first use (hashed for verification only).
  • A suspended terminal receives 403 device_suspended on any API call.

cards

ColumnTypeConstraintsDescription
card_idBYTEA(6)PK, NOT NULL6-byte card identifier set at issuance
user_idINTEGERFK → users.user_id, NOT NULLOwning user
statusVARCHAR(20)NOT NULL, default 'ACTIVE'Mirrors card status field; see status codes
balanceINTEGERNOT NULL, default 0, ≥ 0Last known balance after most recent reconciliation (IDR)
counterBIGINTNOT NULL, default 0Most recent reconciled counter value; used to detect replay
key_versionSMALLINTNOT NULLKey version written to the card at issuance or last re-issuance
created_atTIMESTAMPTZNOT NULLIssuance timestamp
last_activity_atTIMESTAMPTZTimestamp of most recently reconciled event
expires_atTIMESTAMPTZNOT NULLCard expiry, mirrors trailer expiresAt
notesTEXTOperator notes (e.g., block reason narrative)

Notes:

  • balance and counter reflect the last reconciled state, not necessarily the current card state. A card may have offline transactions not yet submitted.
  • Status changes (BLOCKED_*, ACTIVE) applied by reconciliation or station operations are written here.
  • card_id is never reused. Re-issued cards receive the same card_id with updated fields.

Constraints:

CONSTRAINT cards_balance_non_negative CHECK (balance >= 0),
CONSTRAINT cards_balance_ceiling CHECK (balance <= 16000000),
CONSTRAINT cards_status_valid CHECK (status IN ('ACTIVE','BLOCKED_TAMPER','BLOCKED_FRAUD','BLOCKED_EXPIRED','BLOCKED_ADMIN'))

audit_log

Append-only. No rows are updated or deleted after insertion.

ColumnTypeConstraintsDescription
log_idBIGSERIALPK, NOT NULLAuto-incrementing server-side log entry ID
card_idBYTEA(6)FK → cards.card_id, NOT NULLCard involved in this event
terminal_idSMALLINTFK → terminals.terminal_idTerminal that created this event
counterBIGINTNOT NULLCard counter value at time of event; uniqueness enforced per card_id
tx_typeVARCHAR(20)NOT NULLEvent type: debit, credit, checkin, checkout, admin
amountINTEGERNOT NULL, ≥ 0Transaction amount (IDR); 0 for non-financial events
balance_afterINTEGERNOT NULL, ≥ 0Card balance after this event
chain_hashBYTEA(6)NOT NULL6-byte truncated SHA-256 chain hash from the card log entry
event_atTIMESTAMPTZNOT NULLTimestamp of the event (from card deltaTime + session.startTime)
reconciled_atTIMESTAMPTZNOT NULLServer timestamp when this entry was accepted
offline_flagBOOLEANNOT NULL, default falsetrue if the terminal was offline when this event was created
suspect_flagBOOLEANNOT NULL, default falsetrue if terminal or backend flagged as suspicious
review_flagBOOLEANNOT NULL, default falsetrue if backend flagged for limit breach or policy review
batch_idBIGINTFK → reconciliation_batches.batch_idReconciliation batch that delivered this entry

Constraints:

CONSTRAINT audit_log_counter_unique UNIQUE (card_id, counter),
CONSTRAINT audit_log_amount_non_negative CHECK (amount >= 0),
CONSTRAINT audit_log_balance_non_negative CHECK (balance_after >= 0)

reconciliation_batches

One row per POST /api/reconcile call.

ColumnTypeConstraintsDescription
batch_idBIGSERIALPK, NOT NULLAuto-incrementing batch ID
terminal_idSMALLINTFK → terminals.terminal_id, NOT NULLSubmitting terminal
submitted_atTIMESTAMPTZNOT NULLServer timestamp of receipt
event_countINTEGERNOT NULLTotal events in the submitted batch
acceptedINTEGERNOT NULLNumber of events accepted
rejectedINTEGERNOT NULLNumber of events rejected (duplicate, tamper, malformed)
flaggedINTEGERNOT NULL, default 0Number of events accepted but flagged for review

key_versions

ColumnTypeConstraintsDescription
key_versionSMALLINTPK, NOT NULLKey version identifier (uint8, 1–255)
statusVARCHAR(20)NOT NULLactive, retired, revoked
created_atTIMESTAMPTZNOT NULLWhen this key version was activated
retired_atTIMESTAMPTZWhen this key version was retired (no longer issued to new cards)
revoked_atTIMESTAMPTZWhen this key version was revoked (all cards must be re-issued)

Notes:

  • The raw key material is not stored in this table. Key material lives in a secrets manager (e.g., HashiCorp Vault, AWS Secrets Manager). This table tracks only version lifecycle.
  • retired means no new cards are issued with this version, but existing cards remain valid.
  • revoked means all cards with this key_version must be re-issued at next station visit.

Indexes

-- Fast card lookup by user
CREATE INDEX idx_cards_user_id ON cards (user_id);

-- Fast audit log queries per card, ordered by counter
CREATE INDEX idx_audit_log_card_counter ON audit_log (card_id, counter);

-- Fast batch queries per terminal
CREATE INDEX idx_audit_log_batch ON audit_log (batch_id);

-- Review queue
CREATE INDEX idx_audit_log_review ON audit_log (review_flag) WHERE review_flag = true;

Cross-references