Security
Boundaries
| Boundary | Enforced by | Page |
|---|---|---|
Is this request from the company network? |
Vercel WAF rules in |
|
Is this a real user? |
Supabase Auth session cookies, checked by middleware and every route |
|
Is this user allowed here at all? |
|
|
Which workspace’s data? |
|
|
Can they do this to the workspace? |
Role checks in the members routes |
|
Is this machine caller legitimate? |
|
|
Did this webhook really come from the provider? |
Svix, HubSpot HMAC, Nooks secret |
Non-negotiable rules
- Every tenant-scoped query filters on
subscriptionId -
Isolation is enforced in application code at the repository layer, not by PostgreSQL row-level security. There is no database-level backstop, so a missing filter is a data leak. See Multi-tenancy.
- Validate every input with Zod
-
Route handlers parse bodies and query strings before use. Client-side types are inferred from the same schema, so validation and typing cannot drift.
- Never build raw SQL from user input
-
Prisma parameterises everything. The places that emit raw SQL —
fetchAndLockPendingTargets,incrementSendingDomainDailyCountand the BigQuery gate builder — use bound parameters for values, and the BigQuery builder validates identifier names against a strict allowlist because identifiers cannot be parameterised. - The service-role key is server-only
-
lib/supabase/admin-client.tsis never imported from a client component. OnlyNEXT_PUBLIC_*variables reach the browser. - Machine secrets fail closed
-
verifyInternalSecret()rejects every request whenINTERNAL_API_SECRETis unset, rather than allowing it through. Scheduled jobs are Inngest cron functions behind the signed/api/inngestendpoint, so there are no cron routes to protect. - Firewall rules change only through
vercel.firewall.json -
Changes are reviewed in a pull request and applied from
mainwithpnpm firewall:apply. A route that machines call from outside the VPN must be added to the Allow machine routes rule and enforce its own auth, because the bypass makes it reachable from anywhere. - Sanitise user-generated HTML before rendering it
-
React escapes JSX by default, but email bodies are rendered as HTML. Anything user-authored that reaches
dangerouslySetInnerHTMLmust be sanitised first.
Known gaps
Recorded so they are decided rather than forgotten.
| Gap | Impact | Mitigation today |
|---|---|---|
|
Anyone who learns the URL can forge Retell call events. The firewall bypasses |
The URL is treated as a secret; correlation requires valid |
|
Forged events from any IP, because the firewall bypasses |
None yet; add shared-secret or signature checks |
|
Forged email events in an environment where it is not configured |
Set it in every deployed environment; the skip exists for local development |
|
Replays are not idempotent, and rows are stored under |
The payload is stored only and never dispatched; filter on |
No rate limiting on API routes |
Brute-force and abuse surface on auth endpoints |
Supabase Auth applies its own throttling on sign-in |
|
Two workspaces cannot hold different values for the same key |
Treat |
No PostgreSQL row-level security |
A repository method missing its |
Review checklist in Multi-tenancy |