Multitenancy & idempotency
AiHummer is multitenant: one gateway can serve many workspaces from one database while keeping their data apart. Two mechanisms make that safe — Postgres Row-Level Security (RLS) for read/write isolation, and an idempotency layer so that retries and turn recovery never duplicate a real-world side-effect.
Tenant isolation with Row-Level Security
Isolation is enforced at the database, not just in application code. AiHummer
runs queries through a restricted Postgres role (aihummer_app) that has RLS
policies applied, so the database itself rejects rows that do not belong to the
active tenant.
RLS is opt-in. You activate it by giving the gateway a second connection string for the restricted role:
# gateway.env
# Owner pool — runs migrations and privileged maintenance
AIHUMMER_DATABASE_URL=postgres://aihummer:***@localhost:5432/aihummer?sslmode=disable
# Restricted role — activates RLS for normal request traffic
AIHUMMER_DB_APP_URL=postgres://aihummer_app:***@localhost:5432/aihummer?sslmode=disable
[!NOTE] For host-native installs the installer sets
AIHUMMER_DB_APP_URLautomatically, so RLS is on by default in a standard deployment. If the variable is absent the gateway runs on the owner pool only and RLS is not active.
Per-tenant scoping
Within the restricted role, every request is scoped to its tenant: the gateway
sets the current tenant context on the connection so that RLS policies resolve
against the right workspace. WHERE tenant_id = … filters are not hand-rolled
everywhere; the policy enforces it centrally, which means a missed filter cannot
leak another tenant’s rows.
System / bypass mode
Some work is legitimately cross-tenant — background workers, schedulers and maintenance jobs that operate across the whole instance. These run in a system / bypass mode so they can see what they need to. Bypass is reserved for trusted internal workers, not for request-handling paths.
[!WARNING] Migrations always run on the owner pool, never under the restricted role. The owner connection has the privileges to alter schema and apply RLS policies; the restricted role deliberately does not. Keep the two connection strings distinct and grant the
aihummer_approle only what it needs.
Idempotent side-effects
A turn can produce real-world side-effects: sending mail, posting a message back to a channel. If a turn is retried — because of a transient error, or because the gateway restarted mid-turn and recovery replays it — those side-effects must not happen twice. AiHummer guarantees this with two cooperating pieces.
A resume-stable ledger key
Each side-effect is recorded against a resume-stable ledger key: a key that is derived deterministically from the turn, so a replay of the same turn computes the same key rather than a fresh one. The ledger remembers which keys have already been actioned.
A side-effect barrier
Before an effect such as mail or channel-send is performed, it passes through a side-effect barrier that checks the ledger. If this key has already fired, the barrier short-circuits and the effect is skipped; if not, the effect runs and the key is committed.
side-effect requested
└─▶ compute resume-stable ledger key
└─▶ barrier: key already committed?
├─ yes ─▶ skip (no double-send)
└─ no ─▶ perform effect ─▶ commit key
The result is exactly-once external behaviour even though delivery is at-least-once internally. Retries are safe by construction, which is what lets turn recovery (see Delivery, outbox & recovery) replay an interrupted turn without a customer receiving the same email or the same reply twice.
[!TIP] This is why AiHummer can offer guaranteed delivery and automatic turn recovery without the usual risk of duplicate messages — idempotency is the foundation the delivery guarantees are built on.
Where to next
- The runtime that drives a turn: Gateway & turn engine.
- How replies are returned exactly once: Delivery, outbox & recovery.