AiHummer
English
Sign inAccount
v1.0.x
{ }Swagger

Row-Level Security

v1.0.x · updated 2026-06-26

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. Enabling RLS is a matter of setting AIHUMMER_DB_APP_URL — and local/standard (host-native) installs set that variable up automatically, so RLS is active out of the box. It is “opt-in” only in the sense that a custom or manual deployment must set AIHUMMER_DB_APP_URL itself.

[!NOTE] Without AIHUMMER_DB_APP_URL, the gateway uses the owner pool for everything and RLS is effectively not enforced. Set the restricted pool (which the standard installer does for you) to turn the database-level isolation on.

[!IMPORTANT] RLS is not tier-gated. It works identically on every plan, Community included. If the license screen shows “Row-level security” as unavailable, that is an inaccuracy of the screen, not the state of your database.

Where RLS is on already and where you must turn it on

How you installed RLS after the install
Standard install, the installer provisions PostgreSQL Active. The restricted role and AIHUMMER_DB_APP_URL are created for you
Your own or managed PostgreSQL (the database URL was supplied) Not active. You create the role and set AIHUMMER_DB_APP_URL
Rootless install (no administrator rights) Not active. Same as above

[!WARNING] Two mistakes that leave RLS “on” but not protecting anything. First: AIHUMMER_DB_APP_URL points at the table owner or a superuser — PostgreSQL exempts such roles from policies, and the gateway will still report RLS as active. Use a separate restricted role. Second: on your own PostgreSQL the restricted role may be created automatically with a predictable password — give it a password of your own before the database is reachable over the network.

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, delivery recovery and similar maintenance. 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