Data model
In short
Section titled “In short”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.
The main records
Section titled “The main records”WHO WHAT HAPPENED HOW WE JUDGE IT WHAT WE FOUND / DID───────────────── ───────────────── ───────────────── ──────────────────────customers transactions rule_packs rule_hitsaccounts decisions rules alertscounterparties fx_rates rule_versions casesdevices 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 |
What happened
Section titled “What happened”| 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 |
How money is stored
Section titled “How money is stored”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.
Personal data is stored encrypted
Section titled “Personal data is stored encrypted”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.
Large tables are split by month
Section titled “Large tables are split by month”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 row knows its owner
Section titled “Every row knows its owner”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.
Conventions (for engineers)
Section titled “Conventions (for engineers)”- 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
rawcolumns, 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_idand 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).
What this means for you
Section titled “What this means for you”- 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.