docs/platform/concepts/tenants.mdx
Multi-tenancy is a common requirement for applications that serve multiple organizations, workspaces, or customers from a single Novu project. Tenants are also commonly called workspaces or organizations.
Typical multi-tenancy goals include:
Novu supports multi-tenancy through contexts. Use a tenant context to define tenant boundaries instead of duplicating subscribers, prefixing subscriber IDs, or maintaining separate workflows per tenant.
Multi-tenancy in Novu is built on top of contexts. A tenant context identifies which organization a notification belongs to.
When you trigger a workflow with a tenant context:
When you initialize the Inbox with the same tenant context, the subscriber sees only notifications for that tenant.
```ts
const tenantContext = {
tenant: {
id: "acme-corp",
data: {
name: "Acme Corporation",
plan: "enterprise",
logo: "https://cdn.acme.com/logo.png",
},
},
};
```
Contexts are auto-created on first use. Manage them from the **Contexts** section in the Novu dashboard or through the [Contexts API](/api-reference/contexts/context-schema).
<Tabs>
<Tab title="Node.js">
import { Novu } from "@novu/api";
const novu = new Novu({ secretKey: "<YOUR_SECRET_KEY_HERE>" });
await novu.trigger({
workflowId: "invoice-paid",
to: { subscriberId: "user-123" },
payload: { amount: "$250" },
context: {
tenant: {
id: "acme-corp",
data: {
name: "Acme Corporation",
plan: "enterprise",
},
},
},
});
</Tab>
<Tab title="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="invoice-paid",
to={"subscriber_id": "user-123"},
payload={"amount": "$250"},
context={
"tenant": {
"id": "acme-corp",
"data": {
"name": "Acme Corporation",
"plan": "enterprise",
},
},
},
))
</Tab>
<Tab title="Go">
import (
"context"
"os"
novugo "github.com/novuhq/novu-go"
"github.com/novuhq/novu-go/models/components"
)
s := novugo.New(novugo.WithSecurity(os.Getenv("NOVU_SECRET_KEY")))
s.Trigger(context.Background(), components.TriggerEventRequestDto{
WorkflowID: "invoice-paid",
To: components.CreateToSubscriberPayloadDto(components.SubscriberPayloadDto{
SubscriberID: "user-123",
}),
Payload: map[string]any{"amount": "$250"},
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">
use novu;
use novu\Models\Components;
$sdk = novu\Novu::builder()
->setSecurity('<YOUR_SECRET_KEY_HERE>')
->build();
$sdk->trigger(
triggerEventRequestDto: new Components\TriggerEventRequestDto(
workflowId: 'invoice-paid',
to: new Components\SubscriberPayloadDto(subscriberId: 'user-123'),
payload: ['amount' => '$250'],
context: [
'tenant' => [
'id' => 'acme-corp',
'data' => [
'name' => 'Acme Corporation',
'plan' => 'enterprise',
],
],
],
),
);
</Tab>
<Tab title=".NET">
using Novu;
using Novu.Models.Components;
using System.Collections.Generic;
var sdk = new NovuSDK(secretKey: "<YOUR_SECRET_KEY_HERE>");
await sdk.TriggerAsync(triggerEventRequestDto: new TriggerEventRequestDto() {
WorkflowId = "invoice-paid",
To = To.CreateSubscriberPayloadDto(new SubscriberPayloadDto() {
SubscriberId = "user-123",
}),
Payload = new Dictionary<string, object>() {
{ "amount", "$250" },
},
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">
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("invoice-paid")
.to(To2.of(SubscriberPayloadDto.builder()
.subscriberId("user-123")
.build()))
.payload(Map.of("amount", "$250"))
.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();
</Tab>
<Tab title="cURL">
curl -L -g -X POST 'https://api.novu.co/v1/events/trigger' \
-H 'Content-Type: application/json' \
-H 'Accept: application/json' \
-H 'Authorization: ApiKey <NOVU_SECRET_KEY>' \
-d '{
"name": "invoice-paid",
"to": { "subscriberId": "user-123" },
"payload": { "amount": "$250" },
"context": {
"tenant": {
"id": "acme-corp",
"data": {
"name": "Acme Corporation",
"plan": "enterprise"
}
}
}
}'
</Tab>
</Tabs>
```tsx
import { Inbox } from "@novu/react";
function TenantInbox({ userId, tenant }) {
return (
<Inbox
applicationIdentifier="YOUR_APPLICATION_IDENTIFIER"
subscriberId={userId}
context={{
tenant: {
id: tenant.id,
data: tenant.data,
},
}}
/>
);
}
```
When a subscriber switches tenants, re-render the Inbox with the new context. Novu refetches notifications and reconnects the WebSocket scope.
<Note>
Secure tenant context in production with `contextHash`. See [Inbox with context](/platform/inbox/configuration/inbox-with-context).
</Note>
```liquid
Hello {{subscriber.firstName}}, your {{context.tenant.data.plan}} plan
on {{context.tenant.data.name}} just renewed.
```
You can also branch workflow logic on tenant data in [step conditions](/platform/workflow/add-and-configure-steps/step-conditions). See [Contexts in workflows](/platform/workflow/advanced-features/contexts/contexts-in-workflows).
The Inbox uses exact-match context filtering. The tenant context on <Inbox /> must match the context used at trigger time.
| Workflow context | Inbox context | Displayed? |
|---|---|---|
{ tenant: { id: "acme-corp" } } | { tenant: { id: "acme-corp" } } | Yes |
{ tenant: { id: "acme-corp" } } | { tenant: { id: "globex" } } | No |
{ tenant: { id: "acme-corp" } } | {} | No |
Each subscriber gets a separate Inbox feed and preference configuration per tenant context.