Multi-Tenancy with Contexts
Use Contexts to scope notifications by tenant, workspace, organization, environment, or feature area — without duplicating workflows or subscribers. The Inbox displays only notifications whose trigger context exactly matches the Inbox context.Contexts are a Novu primitive. If you’re new to them, start with the Contexts overview.
How context filtering works
The Inbox uses exact-match filtering. The full context object passed to the<Inbox> must equal (key for key, value for value) the context used at trigger time:
| Workflow Context | Inbox Context | Displayed? |
|---|---|---|
{ tenant: "acme" } | { tenant: "acme" } | ✅ |
{} | {} | ✅ |
{} | { tenant: "acme" } | ❌ |
{ tenant: "acme" } | { tenant: "globex" } | ❌ |
{ tenant: "acme" } | {} | ❌ |
{ tenant: "acme", app: "first" } | { tenant: "acme" } | ❌ |
contextHash).
End-to-end setup
1. Define the tenant context
A tenant context is a JSON object that identifies a tenant. Theid is the only required field; data is optional metadata.
@novu/api.
2. Trigger workflows with context
acme-corp tenant. They will only surface in an Inbox initialized with the same tenant context.
3. Filter the Inbox by tenant
4. Secure the context with contextHash
Because context is set on the client, a hostile user could swap the tenant ID to peek at another tenant’s notifications. To prevent that, generate an HMAC hash of a canonicalized context server-side.
Canonicalization is required because JSON objects with the same data but different key order would otherwise produce different hashes. Use a library that implements RFC-8259 canonicalization.
context and contextHash to the Inbox:
If you change thecontextyou must regenerate thecontextHash. The hash is bound to the exact, canonicalized JSON.
Patterns
Switching tenants client-side
contextHash per tenant on the server:
Per-feature scoping
Contexts aren’t limited to tenants — use them to scope by feature area, environment, or anything else:Combining tenant + app
Using context data in templates
Once a context is created, itsdata is exposed in every template editor (email, in-app, SMS, push) via the {{context}} helper. This lets you personalize content per tenant from a single workflow definition.
Example In-App body:
Operational notes
- Auto-create: contexts referenced for the first time are automatically created in Novu. Visible in the dashboard under Contexts.
- No auto-update: existing context data is never overwritten by a new trigger or Inbox initialization. To update tenant data, use the API.
- Context + HMAC: when HMAC is enforced for the project, you must also pass
contextHashifcontextis set. Subscribers will see no notifications otherwise. - Order-independent hashing: always canonicalize the JSON before hashing so
{ a: 1, b: 2 }and{ b: 2, a: 1 }produce the same hash. - Tenant switch ⇒ re-render: changing
contexttriggers a re-fetch and a new WebSocket subscription scope.
Common pitfalls
- Empty context vs no context —
context={{}}is treated as “no context”. Notifications triggered with a non-empty context will not appear, and vice versa. - Mismatched object shape —
{ tenant: "acme" }is not equal to{ tenant: { id: "acme" } }. Pick one shape and use it consistently in both trigger and Inbox. - Forgetting to regenerate the hash — any change to the context object requires a new
contextHash. A stale hash silently drops notifications. - Canonicalization mismatch — if your server canonicalizes but your test fixture doesn’t (or vice versa), hashes diverge. Always go through the same
canonicalizestep. - Storing secrets in
data— context data is read from the client. Don’t include API keys, tokens, or PII you wouldn’t otherwise expose.