Multi-tenant data with rules the code cannot skip
Row level security puts tenant isolation in PostgreSQL, where every client path has to respect it, including the Data API and a direct psql session.
- Isolation
- In the database
- Policies apply to every connection, not just your ORM.
- Access
- Scoped keys
- Per-integration credentials with explicit permissions.
- Evidence
- Audit log
- Control-plane changes are attributed and durable.
Tenant isolation that survives a bug
A missing WHERE clause in application code is a normal mistake. A row level security policy makes it a non-event rather than a data breach.
Policies per command
SELECT, INSERT, UPDATE and DELETE get their own policies, because who may read a row and who may change it are different questions.
Serverless-friendly access
The Data API runs parameterized SQL over HTTPS, so an edge function can query without holding a Postgres socket. RLS applies there too.
Credentials per consumer
Your web app, your worker and your customer's webhook receiver each get a scoped key. Rotation is targeted rather than a coordinated outage.
Background reconciliation
Long-running functions handle the sync jobs, invoice runs and retries that do not fit inside a request timeout.
Answers for the security questionnaire
Scoped keys, row level policies, an audit log and rootless containers with dropped capabilities. Concrete mechanisms, not badges.
Where the tenant boundary actually lives
Three separate mechanisms carry multi-tenancy on Veltic: policies inside the database, one login role per database, and platform roles for the people on your team. They fail independently, which is the point.
1. Row level security per tenant
- iduuidnot null
- tenant_iduuidnot nulltenant boundary
- total_centsbigintnot null
- created_attimestamptznot null
using (tenant_id = current_setting('veltic.tenant')::uuid)
The same query, two tenants
- tenant_id = acme8 214visible
- tenant_id = globex3 907filtered out
- tenant_id = initech1 442filtered out
The same query returns 8 214 rows for acme and 3 907 for globex. The application never sends a tenant filter.
2. One PostgreSQL role per database
| Role | Database | Connections | Grants |
|---|---|---|---|
| usr_orders | orders | 10 | select, insert, update, delete |
| usr_billing | billing | 10 | select, insert |
| usr_analytics | analytics | 10 | select |
A role can only connect to the database it belongs to. Rotation is a single statement and affects one consumer.
3. Platform roles for your team
| Capability | Master | Admin | Developer | Readonly |
|---|---|---|---|---|
| Read project resources | granted | granted | granted | granted |
| Run write statements | granted | granted | granted | not granted |
| Create databases | granted | granted | not granted | not granted |
| Restore a backup | granted | granted | not granted | not granted |
| Manage users and roles | granted | not granted | not granted | not granted |
| Change platform settings | granted | not granted | not granted | not granted |
How a tenant request reaches its own rows
Tenant request
The session token identifies the account, the account resolves to a tenant.
- Authorization: session
- tenant = acme
Edge worker
Resolves the tenant once per request and never trusts a client supplied tenant id.
- readDashboardSession()
- set_config('veltic.tenant')
usr_orders
The connection uses the database role, not a superuser, so policies are enforced.
- connection limit 10
- no bypassrls
Filtered rows
PostgreSQL applies the policy before the rows leave the server.
- 8 214 of 13 563 rows
- audit entry written
Answers for a security questionnaire
- Isolation in the databaseRow level security is evaluated by PostgreSQL, so a forgotten WHERE clause in application code cannot cross a tenant boundary. The Data API and a direct psql session go through the same policies.
- One role per databaseEach managed database gets its own login role named usr_<database> with its own connection limit. A leaked credential reaches exactly one database, and revoking it does not touch the others.
- Attributed control planeCreating a database, rotating credentials, restoring a backup and changing a role all land in the audit log with the acting account, the address and the payload.
- Constrained runtimeApplications and functions run as rootless containers with a read only filesystem, all capabilities dropped, no new privileges and a hard memory limit enforced by the kernel.
A data processing agreement, the hosting region and subprocessor list come from the operator's contract documents, not from this page. Ask for them before an assessment.
Test policies as the application role
A policy that looks correct as a superuser can be wrong for the role your application connects with. Switch roles in the SQL editor and run the exact queries your code issues before you trust the isolation.
What we will not claim
Container isolation is not a dedicated virtual machine. Any assessment of data protection, hosting region or a data processing agreement needs the operator's actual contract documents, not a marketing page. Ask for them.
Build the whole backend in one project
One plan, one dashboard, one command line interface. PostgreSQL, applications, functions, storage and realtime.