Security & trust
Your tenant boundary is enforced by the database, not only by our application code.
Funnel One is a GTM platform for running a B2B funnel under one set of rules, from first touch to closed-won, and it is multi-tenant. Every tenant-scoped table carries an organization column, has row-level security enabled and forced, and has a policy pinning reads and writes to the active tenant. The application connects as a role that cannot bypass that policy. If our code forgets a where clause, Postgres still returns nothing.
The boundary
One tenant identity, and no second way to be a tenant.
There is exactly one tenant identity field, and it is the same one on every table. No secondary workspace id, no per-feature scoping scheme, nothing that could disagree with the primary boundary. Every table that holds tenant data has the same four things: the organization column, row-level security enabled, row-level security forced so even a table owner is subject to it, and a policy whose read and write clauses both pin the organization to the one in the active session context. That context is set per request, from the session — never from anything in the request body.
ALTER TABLE funnel_transitions ENABLE ROW LEVEL SECURITY;
ALTER TABLE funnel_transitions FORCE ROW LEVEL SECURITY;
CREATE POLICY funnel_transitions_tenant_isolation ON funnel_transitions
USING (organization_id = current_setting('app.organization_id', true)::bigint)
WITH CHECK (organization_id = current_setting('app.organization_id', true)::bigint); Background work
A scheduled job runs inside one tenant at a time, or it declares that it doesn’t.
The place multi-tenant systems usually leak is not the request path — it is the cron. So there are two allowed patterns and no third. A job either fans out and does each tenant’s work inside that tenant’s own context, or it is explicitly a platform-level job and has to say, in code, what crosses the boundary and why. A job with no declared reason does not merge. The escape hatch exists, it is narrow, and every use of it is written down where an auditor can grep for it.
Enforcement
The rules are checked by the build, not by a memo.
A new table that misses any part of the envelope fails the build. A route added outside the authentication gate fails the build. The set of tenant tables is derived by reading the migrations rather than maintained by hand, so it cannot drift from what is actually deployed. And the isolation tests run against real Postgres as the restricted application role, with the checks proven to fail when the protection is removed — because a test that passes either way is not a test.
- A new tenant table without row-level security — the build fails.
- A new
/apiroute outside the auth gate — the build fails. - An isolation test that still passes with row-level security off — the build fails.
Questions
Do you have SOC 2?
No. We are not SOC 2 certified, and we are not going to imply a date we have not committed to. What we can show you instead is the mechanism underneath, which is what the audit would examine anyway. Every tenant-scoped table carries row-level security, enabled and forced, and the application connects as a role that cannot bypass it. Every background job runs inside one tenant at a time, or declares in code what crosses the boundary and why. And the build refuses a new table that misses any part of that envelope. The three sections above are that answer in detail.
Can one tenant’s data reach another?
Not through the application, because the application’s database role cannot bypass row-level security, and the policy is forced rather than merely enabled. Cross-tenant reads exist in exactly the places they must — the list of organisations a user belongs to, for instance — and each one is individually documented with what crosses and why. The design position is that isolation is enforced at the last possible layer, so a mistake anywhere above it fails closed.
Security
Send us your questionnaire.
We would rather answer a specific question than publish a page of reassurance. If you are running a review, tell us what you need and who needs it.