docs/platform/inbox/advanced-features/multi-tenancy.mdx
Multi-tenancy in Novu lets you isolate notifications for different organizations, environments, or workspaces within the same Novu project. Instead of creating separate subscribers or workflows for each tenant, you can use contexts to define and manage tenant boundaries.
<Note> This guide assumes you already understand what contexts are. If not, start with [Contexts](/platform/concepts/contexts). </Note>Multi-tenancy in Novu is built on top of Contexts, which act as lightweight tags that group notifications by logical boundaries such as tenants, workspaces, or environments.
When a notification is triggered with a tenant context, Novu automatically associates all notifications and preferences with that tenant. The same tenant context passed to the <Inbox /> filters notifications for that specific organization, ensuring users only see messages relevant to their workspace.
Implementing a multi-tenant system involves a few key steps:
A Tenant Context is a JSON object that identifies a specific tenant and can store any related data you need, such as company name, logo, or plan type.
You can create a tenant context using the Novu dashboard, API or from the <Inbox />. if it doesn’t already exist, Novu automatically creates it.
Here are ways you can pass context:
// Simple tenant ID
context: {
tenant: 'acme-corp'
}
// Rich tenant object
context: {
tenant: {
id: 'acme-corp',
data: {
name: 'Acme Corporation',
logo: 'https://cdn.acme.com/logo.png'
}
}
}
You can also manage all tenant contexts centrally from the Novu dashboard or API.
<Note> To learn more about creating, updating, and deleting contexts, see [Manage contexts](/platform/workflow/advanced-features/contexts/manage-contexts). </Note>Once you’ve defined your tenant context, you can use it when triggering workflows. During workflow trigger, Novu first checks if that context already exists.
If not, Novu automatically creates it — but if it does exist, Novu reuses the existing record instead of updating it, to prevent accidental overwrites.
<Tabs> <Tab title="Node.js"> ```ts import { Novu } from "@novu/api"const novu = new Novu({ secretKey: "<YOUR_SECRET_KEY_HERE>" });
await novu.trigger({ workflowId: "workflowId", to: { subscriberId: "user-123" }, payload: { amount: "$250", plan: "Pro", }, context: { tenant: { id: "acme-corp", data: { name: "Acme Corporation", plan: "enterprise", }, }, }, });
</Tab>
<Tab title="Python">
```python
import os
import novu_py
from novu_py import Novu
with Novu(secret_key=os.getenv("NOVU_SECRET_KEY", "")) as novu:
novu.trigger(trigger_event_request_dto=novu_py.TriggerEventRequestDto(
workflow_id="workflowId",
to={"subscriber_id": "user-123"},
payload={"amount": "$250", "plan": "Pro"},
context={
"tenant": {
"id": "acme-corp",
"data": {
"name": "Acme Corporation",
"plan": "enterprise",
},
},
},
))
novugo "github.com/novuhq/novu-go"
"github.com/novuhq/novu-go/models/components"
)
s := novugo.New(novugo.WithSecurity(os.Getenv("NOVU_SECRET_KEY")))
res, err := s.Trigger(context.Background(), components.TriggerEventRequestDto{ WorkflowID: "workflowId", To: components.CreateToSubscriberPayloadDto(components.SubscriberPayloadDto{ SubscriberID: "user-123", }), Payload: map[string]any{ "amount": "$250", "plan": "Pro", }, Context: map[string]any{ "tenant": map[string]any{ "id": "acme-corp", "data": map[string]any{ "name": "Acme Corporation", "plan": "enterprise", }, }, }, }, nil)
</Tab>
<Tab title="PHP">
```php
use novu;
use novu\Models\Components;
$sdk = novu\Novu::builder()
->setSecurity('<YOUR_SECRET_KEY_HERE>')
->build();
$sdk->trigger(
triggerEventRequestDto: new Components\TriggerEventRequestDto(
workflowId: 'workflowId',
to: new Components\SubscriberPayloadDto(subscriberId: 'user-123'),
payload: [
'amount' => '$250',
'plan' => 'Pro',
],
context: [
'tenant' => [
'id' => 'acme-corp',
'data' => [
'name' => 'Acme Corporation',
'plan' => 'enterprise',
],
],
],
),
);
var sdk = new NovuSDK(secretKey: "<YOUR_SECRET_KEY_HERE>");
await sdk.TriggerAsync(triggerEventRequestDto: new TriggerEventRequestDto() { WorkflowId = "workflowId", To = To.CreateSubscriberPayloadDto(new SubscriberPayloadDto() { SubscriberId = "user-123", }), Payload = new Dictionary<string, object>() { { "amount", "$250" }, { "plan", "Pro" }, }, Context = new Dictionary<string, object>() { { "tenant", new Dictionary<string, object>() { { "id", "acme-corp" }, { "data", new Dictionary<string, object>() { { "name", "Acme Corporation" }, { "plan", "enterprise" }, } }, } }, }, });
</Tab>
<Tab title="Java">
```java
import co.novu.Novu;
import co.novu.models.components.*;
import java.util.Map;
Novu novu = Novu.builder()
.secretKey("<YOUR_SECRET_KEY_HERE>")
.build();
novu.trigger()
.body(TriggerEventRequestDto.builder()
.workflowId("workflowId")
.to(To2.of(SubscriberPayloadDto.builder()
.subscriberId("user-123")
.build()))
.payload(Map.ofEntries(
Map.entry("amount", "$250"),
Map.entry("plan", "Pro")))
.context(Map.ofEntries(
Map.entry("tenant", TriggerEventRequestDtoContextUnion.of(
TriggerEventRequestDtoContext2.builder()
.id("acme-corp")
.data(Map.ofEntries(
Map.entry("name", "Acme Corporation"),
Map.entry("plan", "enterprise")))
.build()))))
.build())
.call();
In this example:
Once your workflows are sending tenant-scoped notifications, you can filter the <Inbox /> so that each subscriber only sees notifications relevant to their tenant.
This is done by passing the same tenant context used in your workflow triggers to the <Inbox />.
import { Inbox } from '@novu/react';
<Inbox
applicationIdentifier="APPLICATION_IDENTIFIER"
subscriber="SUBSCRIBER_ID"
context={{
tenant: {
id: 'acme-corp',
data: {
name: 'Acme Corporation',
plan: 'enterprise',
},
},
}}
/>
In this setup:
<Inbox /> filters notifications based on an exact context match.{ id: 'acme-corp' } will appear in the <Inbox />.With contexts, you can use tenant-level data to dynamically personalize notification content, branding, and workflow logic, all from a single workflow definition.
This eliminates the need to duplicate workflows or templates for each tenant while still keeping the experience distinct and relevant.
Once a tenant context is created, its data becomes accessible in all template editors (email, in-app, SMS, and push) through the {{context}} helper.
You can also use tenant context data to control conditional logic inside your workflows. For example, you may want to send certain updates only to enterprise tenants.
<Note> To learn more about customizing notification content with context, refer to the [Contexts in Workflows](/platform/workflow/advanced-features/contexts/contexts-in-workflows) documentation. </Note>