Skip to Content
Security & TrustMulti-Tenant IsolationData Isolation Architecture

Multi-Tenant Isolation

VeriProof enforces strict data separation between tenant accounts through two independent isolation layers: EF Core global query filters at the application layer, and PostgreSQL Row-Level Security (RLS) at the database layer. Each layer enforces isolation independently, so a failure in one cannot expose another tenant’s data.

Tenant isolation in VeriProof is automatic and convention-based. Developers writing application logic do not need to add per-query customer ID filters — the system applies them universally.


The two-layer architecture

Customer JWT / API Key │ Resolved Customer ID ┌────────────────────────────────────────┐ │ Layer 1: EF Core Global Query Filters │ │ │ │ Reflection scans all entities with │ │ CustomerId property at startup. │ │ Adds WHERE customer_id = @id to │ │ every ORM query automatically. │ └────────────────────────────────────────┘ │ SQL query with customer_id filter ┌────────────────────────────────────────┐ │ Layer 2: PostgreSQL Row-Level │ │ Security (RLS) │ │ │ │ RLS policy enforces: │ │ customer_id = current_setting( │ │ 'app.current_customer_id') │ │ │ │ Even if Layer 1 fails, Layer 2 │ │ blocks cross-tenant reads. │ └────────────────────────────────────────┘ Customer's rows only

Layer 1: EF Core global query filters

All customer-scoped entities in the VeriProof data model define a CustomerId property. At application startup, CustomerPortalDbContext scans all registered entity types using reflection and automatically registers a global query filter for each one:

// Applied automatically — no per-query code needed: // SELECT * FROM attestations WHERE customer_id = @CurrentCustomerId // Application code simply queries: var sessions = await _dbContext.Sessions.ToListAsync(); // EF Core adds: WHERE customer_id = @CurrentCustomerId

Convention: any entity with a CustomerId property is automatically tenant-filtered. This is checked at startup, not at query time — there is zero performance overhead on individual queries.

Application developers cannot accidentally forget to add the customer filter to a new query, because the filter is not optional. The only way to bypass it is to explicitly call .IgnoreQueryFilters() — a deliberate action reserved for staff-portal admin contexts. All such bypasses are subject to a separate code review policy.

Tenant context propagation

The current customer ID is extracted from the authenticated request (JWT claim or resolved API key) and stored in an AsyncLocal<string> for the lifetime of the request. EF Core reads from this AsyncLocal when building query filter expressions.

This means tenant identity flows automatically through async/await chains without being explicitly passed as a parameter to every service method.


Layer 2: PostgreSQL Row-Level Security

Even if the application layer were to malfunction — a bug that bypassed the EF Core filter, or a raw SQL query that bypassed the ORM entirely — PostgreSQL RLS acts as a second, independent enforcement layer.

How RLS is configured

Before each database connection executes queries for a customer request, a session variable is set:

SET app.current_customer_id = '<resolved-customer-id>';

RLS policies on each table enforce this setting:

-- Example: sessions table CREATE POLICY customer_isolation_policy ON sessions FOR ALL TO customer_role USING (customer_id = current_setting('app.current_customer_id')::uuid); ALTER TABLE sessions FORCE ROW LEVEL SECURITY;

FORCE ROW LEVEL SECURITY ensures the policy applies even to the table owner — there is no privileged database role that can bypass it for production queries.

Staff vs. customer database roles

VeriProof uses two distinct PostgreSQL roles:

RoleIsolationUsed by
customer_roleFull RLS enforcementCustomer Portal API, Ingest API
staff_roleRLS bypassed (superuser equivalent for data access)Staff Portal API, admin operations

Staff Portal operations execute under staff_role with cross-tenant visibility. This is intentional — staff need to manage all customer accounts — but all staff-portal actions are audit-logged with the staff user’s identity.


What this means in practice

For customers building integrations

Your data is never shared with or visible to other VeriProof customers, by design. No API endpoint, no dashboard query, and no SDK call can return another customer’s sessions, decisions, or compliance records.

For enterprise customers with multiple applications

Each application within your account shares your tenant ID. Data is not further partitioned within a tenant by application in the database — application-level filtering is a query filter in the portal layer, not an isolation boundary. If you require per-application data isolation within your organization, contact the enterprise team about sub-tenant provisioning.

For audit purposes

Every inbound request to the Customer Portal API and Ingest API that resolves a customer ID sets the RLS context on the database connection. This happens in the request middleware before any business logic executes. The RLS context and query filter are both set from the same resolved customer ID, so they are always consistent.


Tenant provisioning and offboarding

Provisioning

When a new customer account is created:

  1. The customer’s customer_id UUID is assigned.
  2. A CustomerTenantKey (master API key component) is generated and stored encrypted.
  3. The customer’s row is inserted into the customers table.
  4. All subsequent data for this customer is automatically isolated under their customer_id.

No database schema changes, partition creation, or infrastructure modifications are required per tenant. The RLS policies and EF Core filters apply automatically based on the customer_id column.

Offboarding and data deletion

When a customer requests data deletion (GDPR Article 17, Right to Erasure):

  1. All sessions, attestations, and governance records with the customer’s customer_id are deleted or anonymized.
  2. For records already anchored to Solana, cryptographic erasure is applied: the off-chain content is deleted while the on-chain Merkle root remains (Solana state is immutable; VeriProof performs the GDPR Cryptographic Erasure procedure).
  3. The customers row is marked as deleted and eventually purged.
  4. API keys are revoked immediately.

FAQ

Are different VeriProof customers’ data stored in the same database?

Yes — VeriProof uses a shared database with row-level isolation rather than per-tenant databases. This is the industry-standard approach for SaaS platforms and is enforced by two independent isolation mechanisms (EF Core filters + PostgreSQL RLS). Per-tenant database isolation is available in the Enterprise Federated deployment option.

Can a VeriProof employee see my data?

Staff Portal access uses staff_role, which has cross-tenant visibility for specific administrative functions. All staff access is audit-logged with the staff user’s identity and is subject to VeriProof’s internal access control policies. SOC 2 Type II audit controls cover staff data access.

Does the isolation apply to the API Reference (Ingest API) endpoints?

Yes. The ApiKeyMiddleware resolves the Customer ID from the API key before any route handler executes, and the resolved ID is used to set both the EF Core tenant context and the RLS session variable for the duration of the request.


Next steps

Last updated on