Skip to content

Data model

SpeculaPro’s database is organised like a well-run filing room. There are cabinets for who (customers, accounts, counterparties, devices), what happened (transactions and decisions), how we judge it (rules and their versions), what we found (rule hits, alerts, cases) and what we did about it (case timelines, reports). A final cabinet, the audit log, records every action taken in the system. Nothing can be removed from it.

WHO WHAT HAPPENED HOW WE JUDGE IT WHAT WE FOUND / DID
───────────────── ───────────────── ───────────────── ──────────────────────
customers transactions rule_packs rule_hits
accounts decisions rules alerts
counterparties fx_rates rule_versions cases
devices suppressions case_events (timeline)
risk_designations lists reports (STR / CTR)
escalation_policies
teams, team_members
audit_log (everything)
Record What it represents Notable fields
Customer A person or business you serve Type (individual or business), status (active, dormant, restricted, exited), segment, risk rating (unrated → unacceptable), KYC tier, country, occupation, declared monthly volume
Account Something a customer holds Type (bank, wallet, card, merchant, agent, loan, virtual, other), provider (for example mtn_momo), currency, status
Counterparty The other side of a payment Name, masked identifier, provider, country. Deduplicated, so the same recipient seen 500 times is one record
Device A phone, browser, POS terminal, ATM, USSD session or API client Fingerprint, first and last seen
Risk designation A flag such as PEP or sanctioned Type, source, reason, evidence, validity dates. Revoked, never deleted. See Risk designations
Record What it represents
Transaction One movement of money: amount, currency, channel, direction, counterparty, country, device
Decision SpeculaPro’s real-time answer for a transaction: outcome, score, how long it took, the rule versions used and the feature values seen
FX rate The daily exchange rates used to convert amounts, kept so every conversion can be reproduced
Ingest key A record of every external_id you have sent, so a retry is recognised as a duplicate. Kept for 90 days by default, then removed by the worker

How we judge it, what we found, what we did

Section titled “How we judge it, what we found, what we did”
Record Where to read more
Rule packs, rules, rule versions Rules and versions
Suppressions Suppressions
Lists (allow, deny, reference) Risk designations and lists
Rule hits, alerts, cases, case timeline, reports, escalation policies, teams Alerts, cases and escalation
Audit log Audit log

Money is never stored as a decimal number such as 4500.75, because decimals invite rounding errors. Every amount is stored as:

Field Example Meaning
amount_minor 450075 Whole number of the smallest unit (pesewas for GHS, cents for USD)
currency GHS Three-letter ISO currency code
amount_base 450075 The same amount converted to your workspace’s base currency
fx_rate 1 The exchange rate used for that conversion

Why this matters. Suppose your threshold for a large transaction is GHS 50,000, and a customer receives USD 4,000. SpeculaPro converts it with that day’s rate (say 1 USD = 15.20 GHS, giving GHS 60,800) and compares the base amount to the threshold. Because the rate is stored with the transaction, anyone can later recompute and confirm the result, even after rates change.

Sensitive identifiers (national ID numbers such as the Ghana Card, phone numbers, emails, account numbers) are stored encrypted, alongside a keyed fingerprint that lets SpeculaPro spot the same value on two records without decrypting it. See Data protection.

Transactions, decisions, rule hits and the audit log grow quickly. Each is partitioned by month: Postgres keeps one physical table per month behind a single logical table.

  • Queries for “the last 30 days” only touch the recent months, so they stay fast.
  • Old months can be moved to cheaper storage, or removed when your retention period ends, without touching current data.

Every table that holds an institution’s data carries a tenant_id, and Postgres row-level security makes the database itself refuse to return rows belonging to another workspace. See Tenant isolation.

  • Queries are plain SQL, type-checked and turned into Go code with sqlc.
  • JSONB is used only for custom attributes and evidence, never for core columns that rules depend on. (The schema has raw columns, but the ingest API leaves them empty: raw request bodies are not stored.)
  • Identifiers are UUIDv7, which sort by creation time.
  • Each record has your own external_id and is unique per workspace, which is what makes repeated sends safe.
  • The schema is managed with versioned migrations embedded in the binary (speculapro migrate up).
  • Compliance and audit: the case timeline, approved rule versions and the audit log are protected by the database against edits and deletion, not just by the application.
  • Finance and reporting: amounts are exact and every currency conversion is reproducible.
  • Integration teams: send your own stable IDs (external_id). They are how SpeculaPro matches updates to existing records.