Engineering
Backend, Security & Data Access
Server-side architecture, security posture, and data-access model for the Nordic Climate Finance Hub Investor Platform. Optimised for investor-grade due diligence: confidentiality, auditability, and least-privilege access to sensitive deal information.
Design principles
- Confidentiality by default — every record is private until an explicit grant says otherwise.
- Least privilege — every actor (user, service, integration) gets the minimum scope they need.
- Defence in depth — controls layered across network, application, database, and key-management planes.
- Audit everything — every access, decision, and export is recorded in an immutable log.
- EU data residency — primary storage in the EU; cross-border replication is opt-in per tenant.
Stack
- Runtime: TanStack Start server functions on an edge worker runtime; TypeScript strict mode.
- Database: Managed Postgres with row-level security (RLS) enforced on every table.
- Auth: OIDC / SAML SSO for investor tenants; short-lived JWT bearer tokens; SCIM provisioning.
- Object storage: Encrypted bucket with signed, expiring URLs for every upload and download.
- Secrets: Managed secret store; no secrets in code, env files, or client bundles.
- Validation: Zod at every trust boundary (request, server-fn input, webhook payload, env).
Identity & access
- Authentication: SSO (SAML / OIDC) for investor firms; email + password with mandatory MFA for individual members; hardware-key (WebAuthn) supported for partner / admin roles.
- Session policy: short-lived access tokens, refresh rotation, idle timeout, device binding for admin sessions.
- Provisioning: SCIM 2.0 for joiners, movers, and leavers; deprovisioning revokes sessions within 60 seconds.
- Roles:
admin,partner,analyst,observer,sponsor; roles stored in a dedicateduser_rolestable, never on the profile. - Authorization: RLS policies use a
security definerhas_role()function to avoid recursive checks.
Data access model
Access is evaluated in three layers. A request must satisfy all three to read or write a record.
| Layer | Enforces | Mechanism |
|---|---|---|
| Tenant | Investor firm isolation | tenant_id on every row; RLS predicate matches the caller's tenant. |
| Entitlement | Which opportunities / data rooms the tenant can see | entitlements table with explicit grants per opportunity, instrument, or DD workstream. |
| Role | What the caller can do once they can see it | Role check via has_role(auth.uid(), …); mutating operations gated server-side. |
- Data rooms require an active NDA acceptance and a non-expired grant; revocation invalidates signed URLs within 60 seconds.
- Cross-tenant joins are forbidden at the database layer; aggregate views (leaderboards, stats) are materialised through privacy-preserving server functions, never direct table access.
- Service role is used only by trusted server-side admin operations (backfills, webhooks) — never reachable from client code.
- Read paths default to the authenticated Supabase client so RLS applies as the caller; admin client is reserved for verified, server-only flows.
Encryption & key management
- In transit: TLS 1.3 only; HSTS preloaded; modern cipher suites; certificate pinning on mobile.
- At rest: AES-256 on database and object storage; envelope encryption for highly sensitive fields (KYC, beneficial ownership, banking).
- Key management: managed KMS with per-tenant data keys; automatic rotation; access logged.
- Backups: encrypted, geo-redundant within the EU; quarterly restore drills.
Confidentiality controls for documents
- Per-tenant, per-data-room signed URLs with short TTLs (default 5 minutes).
- Dynamic, per-user watermarking (name, email, timestamp, IP) on PDF and image previews.
- NDA gating: an unsigned NDA blocks all document access for that opportunity.
- Download controls per role; observers default to view-only with print disabled.
- Full access log: who viewed which document, when, from where, for how long.
Audit & immutability
- Append-only
audit_logtable covering authentication, access, mutation, decision, and export events. - Hash-chained entries; daily Merkle-root anchored to internal storage for tamper evidence.
- IC decisions, NDA acceptances, and DD sign-offs are hash-stamped and immutable.
- Closed reporting periods cannot be edited; restatements use append-only deltas.
Input validation & abuse prevention
- Zod schemas validate every server-fn input and webhook payload — type, length, format, range.
- Rate limiting on auth, search, document download, and export endpoints; per-tenant and per-IP buckets.
- Webhook signature verification with constant-time compare; replay protection via timestamp + nonce.
- Strict CORS allow-list; CSP with no inline scripts;
SameSite=Strictsession cookies. - File uploads scoped by MIME and size; antivirus scan before the file becomes accessible.
API conventions
- Internal calls: typed server functions via
createServerFn, gated by auth middleware. - External calls: REST + JSON; resource-oriented URLs; cursor pagination; RFC 7807 errors.
- Versioning: URI versioning (
/v1/…); breaking changes ship a new major. - Idempotency:
Idempotency-Keyrequired on POSTs that change state (invites, grants, transactions).
Example error
{
"type": "https://errors.ncfhub.org/forbidden",
"title": "Access not granted",
"status": 403,
"code": "dataroom.not_entitled",
"detail": "Tenant TEN_18f has no active grant for opportunity OPP_9c3.",
"instance": "/v1/datarooms/OPP_9c3/files"
}Compliance posture
- GDPR-aligned: lawful basis recorded per processing activity; DPIA on file for diligence data; DSR (access / erasure) workflows.
- ISO 27001 control framework adopted; SOC 2 Type II on the roadmap.
- Vendor due diligence on all sub-processors; sub-processor list published and versioned.
- Pen tests at least annually and after major releases; findings tracked to closure with SLAs by severity.
- Disclosed responsible-disclosure programme with a published security contact.
Out of scope
- MRV data ingestion or carbon-credit registry integrations.
- Payments, custody, or settlement of investor capital.
- Field data capture from sensors or mobile measurements.