← Back to Compliance Center
🛡

Data Isolation

Three-layer architecture proving your data is fully separated from every other customer.

Last reviewed 2026-04 · Engineering · Owned by AI Safety Officer

On this page

  1. Overview
  2. Layer 1 — Application
  3. Layer 2 — Database (RLS)
  4. Layer 3 — Infrastructure (Enterprise)
  5. Live proof endpoint
  6. Related documents

Overview

Votriz is a multi-tenant SaaS — many customers share the same infrastructure. Three independent isolation layers ensure no customer can read, write, or even infer another customer's data. A failure in any single layer does not cause a cross-tenant breach, because the layers are independent.

Layer 1 — Application

Every authenticated request carries a JWT. The server extracts org_id from the JWT payload — never from request body, query parameters, or headers. Every database query is then filtered by that org_id.

The architectural invariant

# CORRECT — org_id from the JWT org_id = user["org_id"] brands = await conn.fetch( "SELECT * FROM brands WHERE org_id = $1", org_id ) # WRONG — never accept org_id from client input brands = await conn.fetch( "SELECT * FROM brands WHERE org_id = $1", body.org_id )

Pre-deploy gate

Our pre_prod_validation.sh script provisions two isolated organizations, seeds data in Org A, and asserts Org B cannot reach any of it across every tenant-scoped endpoint:

TestWhat it proves
GET /brands/{A's brand} as BReturns 404, never the row
GET /monitoring/mentions as BZero mentions, never A's
GET /ghost-presence/{A}/config as BNo config visible
PATCH /ghost-presence/{A}/config as B404 — write blocked
GET /audit/log as BNever sees A's audit rows
JWT org_id integrityJWT carries the calling org only

Every assertion must pass before any code reaches production. A single failure blocks the deploy.

Layer 2 — Database (RLS)

PostgreSQL Row-Level Security is enabled on every tenant-scoped table. Even if application code somewhere forgot a WHERE org_id = filter, the database itself rejects rows that don't belong to the current session.

Protected tables

TableWhat it holds
brandsBrand profiles, voice settings
channelsConnected social accounts
content_queueAI-generated content
published_postsPublished content history
email_subscribersEmail contact lists
email_campaignsCampaign data
email_sendsPer-recipient send records
mentionsBrand mentions discovered online
presence_incidentsBrand-monitor incidents

How an isolation policy looks

CREATE POLICY brands_isolation ON brands USING (org_id::text = current_setting('app.current_org_id', true));
Honest note on activation: the policies are deployed but the application's connection role currently bypasses RLS (database superuser). Layer 1 is the primary guard today; the connection role flips to a non-superuser in Q4 2026 alongside the SOC 2 Type I audit, at which point Layer 2 enforcement becomes active in production. The /security/isolation-proof endpoint reports the live status honestly — we don't overstate what's enforced.

Layer 3 — Infrastructure (Enterprise)

Enterprise customers can opt into stricter physical separation:

Available as a contract addendum — talk to [email protected].

Live proof endpoint

The /security/isolation-proof endpoint returns a machine-readable attestation any authenticated owner / admin can hand to their security team:

GET /security/isolation-proof Authorization: Bearer <owner-token> { "org_id": "your-org-uuid", "isolation_layers": { "application_level": { "status": "active", ... }, "database_level": { "status": "deployed_inert", "rls_tables": [...] }, "audit_log": { "status": "active", "immutable_trigger_present": true } }, "your_data": { "brands": 3, "channels": 5, "content": 142, "subscribers": 1820 }, "system_totals": { "total_organizations": 87, "total_brands": 312 }, "encryption": { "in_transit": "TLS 1.3", "at_rest": "AES-256", ... } }

Note that system_totals only exposes counts — your queries can never reach another org's row contents.

Related documents

Questions or a custom security review?

Enterprise customers receive dedicated security reviews and direct access to our security team. Reach us anytime at [email protected].

Talk to security →