Row-Level Security
AiHummer is multitenant, and its strongest isolation boundary lives in the database itself: PostgreSQL Row-Level Security (RLS). With RLS enabled, the database — not just application code — enforces that a query only ever sees the rows belonging to the current tenant.
Why RLS
Application-level filtering (WHERE tenant_id = ...) is necessary but fragile: a
single forgotten clause can leak data across tenants. RLS moves the guarantee
into PostgreSQL, so that even an unfiltered query returns only the current
tenant’s rows. It is a defense-in-depth layer beneath the application’s own
scoping. For the broader multitenancy model — and how it pairs with idempotent
side-effects — see
Multitenancy & idempotency.
The restricted role (opt-in)
RLS is opt-in and is activated by giving the gateway a second database connection that uses a restricted role rather than the owner:
# /home/.aihummer/etc/gateway.env
# Owner pool — runs migrations, used for system/bypass operations
AIHUMMER_DATABASE_URL=postgres://owner:...@localhost/aihummer
# Restricted application pool — RLS policies apply (aihummer_app role)
AIHUMMER_DB_APP_URL=postgres://aihummer_app:...@localhost/aihummer
The aihummer_app role is not the table owner, so PostgreSQL applies RLS
policies to it. Application queries flow through this restricted pool. For
host-native installs the installer wires AIHUMMER_DB_APP_URL automatically.
[!NOTE] Without
AIHUMMER_DB_APP_URL, the gateway uses the owner pool for everything and RLS is effectively not enforced. Set the restricted pool to turn the database-level isolation on.
Per-tenant scoping
Inside a request, the application establishes the current tenant on the
connection before running tenant-scoped queries — conceptually db.WithTenant.
Once scoped, RLS policies on the restricted role limit every read and write to
that tenant’s rows. The scope is tied to the unit of work, so it does not leak
between concurrent requests.
request ─▶ resolve tenant ─▶ db.WithTenant(tenant) ─▶ queries see only that tenant
System / bypass mode for workers
Some work is legitimately cross-tenant or tenant-agnostic — background workers, schedulers, the outbox, and similar machinery. For these, the gateway uses a system (bypass) mode that runs on the owner pool, outside the per-tenant RLS policies, so infrastructure tasks can operate across the dataset.
[!WARNING] Bypass mode is for trusted internal workers only. Request-handling code paths that act on behalf of a user must always run through the restricted, tenant-scoped pool — never the bypass path.
Migrations run on the owner pool
Schema changes require privileges the restricted role does not have, so
migrations always run on the owner pool (AIHUMMER_DATABASE_URL), under an
advisory lock, at startup. The restricted aihummer_app role is used only for
ordinary application traffic. This keeps the privilege separation clean:
schema-changing operations use the owner; tenant data access uses the restricted
role with RLS applied.
Where to next
- Multitenancy & idempotency — the full tenant model and how side-effects stay safe under recovery.
- Secrets vault — per-tenant DEKs reinforce the same isolation at the secrets layer.
- RBAC & scoped API keys — authorization above the data layer.