Institutional dossier · prepared for Investec Mauritius
The technical, security and compliance posture of FinVeil — in detail.
The pages at /trust, /security and /compliance are written for prospective customers. This page is written for institutional evaluators. Every claim here cites the file in our repository where the control lives so you can audit, not infer. Where a control is not yet in place we say so explicitly — the "What is not yet in place" section below is the most important one on this page.
1. Technology stack & the rationale behind each choice
Every layer below is a deliberate choice with a security or operational trade-off attached. The evidence column points to the file you can read for yourself.
| Layer | Choice | Why | Evidence in repo |
|---|---|---|---|
| Backend framework | Spring Boot 4.0.0 on Java 25, built with Maven 3.9+ | Mature enterprise tooling, hardened security primitives, deep SA hiring pool. JaCoCo + Flyway + Actuator give us coverage gates, replayable migrations, and ops introspection out of the box. | backend/pom.xml (spring-boot-starter-parent 4.0.0, java.version 25) |
| Frontend | Next.js 15.1 + React 19 + TypeScript 5.6 (strict) + Tailwind 3.4 | SSR / RSC give us SEO on marketing pages; route handlers act as a BFF so JWTs stay in httpOnly cookies and never reach the browser JS heap. TypeScript strict mode catches contract drift between API and UI at compile time. | frontend/package.json |
| Database | PostgreSQL 16, single-region managed (Railway, eu-west Frankfurt) | ACID, native enums for risk levels and roles, JSONB for contributing-factor maps, partial indexes for tenant-scoped queries. Same engine we develop against locally — no cross-environment drift. | backend/src/main/resources/application-prod.yml |
| Authentication | Spring Security + JWT (60-min access, 7-day refresh), BCrypt cost 12 | Stateless bearer auth across SPA, mobile SDKs, server-to-server, and webhooks. BCrypt cost 12 follows OWASP 2024 fintech guidance. CSRF is explicitly disabled and justified inline (no session cookie surface to ride). | backend/src/main/java/io/finveil/config/SecurityConfig.java:61 (BCRYPT_COST=12); application.yml:151 (access-token-expiration 3 600 000 ms) |
| PII encryption at rest | AES-256-GCM per-field via JPA AttributeConverter (random 96-bit IV, 128-bit tag) | Authenticated encryption with per-field IVs prevents both confidentiality and tamper-tag bypass. The lazy-init pattern keeps the Spring context bootable in test slices that never touch encrypted columns. | backend/src/main/java/io/finveil/common/EncryptedStringConverter.java:41 (Converter, CIPHER_ALGO "AES/GCM/NoPadding") |
| Searchable PII (e.g. user.email) | HMAC-SHA256 blind index in a sibling column (email_hash) | Random-IV AES makes the ciphertext non-deterministic so a plain unique index is impossible. The HMAC blind index gives O(1) login lookups without leaking the plaintext to anyone holding only the DB dump. | backend/src/main/java/io/finveil/auth/User.java:33 (email encrypted, email_hash HMAC); UserDetailsServiceImpl.hmacEmail() |
| Webhooks (Paystack/Yoco/PayFast/Ozow/Stitch/Rapyd/BulkSMS/SMSPortal/WhatsApp) | HMAC-SHA256/SHA512 constant-time verification, signature IS the auth boundary | Inbound paths live under /api/webhooks/** and are permitAll at the filter chain. The per-controller signature check uses MessageDigest.isEqual to defend against timing oracles. | backend/src/main/java/io/finveil/webhook/HmacSha256Verifier.java:32; SecurityConfig.java:175 ("/api/webhooks/**") |
| Hosting / edge | Railway (Frankfurt) for API + Postgres; Vercel for marketing; TLS 1.3 everywhere | Managed Postgres with point-in-time recovery, automatic Let's Encrypt cert rotation, container-level isolation. Marketing is fully static-rendered so an outage on the API never blocks the brand surface. | backend/src/main/resources/application-prod.yml |
| Schema migrations | Flyway, versioned + checksummed, 75 migrations on HEAD | Replayable, environment-agnostic, fails fast on checksum drift so a hand-edited migration cannot brick Railway boot. | backend/src/main/resources/db/migration/V*.sql |
| Async / guaranteed delivery | Transactional outbox + poller with exponential backoff (5s → 30s → 5m → 30m → 2h, then DLQ) | Outbox row commits in the same transaction as the business write, so the platform cannot lose an event between commit and dispatch. Dead-lettered operations are surfaced in /admin/operations for human review. | backend/src/main/java/io/finveil/outbox/OutboxService.java:36 (BACKOFF schedule) |
| Audit log | Append-only audit_log table, write on every mutating controller action | Captures user id, employer id, action, entity, IP, timestamp, JSONB details. Retained ≥12 months. AuditLogService writes in REQUIRES_NEW so an audit failure never silently swallows a business commit. | backend/src/main/java/io/finveil/audit/AuditLogService.java (Propagation.REQUIRES_NEW) |
| Cryptographic receipts | Per-transaction token with optional Stellar anchoring for independent verifiability | A counterparty holding only the token id can verify a payment receipt against a public endpoint without any FinVeil credentials. Anchoring is opt-in and toggled per environment. | backend/src/main/java/io/finveil/tokenization/ (TokenService, StellarAnchorService, ReceiptVerificationController) |
2. Security architecture — request lifecycle
Default-deny at the Spring filter chain. CSRF rationale, public-endpoint surface and the order of ApiKeyAuthenticationFilter vs JwtAuthenticationFilter are all documented inline in SecurityConfig.java so a reviewer can audit the surface at a glance.
1. Browser / mobile / partner API caller
Next.js SPA
TypeScript strict
Mobile SDKs
Kotlin / Swift / Flutter
Server-to-server
fvk_… API key or JWT
2. BFF / edge (auth boundary)
Next.js route handler
httpOnly cookie → JWT bearer
CORS allow-list
baseline + env, never wildcard
Tenant-scoped rate limit
per employerId
3. Spring Boot 4 backend (default-deny)
JwtAuthenticationFilter
15-min refresh window
ApiKeyAuthenticationFilter
fvk_… for /v1/**
RBAC: ADMIN / HR_MANAGER / VIEWER
AuditLog interceptor
every mutating action
4. Persistence (multi-tenant by employer_id)
PostgreSQL 16
Railway managed, daily PITR backups
EncryptedStringConverter
AES-256-GCM per-field
HMAC blind index
email_hash for O(1) lookup
Outbox + poller
5 retries → DLQ
5. External providers (signed webhooks both ways)
Paystack · Yoco · PayFast · Ozow · Stitch · Rapyd
HMAC-SHA256/512 verify
BulkSMS · SMSPortal · Africa's Talking
token / signature
WhatsApp Cloud API
X-Hub-Signature-256
Stellar anchor (optional)
public receipt verifiability
3. Compliance posture — what we have, what we do not, by regulation
Honesty earns more trust than puffery. The "gap" column is the bit a regulated buyer cares about most; it is filled in for every row.
| Regulation | What it requires | What FinVeil has done | Evidence | Gap |
|---|---|---|---|---|
| POPIA (Protection of Personal Information Act, 2013) — South Africa | PII protection, lawful processing, consent, breach notification. | Every PII column is AES-256-GCM encrypted. Consent is tracked per-employee with version + timestamp. Audit log on every read/write. Data-subject-request workflow surfaced in /admin/compliance. | EncryptedStringConverter.java; Employee.consent_status enum; AuditLogService.java | POPIA Information Officer registration pending with the Regulator. |
| King IV (Corporate Governance) — South Africa | Board governance, risk register, ethical decision-making. | Risk register maintained internally; quarterly review cadence. Founder-led; advisory board formation under way. | docs/governance/ (internal) | Independent non-executive directors not yet appointed (pre-Series A milestone). |
| FICA (Financial Intelligence Centre Act) — South Africa | KYC, source-of-funds, sanctions screening on counterparties. | Recipient verification flow operational at /admin/verifications. Beneficiary metadata captured before any disbursement leaves the platform. | frontend/app/(app)/admin/verifications/ | Automated sanctions-list screening (OFAC / DFSA / SAMA) integration scoped, not yet shipped. |
| SARB (South African Reserve Bank) — regulatory rates | Use of current repo / prime / CPI rates in customer-facing calculations. | Regulatory rate snapshots tracked and versioned; admin-only mutations behind ADMIN role. Public read endpoint surfaces freshness metadata. | frontend/app/(app)/admin/regulatory-rates/; SecurityConfig.java:226 (/api/regulatory/public/**) | Automated SARB scrape with provenance attestation is roadmap (currently human-validated entry). |
| PCI-DSS | Card-data protection. | FinVeil never stores or transits PAN/CVV. Card tokenisation handled by the underlying provider (Paystack / Yoco / PayFast). FinVeil holds only the provider-issued reference token. | backend/src/main/java/io/finveil/orchestration/provider/adapters/ | Self-assessment Questionnaire (SAQ-A) available on request; no QSA attestation engaged. |
| GDPR — applicable in Mauritius via EU adequacy framework | EU data protection norms; right to access, rectify, erase, port; lawful basis. | Same control surface as POPIA covers all GDPR principles. Data is stored in eu-west Frankfurt today, so cross-border transfer mechanics are not yet triggered. | EncryptedStringConverter.java; Privacy Policy at /legal/privacy | EU DPO appointment deferred until first EU customer. |
| NCR (National Credit Regulator) — South Africa | Credit-agreement origination. | FinVeil does not originate credit. Earned-wage advances route through registered credit providers who hold NCR licences. Affordability calculations follow NCR Regulation 23A. | backend/src/main/java/io/finveil/disbursement/proof/ (affordability + receipt) | NCR juristic-credit-provider registration in progress for future first-party offerings. |
4. Data protection in detail
Per-field encryption
AES-256-GCM applied at the JPA layer via EncryptedStringConverter (line 41). Each ciphertext carries its own random 96-bit IV and 128-bit authentication tag — tamper detection is at the field level, not the disk level. A 32-byte key mismatch surfaces a sentinel in logs rather than crashing the request so the issue is investigable.
Searchable PII
For columns that need an equality lookup (e.g. users.email at login), we store an HMAC-SHA256 blind index in a sibling column — User.emailHash (User.java:38). The plaintext is never written to the database; only the constant-time HMAC is.
Multi-tenant isolation
Every tenant-scoped row carries an employer_id. All repository methods use findByIdAndEmployerId-shaped lookups so a controller cannot accidentally cross tenants. Example: EmployeeRepository.java:23. Cross-tenant guard rails are enforced both in JPA and at the controller boundary.
Key custody — honest
Today the JWT signing secret and the AES key (FINVEIL_ENCRYPTION_KEY) live in Railway environment variables — the standard 12-factor posture. This is the right answer for an early-stage platform; the enterprise next step is HashiCorp Vault or AWS KMS, which is a one-PR swap behind the resolveKey() method.
In-transit security
TLS 1.3 end-to-end. Let's Encrypt certificates provisioned and rotated automatically by the Railway and Vercel edge. HSTS, SameSite, and httpOnly are set on every cookie minted by the BFF route handlers; no JWT ever reaches the browser JS heap.
Backups & recovery
Daily PostgreSQL snapshots through Railway with point-in-time recovery over a 7-day window. RTO is measured in minutes for a single-region recovery. A staging restore drill is run quarterly against the production snapshot.
5. Operational readiness — what is in place today
- 715 backend tests (JUnit 5) + 1 404 frontend test cases (Jest + RTL).
- JaCoCo 50% line-coverage gate at the BUNDLE level — fails the build, not a warning. Exclusions are limited to live-credential-required adapters and listed explicitly in backend/pom.xml.
- GitHub Actions CI: backend unit + integration, frontend tsc + jest, OWASP dependency-check, E2E smoke, SDK sync check, ISO-standards drift detection.
- SDK ecosystem in 9 languages (Java, TypeScript/Node, Python, Go, Kotlin, Rust, C#, PHP, Flutter); 3 implemented to GA, 6 scaffolded against the published OpenAPI spec.
- Cryptographic receipts on every disbursement; optional Stellar anchoring for third-party verifiability without sharing credentials.
- /api/health and /actuator/health UP; /admin/operations surfaces the outbox dead-letter queue for human review.
- Demo mode for partner walkthroughs that uses seeded data and short-circuits all live provider calls — Investec stakeholders can drive the platform without touching prod.
6. What is not yet in place
Investec will trust an honest gap list more than an aspirational one. Here is ours.
- External penetration test — not yet conducted. Budget allocated for an accredited SA CREST firm; would commence on first enterprise sign-on.
- SOC 2 Type I / ISO 27001 — not started. The control set above maps cleanly to both frameworks; we would engage Vanta / Drata in parallel with the first enterprise deal.
- PCI-DSS QSA attestation — not applicable (no card data stored), but a written SAQ-A self-assessment is available under NDA.
- POPIA Information Officer registration — pending with the Regulator; designation already complete internally.
- Multi-region active/active — single-region (Frankfurt) today. Cape Town read-replica is on the roadmap; full active/active would follow a regulated multi-region customer commitment.
- Hardware security module / Vault for key custody — current AES + JWT secrets are env-var-managed (the standard 12-factor pattern). HashiCorp Vault or AWS KMS is the explicit enterprise next step and is a one-PR swap behind the EncryptedStringConverter interface.
7. Why FinVeil for Investec Mauritius specifically
Adapter pattern means MauCAS / IPS is one implementation away
PaymentProviderAdapter is a sealed interface; Paystack, Yoco, PayFast, Ozow, Stitch and Rapyd each ship as ~300-line implementations. Adding MauCAS or Mauritius IPS is the same shape of work — no schema migration, no router rewrite.
20-currency reconciliation aligns with Investec Mauritius capability
Settlement and reconciliation entities carry an ISO-4217 currency column from day one. Multi-currency wallet UI ships in the demo; we are not retrofitting i18n on top of a ZAR-only core.
SA → MUR cross-border corridor architecture sound, awaits sponsorship
Disbursement routing already chooses between rails by cost, latency, and counterparty trust. A SA → MUR corridor adds a destination provider; the routing intelligence does not change. Investec Mauritius sponsorship into MauCAS is the missing piece.
Demo mode lets stakeholders evaluate before integration commitment
demo.finveil.money runs the full platform with seeded data and provider shims. Risk officers, compliance, and ops can drive flows end-to-end before any contract or production credential exchange.
Want the full compliance pack under NDA?
Architecture diagrams, the SAQ-A self-assessment, the POPIA impact assessment, the data-flow inventory, and the risk register are all available under NDA. We will respond within one business day.
Email zared@finveil.money