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
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_MAXafter checking pooler headroom. - Object storage — Supabase Storage with private buckets
evidence(50 MB/file) andsbom(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…
- Middleware (
src/middleware.ts) validates the Supabase session cookie and rotates tokens if needed. - Route handler calls
requireOrgContext()to resolve{ userId, userEmail, userName, orgId, role }. - RBAC via
requirePermission(ctx, action, entity)— 9 roles × 25+ entity types. A failed check returns403before any DB round-trip. - Data access via Prisma, always with an
orgIdpredicate. Repository helpers reject unscoped queries at type-check time. - Audit log via
logAudit()/logUpdate()for every mutation — storesbefore+aftersnapshots. - 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
orgIdfrom 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 assertsrow.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,
JobExecutionrows, 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.