Skip to content

Security

Every platform in the ecosystem is built on one security baseline. This page defines that baseline — when adding a new platform, treat these as requirements, not options.

Defence in depth

Layered on the principle that no single layer is sufficient on its own:

Layer Protection
Edge (nginx) TLS, HSTS, rate limiting, body-size limits
App (HTTP) Security headers, CORS allow-list, CSRF, timeouts
App (logic) RBAC, permission checks, input validation
Database RLS, parameterised queries
Audit Hash-chained audit log

Tokens never reach client JS

The BFF (Backend-for-Frontend) pattern is the foundation of frontend security.

The browser never talks to the backend directly — it only talks to Next.js routes on the same domain, and those proxy to the backend from the server side. The token lives in an httpOnly cookie.

The result: XSS stops being a token-theft vector. Even if an attacker manages to run JavaScript, they cannot reach the token.

On top of that comes double CSRF protection: a custom header plus an origin check.

Security headers

On every response:

Header Purpose
Content-Security-Policy Closes off the execution vector for XSS
Strict-Transport-Security Enforces HTTPS (HSTS)
Cross-Origin-Opener-Policy Isolates against cross-window attacks
Cross-Origin-Embedder-Policy Controls external resources
Cross-Origin-Resource-Policy Restricts external use of resources

CORS is allow-list based — never *.

Postgres Row-Level Security

Isolation of organisation and user data is enforced at the database level, not through a WHERE clause in application code.

Why: one forgotten WHERE user_id = ? in application code leaks data. RLS applies that check to every query, regardless of coding mistakes.

Boot-time enforceability guard

On start-up the app verifies that RLS is genuinely active. If a policy is missing, or the connecting user has the right to bypass RLS, the app refuses to start.

The reason is that RLS silently turning off is the most dangerous case of all — everything looks like it is working normally.

SQL

  • Parameterised queries — no string concatenation.
  • No ORM — hand-written SQL on pgx. Every query is visible.

Rate limits and timeouts

Protection Where
Authentication rate limit (strict) /login, /oauth, /auth, /api/auth paths
General app rate limit (lenient) All other paths
API rate limit Public APIs such as /v1/*
HTTP server timeouts read · write · idle · header — all configured
Body-size limit At the edge

Strict limits only on authentication

Applying the strict authentication rate limit to every path makes Next.js RSC prefetches exceed the limit and return 503. So the strict limit goes only on the authentication paths, with a lenient limit elsewhere.

Secrets management

  • Token encryption — third-party OAuth tokens (Google Drive, Dropbox and so on) are stored encrypted with AES-256-GCM.
  • Cryptographic keys — in HSMs; on-premise and cloud HSMs fail over to each other.
  • Application secrets — via environment variables, never committed to git.

Never put secrets in the repository

Server credentials, API keys, client secrets, HSM PINs — none of these are ever written into a repository, a script or documentation. Deploy scripts accept secrets only through environment variables.

If a secret is accidentally exposed: (1) rotate it immediately, (2) review the access logs, (3) move it into a secrets-management system.

Endpoints closed in production

/metrics and /swagger expose internal structure and endpoint listings. In production they are behind a bearer token.

Audit log

A hash-chained, append-only log. Each record contains the hash of the one before it.

What that means: no record in the middle can be deleted or altered — the chain breaks and the integrity check fails. Only admins can read it, and there is a separate operation to verify integrity.

Signatures and non-repudiation

Actions with legal effect require a PIN2 signature — authentication (PIN1) is not sufficient. See Authentication & authorization for the detail.

The private signing key is split by 2-of-2 threshold ECDSA — neither party can produce a signature alone.

Testing

  • Unit tests — business logic and permission checks.
  • testcontainers integration tests — against a real PostgreSQL/Redis, exercising RLS for real.
  • Golden vector tests — cryptographic wire compatibility.

Security checklist

Before a new platform goes to production:

  • [ ] TLS + HSTS active, certificates renew automatically
  • [ ] All security headers configured (CSP · HSTS · COOP · COEP · CORP)
  • [ ] CORS allow-list — no *
  • [ ] Rate limits: strict on authentication, lenient elsewhere
  • [ ] All HTTP server timeouts configured
  • [ ] RLS active and the boot-time guard working
  • [ ] Every query parameterised
  • [ ] Tokens in httpOnly cookies, never reaching client JS
  • [ ] Double CSRF protection
  • [ ] /metrics and /swagger closed
  • [ ] Audit log being written, integrity verifiable
  • [ ] Secrets via environment variables, absent from the repository
  • [ ] Integration tests genuinely exercising RLS