Tenant isolation in MAEL is a database primitive, not application logic layered on top of a shared table. Every tenant-scoped table carries a PostgreSQL Row-Level Security (RLS) policy, enforced by the database role a connection uses — independent of whether application code remembers to filter by tenant_id.

Four database roles

Application code picks a role by which session factory it uses (core/database/session.py) — there’s no single shared connection string that “just happens” to see everything. BYPASSRLS is confined to a deliberately small platform connection pool.

Fail-closed by design

seo_app has a sentinel default for app.current_tenant_id:
A session that forgets SET LOCAL app.current_tenant_id = ? doesn’t see every tenant’s data — it sees zero rows, because no real tenant has that UUID. This is verified by a restore test: even a fresh database restored from backup preserves the fail-closed default, and cross-tenant isolation is asserted directly in the test suite (a tenant A JWT cannot read tenant B’s data, checked against the real database, not mocked).

Transactional tenant context (ADR-003)

SET LOCAL only takes effect for the duration of the current transaction — so tenant context is always set inside an explicit transaction, per request. This closes a real class of bug: SET (without LOCAL) on a pooled connection can leak tenant context to the next request that happens to reuse the same physical connection from the pool. SET LOCAL inside a transaction cannot leak past COMMIT/ROLLBACK.

Super admin (ADR-005)

Platform-level operations (cross-tenant support, billing, ops) go through seo_platform’s BYPASSRLS, gated by the super_admin role — a deliberately separate access path, not “tenant_id NULL” sprinkled through tenant-facing code. super_admin access tokens are additionally tracked in a Redis denylist (revocable before expiry), unlike ordinary sessions which rely on the session-active check.

Tenant-scoped resources

Beyond row data, isolation extends to:
  • Memory — session and long-term memory are namespaced per tenant (Redis key prefix, RLS on PostgreSQL tables)
  • Rate limits and budgets — per-tenant token buckets and monthly_budget_usd enforced pre-dispatch
  • Feature flags — per-tenant overrides on top of platform defaults
  • API keys and sessions — scoped to one tenant (or NULL for super_admin, never both)

Security

Authentication