Platform

Architecture

How OneComply is built — runtime topology, request lifecycle, multi-tenant isolation, and the EU data plane.

Owner: Platform TeamLast reviewed: 2026-05-22

Region

EU · Frankfurt

Vercel fra1 + Supabase eu-central-1

Runtime

Next.js 16.2

App Router · Node runtime

DB pool

3 conn / fn

Override with DATABASE_POOL_MAX

Observability

Sentry

Errors · traces · cron · metrics

Rendering diagram…
Runtime topology — EU data plane plus observability and analytics sinks.

Runtime Topology

OneComply is a serverless Next.js 16 (App Router) application deployed on Vercel in the Frankfurt (fra1) region. The data plane is a Supabase Postgres 15 cluster co-located in eu-central-1 with private object storage for evidence and SBOM artifacts.

  • Edge & compute — Vercel Functions (Node runtime). One request, one invocation; no long-lived workers.
  • Database — Supabase Postgres accessed via the session-mode pooler. Functions default to 3 Postgres connections each and can be tuned with DATABASE_POOL_MAX after checking pooler headroom.
  • Object storage — Supabase Storage with private buckets evidence (50 MB/file) and sbom (10 MB/file). All reads via signed URLs.
  • Auth — Supabase Auth, cookie-based SSR sessions via @supabase/ssr. Access tokens are JWTs; refresh tokens rotate on use.
  • Rate limiting — Upstash Redis sliding-window counters (in-memory fallback if Redis is unreachable).
  • Email — Resend, transactional only, EU data routing.
  • Payments — Stripe (Dublin) with subscription-lifecycle webhooks. Card data never touches our infrastructure.
  • Observability — Sentry captures scrubbed errors, traces, cron check-ins, release/source-map correlation, and operational metrics.
  • Analytics — Plausible covers cookie-less public-site analytics; PostHog remains scoped to authenticated product analytics when configured.

Request Lifecycle

Every authenticated request flows through the same spine:

Rendering diagram…
From browser to row — middleware, context, RBAC, data access, audit.
  1. Middleware (src/middleware.ts) validates the Supabase session cookie and rotates tokens if needed.
  2. Route handler calls requireOrgContext() to resolve { userId, userEmail, userName, orgId, role }.
  3. RBAC via requirePermission(ctx, action, entity) — 9 roles × 25+ entity types. A failed check returns 403 before any DB round-trip.
  4. Data access via Prisma, always with an orgId predicate. Repository helpers reject unscoped queries at type-check time.
  5. Audit log via logAudit() / logUpdate() for every mutation — stores before + after snapshots.
  6. Response — JSON; errors use a consistent { error: string } shape with the correct HTTP status.

Multi-Tenant Isolation

Every row in every domain table carries an orgId column. Three layers enforce isolation in depth:

  • Query layer — Prisma queries are always filtered by the org resolved from the server session. There is no code path that accepts orgId from the client.
  • Storage layer — evidence files live at evidence/{orgId}/{evidenceId}/filename. Before issuing a signed URL, the API re-fetches the DB row and asserts row.orgId === ctx.orgId.
  • Transport layer — signed URLs expire in 1 hour, scoped to a single object key.

Architectural invariant

Cross-tenant reads are impossible by construction: no handler can return a row without an orgId predicate, and object-storage paths are derived from the authenticated context rather than client input.

Resilience & Recovery

  • Backups — daily Postgres snapshots with 7-day point-in-time recovery, retained in region.
  • RTO / RPO — 4 hours / 1 hour targets (see DR runbook).
  • Rate limiting — 120 req/min per IP with a sliding window; burst capacity on protected endpoints.
  • Graceful degradation — Redis loss falls back to in-memory limits; Resend failures are retried with exponential backoff.
  • Cron — Vercel Cron runs per-job schedules with DB locks, JobExecution rows, dead-letter capture, and Sentry check-ins.

Why serverless + Supabase

  • EU residency by default — zero effort to keep data in Frankfurt.
  • Pooled connections — session-mode pooler absorbs burst traffic without per-request cold-start cost.
  • Managed backups + PITR — no self-hosted WAL shipping.
  • No SSH / OS maintenance — the attack surface is the application code.