Solutions · Multi-tenant SaaS

Multi-tenant Postgres without the foot-guns.

Pick the isolation model that fits — row-level security, schema-per-tenant, or database-per-tenant — on managed Postgres with per-tenant credentials enforced at the proxy.

Three patterns, one platform

Multi-tenant on Postgres comes in three flavors. RLS keeps everything in a single shared schema and uses policies to filter by tenant — cheap, simple, but harder to recover from a leaked policy. Schema-per-tenant gives each tenant its own schema in the same database — clearer isolation, easier per-tenant migrations, but breaks down past ~5,000 tenants. Database-per-tenant gives each tenant its own database (and on Luceris, its own credentials at the proxy) — strongest isolation, simplest disaster recovery, more clusters to manage. We support all three; the right answer depends on your tenant count, blast-radius tolerance, and per-tenant data volume.

Per-tenant credentials

When you pick DB-per-tenant, each cluster has its own credentials enforced by bcrypt at the proxy. A leaked password compromises one tenant, not your whole fleet.

RLS-friendly defaults

pg_hba and search_path are configured so RLS policies behave predictably under connection pooling. We document the gotchas (search_path leak, RESET ROLE) so you do not learn them in production.

Logical replication for migrations

Moving a noisy tenant from a shared cluster to a dedicated one is a publish/subscribe operation, not a downtime window.

Per-tenant observability

Audit log dual-sink (CloudWatch + Postgres) keeps a per-tenant trail of admin actions. Per-database query stats via pg_stat_statements.

Code sample

sql
RLS pattern with a session GUC
-- Tables carry a tenant_id; policies filter by it.
CREATE TABLE projects (
  id          BIGSERIAL PRIMARY KEY,
  tenant_id   BIGINT NOT NULL,
  name        TEXT NOT NULL
);
ALTER TABLE projects ENABLE ROW LEVEL SECURITY;

-- Policy reads the tenant from a session variable.
CREATE POLICY tenant_isolation ON projects
  USING (tenant_id = current_setting('app.tenant_id')::BIGINT);

-- Before each request, your app sets the session variable.
-- Use SET LOCAL inside a transaction so it cannot leak across pooled connections.
BEGIN;
SET LOCAL app.tenant_id = '42';
SELECT * FROM projects;
COMMIT;

How it looks on Luceris

For RLS or schema-per-tenant, one Postgres cluster holds all tenants; your app encodes the tenant in a session variable. For database-per-tenant, you provision one cluster per tenant via the dashboard or the API; each cluster has its own credentials and its own connection string. Either way, the Luceris proxy validates credentials and routes traffic — your app does not see the underlying topology.

Examples

  • Small team, 50 tenants, low per-tenant data volume → RLS in a single shared cluster
  • Mid-stage SaaS, 1,000 tenants with diverging schemas → schema-per-tenant in a single cluster, with per-tenant migrations
  • Enterprise SaaS with regulated per-tenant isolation → database-per-tenant, one cluster per tenant

Frequently asked questions

Does RLS hurt performance?
Modest overhead — typically 5–15% on filtered queries — because Postgres applies the policy as a WHERE clause. Far less than the cost of getting it wrong with hand-rolled tenant filters in application code.
How do you handle per-tenant migrations?
In schema-per-tenant, you iterate schemas and run migrations one at a time (or in parallel). In DB-per-tenant, you script the migration across the cluster list. We expose a list-clusters API to make scripting straightforward.
What about noisy tenants?
In a shared model, watch pg_stat_statements per tenant; move outliers to their own cluster via logical replication. In DB-per-tenant, scale the noisy cluster independently — the rest are unaffected.

Pick the isolation model that fits, not the one your vendor enforces.

Start on RLS in the Free tier; promote to DB-per-tenant when you need to.