DataAcuity — BI Pipeline Implementation Runbook
Status: 🔵 DESIGNED — implementation tracking starts at Phase 0
Owner: Tinashe Bhengu
Companion docs: DataAcuity_BI_Pipeline.md (architecture/design), DataAcuity_BI_Anonymisation_Standard.md (controls), DataAcuity_BI_Compliance_Map.md (regulator mapping), DataAcuity_Security_Posture.md (security pre-reqs)
The execution-side counterpart to the BI Pipeline design doc. Phase-by-phase: what to build, exact commands, acceptance tests, rollback procedures, monitoring queries.
Security pre-requisite: Security Phases A+B+C+D from
DataAcuity_Security_Posture.mdMUST be complete before BI Pipeline Phase 0 starts carrying customer data. Phase 0 itself (foundation audit) can happen in parallel.
1. Implementation status — at a glance
| Phase | Status | Started | Completed |
|---|---|---|---|
| Security A: Port lockdown | ✅ COMPLETE | 2026-05-28 | 2026-05-28 |
| Security B: Traefik + auth | 🔵 PENDING | — | — |
| Security C: Backup DR | 🔵 PENDING | — | — |
| Security D: IR + alerting | 🔵 PENDING | — | — |
| BI Phase 0: Foundation audit | 🔵 PENDING | — | — |
| BI Phase 1: Extract framework | 🔵 PENDING | — | — |
| BI Phase 2: Anonymisation layer | 🔵 PENDING | — | — |
| BI Phase 3: First marts + push-back | 🔵 PENDING | — | — |
| BI Phase 4: Source coverage | 🔵 PENDING | — | — |
| BI Phase 5: Enrichment | 🔵 PENDING | — | — |
| BI Phase 6: BI surfaces | 🔵 PENDING | — | — |
| BI Phase 7: ML feature stores | 🟢 NOT STARTED (open-ended) | — | — |
Update this table as work progresses.
2. BI Phase 0 — Foundation audit (1 week)
Goal: Honest accounting of what exists today so subsequent phases build on truth, not assumptions.
Tasks
| # | Task | Owner | Verification |
|---|---|---|---|
| 0.1 | Verify postgres roles + connection strings for data_warehouse, markets_db, superset_db |
DBA | psql -U <role> -d <db> -c "select current_user" succeeds for each documented role |
| 0.2 | Document the existing dbt models (5 marts + 2 staging) — owner, source, freshness, current row counts | Data engineer | New section added to DataAcuity_BI_Pipeline.md §8.4 with concrete numbers |
| 0.3 | Set up Grafana "BI Pipeline Health" dashboard scraping existing metrics | Ops | Dashboard live in Grafana, link added to DataAcuity_BI_Pipeline.md §11.2 |
| 0.4 | Audit n8n's existing 55 MB sqlite — what workflows are running? | Ops | Documented list of workflows + decision per workflow (keep / retire / document) added to a new DataAcuity_n8n_Audit.md |
| 0.5 | Create compliance DB on .118 (empty, just the schema for §9 below) |
DBA | psql -h 118 -U compliance_admin -d compliance -c "\dn" shows compliance schema |
| 0.6 | Set up the named compliance_admin role + dbt_runner role + etl_user role |
DBA | All three roles exist, with permissions per DataAcuity_Security_Posture.md §8.4 |
| 0.7 | Confirm .105 is an actual read-replica and what user the ETL should use |
DBA | psql -h 105 -U replicator -d auth_db -c "select pg_is_in_recovery()" returns t |
Acceptance
- Updated docs reflect current state accurately
- Grafana dashboard is live
- All roles required for Phase 1 exist
- n8n audit is committed to the repo
Rollback
Phase 0 is read-only. No rollback needed.
3. BI Phase 1 — Extract framework (1 week)
Goal: Reliable, idempotent, restartable extraction from .105 into data_warehouse.raw.* for the 3 revenue-critical source DBs.
Tasks
| # | Task | Owner | Verification |
|---|---|---|---|
| 1.1 | Decide: build custom Python extract, or wire up Airbyte (already at /home/geektrading/airbyte/) |
Tech lead | Decision recorded in this doc + ADR file |
| 1.2 | Implement extract for ledger_db.transactions, ledger_db.accounts, ledger_db.holds, ledger_db.reversals |
Data engineer | After job runs, SELECT COUNT(*) FROM raw.ledger_db__transactions matches expected row count from .105 |
| 1.3 | Implement extract for sdpkt_db.wallets, sdpkt_db.payments, sdpkt_db.holds, sdpkt_db.merchant_settlements |
Data engineer | Same verification pattern |
| 1.4 | Implement extract for bidbaas_db.auctions, bidbaas_db.bids, bidbaas_db.winners, bidbaas_db.payouts |
Data engineer | Same verification pattern |
| 1.5 | Configure schedule: every 5 min for revenue-critical event tables, hourly for dimension tables | Ops | Cron / Airbyte schedule shows correct cadence |
| 1.6 | Add freshness alert: any source table with event_time > 2× expected interval old triggers warning |
Ops | Alertmanager rule live; verified by stopping extract and confirming alert fires |
| 1.7 | Document the extract config (source registry) as a single YAML/JSON the extract reads | Data engineer | /home/geektrading/etl-config/sources.yml exists with all 3 source DBs |
Schema for raw.*
Tables are named raw.{source_db}__{table}. Schema mirrors source exactly except:
- Add
_extracted_attimestamp column - Add
_extract_batch_idUUID column (so we can correlate rows extracted together) - Keep all source columns (no filtering yet — that happens at staging)
Acceptance
- Every 5 min, fresh rows from the 3 revenue-critical sources land in
raw.* - Freshness alert fires correctly when extract is paused
- Extract is idempotent — running it twice on the same window doesn't create duplicates
Rollback
Stop the extract scheduler. Optionally TRUNCATE raw.* for the affected tables. No app impact since this is read-only against .105.
4. BI Phase 2 — Anonymisation layer (1 week)
Goal: Move data from raw.* → staging.* → intermediate.* with all PII removed at the intermediate.* boundary per DataAcuity_BI_Anonymisation_Standard.md.
Pre-requisites
- Security Phase A complete (port lockdown) ✅
compliance.saltstable exists on.118(Phase 0 task 0.5)- A named compliance reviewer is identified
Tasks
| # | Task | Owner | Verification |
|---|---|---|---|
| 2.1 | Implement hash helper functions in Postgres (compliance.hash_user, compliance.hash_device, etc.) |
DBA | SELECT compliance.hash_user('test') returns 64-char hex |
| 2.2 | Implement token vault on .118 with tokenise_* functions per Anonymisation Standard §4 |
DBA + compliance | Functions exist; access controls verified |
| 2.3 | Write the PII-absence dbt test macros from Anonymisation Standard §7 | Data engineer | Tests fail on a deliberately-leaky test model |
| 2.4 | Write intermediate.int_users_anon joining auth_db.users across sources with deterministic hashing |
Data engineer | Output rows have user_hash (64-char hex), no email/phone/name_* columns |
| 2.5 | Write intermediate.int_ledger_transactions_anon |
Data engineer | Same pattern, applied to ledger |
| 2.6 | Write intermediate.int_sdpkt_payments_anon |
Data engineer | |
| 2.7 | Write intermediate.int_bidbaas_auctions_anon |
Data engineer | |
| 2.8 | Compliance review of sample anonymised output (10 rows from each intermediate.* model) |
Compliance reviewer | Sign-off recorded in PR + compliance log |
| 2.9 | Wire dbt test suite into nightly run; alert on failure | Ops | Test failure triggers Alertmanager P1 |
Acceptance
- An independent reviewer (not the model author) verifies a sample of
intermediate.*rows and confirms no PII leaks - dbt test suite passes on every
intermediate.*model - Salt rotation procedure documented and tested in staging environment
Rollback
If a PII leak is found post-deployment:
- Immediately
REVOKE ALL ON ALL TABLES IN SCHEMA intermediate FROM PUBLICand other roles - Drop the leaky model
- Fix the model, re-run anonymisation tests, re-deploy
- Log the incident in
compliance.incident_log
5. BI Phase 3 — First 5 marts + push-back (2 weeks)
Goal: Five highest-value marts built; postgres_fdw push-back wired; BidBaas reads its summary from .104 analytics_db.
Marts to build (in order)
marts.fct_ledger_transactions_anon— every ledger event with hashed user, FX-normalisedmarts.fct_bidbaas_auctions— auction × winner-hash × revenuemarts.fct_sdpkt_payments— payments × merchant-hash × FX-normalisedmarts.dim_user_360_anon— anonymised user × 30-day rollup across all apps in scopemarts.agg_revenue_daily_app_country— daily revenue rolled up to (app, country, day)
Each mart:
- Has a
description:block stating purpose (POPIA Sec 13 / GDPR Art 5(1)(b)) - Has
meta.owneridentifying the named owner - Has
meta.compliance_classfrom{operational, ops_analytics, marketing, financial} - Has dbt tests: not_null on PK, unique on natural key, k-anonymity ≥ 5 for user-aggregating marts
Push-back tasks
| # | Task | Owner | Verification |
|---|---|---|---|
| 3.1 | Create analytics_db on .104 + replicate to .105 |
DBA | psql -h 104 -d analytics_db succeeds |
| 3.2 | Create per-app schemas: analytics_db.bidbaas, analytics_db.tagme, etc. |
DBA | \dn in analytics_db shows expected schemas |
| 3.3 | Create per-app read role: bidbaas_app_reader with SELECT on analytics_db.bidbaas.* |
DBA | Role can SELECT, cannot INSERT/UPDATE |
| 3.4 | Set up postgres_fdw from data_warehouse on .106 to analytics_db on .104 |
DBA | CREATE FOREIGN TABLE succeeds; can SELECT over FDW |
| 3.5 | Implement push-back job for analytics_db.bidbaas.user_summary |
Data engineer | After run, SELECT * FROM analytics_db.bidbaas.user_summary LIMIT 5 returns expected rows |
| 3.6 | BidBaas API consumes the summary view | App developer | BidBaas in-app shows "your bids this week" sourced from analytics_db |
| 3.7 | Document the push-back pattern for the next 5 apps to follow | Data engineer | New section in DataAcuity_BI_Pipeline.md §9 |
Acceptance
- BidBaas in-app shows "your bids this week" with data sourced from
analytics_db - End-to-end latency (event captured → visible in app) < 1 hour
- No PII leaks past the
intermediate.*boundary (verified by dbt tests) - Push-back job is idempotent (re-running doesn't duplicate or corrupt)
Rollback
If the BidBaas integration breaks:
- App falls back to direct query against
bidbaas_db(the existing path) — verify this fallback works before shipping - Drop the FDW connection
- Diagnose, fix, re-deploy
6. BI Phase 4 — Coverage expansion (2 weeks)
Goal: All source DBs in scope are being extracted, anonymised, and the per-app analytics.*_user_summary views are live.
Tasks
| # | Task | Owner | Verification |
|---|---|---|---|
| 4.1 | Add 18 source DBs to extract registry per DataAcuity_BI_Pipeline.md §5 |
Data engineer | All 18 sources have rows in raw.* within 24 h |
| 4.2 | Write intermediate.* models for each source |
Data engineer | All have PII-absence tests passing |
| 4.3 | Build the remaining marts listed in DataAcuity_BI_Pipeline.md §8.4 |
Data engineer | All marts populated with current data |
| 4.4 | Wire per-app push-back: TagMe, TxTMe!, Takemehome, WhatWeWant, SDPKT, SleptOn, etc. | Data engineer + app teams | Each app's analytics tab shows real data |
| 4.5 | Compliance review of each new intermediate.* model |
Compliance reviewer | Sign-off recorded per model |
Acceptance
- Every TGN app with an analytics surface is reading from
analytics_db - Source registry is exhaustive (no app data missing)
- Per-app team has signed off on their summary's shape
7. BI Phase 5 — Enrichment expansion (1 week)
Goal: Geographic, temporal, FX, and cross-app context attached to relevant marts.
Tasks
| # | Task | Owner | Verification |
|---|---|---|---|
| 5.1 | Geo enrichment: integrate geo_mcp.reverse_geocode calls into dbt transformation for any location-bearing fact |
Data engineer | Sample row has city, country, nearest_poi_category populated |
| 5.2 | Temporal enrichment: add day_of_week, hour_of_day, is_weekend, is_payday_week, is_public_holiday, season to all timestamped facts |
Data engineer | Sample row has all 6 columns populated |
| 5.3 | FX enrichment: ingest daily ZAR/USD/EUR/GBP/BTC into dim_fx_rates_daily from exchangerate.host (until Markets API repaired) |
Data engineer | Daily rows present; sample monetary fact has amount_zar AND amount_usd AND amount_btc_sats |
| 5.4 | User segment enrichment: KYC tier, lifecycle stage, mesh node class | Data engineer | Sample user has all 3 columns populated |
| 5.5 | Cross-app session derivation: cross_app_session_id via 30-min sliding window |
Data engineer | A test user with events in 2 apps within 30 min shares a session_id |
| 5.6 | Build marts.agg_funnel_app_to_app |
Data engineer | Shows real funnel rates between app pairs |
Acceptance
marts.fct_*tables have enriched columns populated- A test "user journey" trace across 3+ apps shows correct session linking
- FX conversion is correct (spot check: 100 ZAR transaction shows ~5.5 USD at current rate)
8. BI Phase 6 — BI surfaces (1-2 weeks)
Goal: BigBruh! Analytics tab is live with Superset embeds; per-app dashboards published.
Tasks
| # | Task | Owner | Verification |
|---|---|---|---|
| 6.1 | Build BigBruh! "Analytics" tab — Superset embed via SSO token | Frontend + ops | Tab loads dashboards; user identity flows through |
| 6.2 | Build per-app Superset dashboards: TagMe, BidBaas, SDPKT, Takemehome, WhatWeWant, TxTMe!, SleptOn | BI author | Each dashboard is published, has owner, has refresh schedule |
| 6.3 | Public-facing aggregate dashboards (if approved in open question §13 of BI Pipeline doc) | BI author | If yes, lives at dataacuity_portal |
| 6.4 | Retire BigBruh!'s "live counters only" view | Frontend | View removed from navigation |
Acceptance
- BigBruh!'s Overview includes both live ops counters AND historical analytics
- Each per-app dashboard has a named owner and a refresh schedule
- Performance: dashboards load < 3 s for typical queries
9. BI Phase 7 — ML feature stores (open-ended)
Goal: First two ML feature stores defined, tested for leakage, wired to a training pipeline.
Will be specced out when an ML team is in place. For now:
ml.*schema reserved- Naming convention:
ml.feat_<entity>_v<N> - Strict time-aligned features (no future-bleed)
- Versioned tables (never
DROP TABLE; add v2, v3 alongside v1)
10. Monitoring queries
Useful one-liners for the on-call to run.
10.1 Extract freshness
-- Show last extract per source table
SELECT table_name, max(_extracted_at) AS last_extract_at, now() - max(_extracted_at) AS lag
FROM (
SELECT 'ledger_db__transactions' AS table_name, _extracted_at FROM raw.ledger_db__transactions
UNION ALL
SELECT 'sdpkt_db__payments', _extracted_at FROM raw.sdpkt_db__payments
-- ... etc
) t
GROUP BY table_name
ORDER BY lag DESC;
10.2 dbt test failure summary
-- After a dbt run, what's failing?
SELECT model_name, test_name, status, num_failing_rows
FROM dbt_artifacts.test_results
WHERE run_id = (SELECT max(run_id) FROM dbt_artifacts.test_results)
AND status != 'pass'
ORDER BY num_failing_rows DESC;
10.3 K-anonymity health check (ad-hoc)
-- For a given mart, find groups below k=5
SELECT city, age_band, count(DISTINCT user_hash) AS n
FROM marts.fct_some_user_event
GROUP BY city, age_band
HAVING count(DISTINCT user_hash) < 5
ORDER BY n;
10.4 Push-back lag
-- On .104 analytics_db
SELECT 'bidbaas.user_summary' AS view, max(last_updated_at) AS last_push, now() - max(last_updated_at) AS lag
FROM bidbaas.user_summary
UNION ALL
SELECT 'tagme.user_summary', max(last_updated_at), now() - max(last_updated_at) FROM tagme.user_summary
-- ... etc
ORDER BY lag DESC;
10.5 Token vault access in last 24h
-- On .118 compliance_db
SELECT accessed_by, count(*) AS access_count, array_agg(DISTINCT token_family) AS families
FROM compliance.token_access_audit
WHERE accessed_at > now() - interval '24 hours'
GROUP BY accessed_by;
11. Deploy procedures
11.1 Deploying a new dbt model
- PR includes: model SQL,
description:block,meta.ownerandmeta.compliance_class, dbt tests, sample output (10 rows of post-anonymisation result) - CI runs
dbt build --select <new_model>+on staging warehouse - Compliance reviewer signs off if it touches
intermediate.*or any user data - Merge → next dbt run on
.106picks up the new model - Verify: query the new model, check row count + sample output
11.2 Deploying a new push-back view
- Create the
marts.agg_*source mart first - Create the
analytics.{app}_*view definition - Run the push-back job manually once:
docker exec dbt_transform dbt run --select <push_back_model> - Verify
psql -h 104 analytics_dbshows the new view - App team queries the view, integrates into UI
- Schedule push-back in cron
11.3 Salt rotation (annual)
- Schedule a change window (low traffic period)
- Generate new salt:
openssl rand -base64 32 > /tmp/new_salt.txt - Update
compliance.saltsSETis_retired = true, then INSERT new row - Update orchestrator's env var with new salt
- Run full
intermediate.*rebuild (will use new salt) - Cascade through
marts.*,analytics.* - Verify analytics still work (joins succeed)
- Document rotation in
compliance.saltsaudit columns
11.4 Right-to-be-forgotten request
- Request lands at AuthAPI →
users.deleted_atset on.104 - Within 24 h: extract job picks up the soft-delete
- Within 7 days: retention job cascades deletes through all warehouse layers
- Within 30 days: compliance ticket closed with
compliance.deletion_completionrow + email confirmation to user
Manual escalation if SLA at risk: data engineer runs retention job ad-hoc.
12. Cross-references
DataAcuity_BI_Pipeline.md— the designDataAcuity_BI_Anonymisation_Standard.md— the controlsDataAcuity_BI_Compliance_Map.md— the regulator mappingDataAcuity_Security_Posture.md— security pre-reqsDeployment/deployment-credentials.ps1—replicatorcredentials for ETL
13. Change log
| Date | Change | By |
|---|---|---|
| 2026-05-28 | Initial document — execution runbook split out from BI Pipeline design | Tinashe Bhengu |
| 2026-05-28 | Phase A (Security port lockdown) executed | Tinashe Bhengu |