Last updated 2026-05-28
Multi-Tenancy
notify carries a tenant_id on every persisted row and
every RPC. The model is deliberately lightweight: tenants are an
isolation key, not a billing identity or a feature gate.
When you'd care
- You run one notify deployment that serves multiple customer organizations and need to keep their data apart.
- You issue JWTs that already carry a
tenantclaim and want that to flow through to the inbox. - You're using the EntDB driver and want to map notify tenants onto EntDB tenant shards.
Where the tenant id comes from
Two paths, depending on the service:
- Producer surface (
NotificationInternalService) — the body'stenant_idfield. The internal-token check confirms the caller is trusted; the caller is expected to know which tenant it is acting on behalf of. - Client surface (
NotificationClientService) — the JWT'stenant(ortenant_id) claim. The request body has no tenant field at all on these RPCs.
How tenant scoping works in the store
Every Store method that reads or writes carries a tenantID
parameter, and every implementation honors it as a hard filter:
CreateNotification— the(TenantID, UserID, NotificationID)triple is the uniqueness key. Re-using a notification id under a different tenant produces a new row, not an idempotent hit.GetNotification— a row that exists under a different tenant returnsErrNotFound. The same goes for cross-user reads under the same tenant.QueryUserNotifications— scoped by(TenantID, UserID). The cursor and unread count are also scoped.UpsertDevice/ListDevices— same shape.
The conformance suite exercises this with a dedicated
UserIsolation subtest that creates three rows
((acme, u1, x), (acme, u2, x),
(beta, u1, x)) and asserts that
QueryUserNotifications for (acme, u1)
returns exactly the first.
One deployment, many tenants
Notify is happy to serve many tenants from one container — the tenant id is just data. There is no per-tenant configuration knob today, no per-tenant rate limiting, no per-tenant provider override. Provider configuration is global, and the same providers serve every tenant.
If you need per-tenant behaviour (e.g. different SMS sender numbers
per customer), the right shape today is library mode: wire one
Notifier per tenant with a tenant-specific
ProviderRegistry sharing a single underlying
Store. The container will grow this as a follow-up wave;
until then library mode owns the customization story.
EntDB driver and tenant shards
EntDB itself is tenant-sharded. The notify EntDB driver maps onto a
single EntDB tenant per process via NOTIFY_ENTDB_TENANT_ID:
-e NOTIFY_STORE_DRIVER=entdb \-e NOTIFY_ENTDB_ADDRESS=entdb:50051 \-e NOTIFY_ENTDB_TENANT_ID=notify-prod
Inside notify's logical model, you can still serve many tenants over
that one EntDB shard — they all live under the same EntDB tenant,
differentiated by the tenant_id field on each row. The
EntDB tenant is the storage shard; the notify tenant is the
application-level isolation key.
Operationally large customers may want a dedicated EntDB shard per
customer. The shape is to run a notify container per shard,
pointing each at its dedicated NOTIFY_ENTDB_TENANT_ID.
Postgres driver and tenant scoping
The Postgres schema carries tenant_id as a column on
both tables. Every WHERE clause includes it; every UNIQUE constraint
starts with it. There is no Postgres-level row security or schema
fan-out — the application-level filter is the contract.
-- Composite uniqueness keyed on tenant.CREATE UNIQUE INDEX notify_notifications_tenant_user_nid_uq ON notify_notifications (tenant_id, user_id, notification_id);
-- Paging index orders within a tenant + user.CREATE INDEX notify_notifications_user_created ON notify_notifications (tenant_id, user_id, created_at_ms DESC, id DESC);Cross-tenant operations are intentionally absent
Why this design. There is no "fanout to all tenants" RPC, no "list every tenant" admin endpoint, no global query. The tenant boundary is a hard wall because the auth boundary is — the JWT pins the recipient to one tenant, and the producer surface is per-call tenant-scoped. Cross-tenant operations would either need an admin role concept the platform doesn't model (yet) or a producer with super-user reach that defeats the isolation promise. Neither pays off enough at v0.1 to be worth the footgun.
Related
- Auth Model — how the JWT supplies the tenant
- EntDB driver — schema-aware mode + tenant shards
- Postgres driver — schema and tenant scoping