Skip to main content
Sub-tenants let you nest tenants in a parent–child tree, up to 5 levels deep. A child automatically inherits its parent’s preferences, templates, vendors, branding, and properties — you only override what should differ at that level. Use it when you’d otherwise be copying the same setup across many tenants.

Usecases it solves

  • Build hierarchical admin preferences. Take a project management tool with Company → Project → Cycle. An admin turns email off at the Company and no user gets email in any Project or Cycle; turn it back on for one Cycle and only that Cycle sends email.
  • Users can set different preferences along the tenant tree. A user picks “don’t email me” at the Company and it holds for every Project and Cycle; turn email on for one Project and they get email for that Project and its Cycles, still opted out everywhere else.
  • Globally turn off a channel or category for a tenant. Turn off email at the company level to keep costs down, block SMS at a customer’s tenant when their plan doesn’t include it, or turn off a specific notification category at the project level so it stays off for every tenant below.
  • Inherit templates, vendors, and branding — override where you need to. A white-label agency can let each client brand have its own vendors (Twilio, SendGrid, FCM) and templates. A notification SaaS can give each customer a default template that their sites, projects, or downstream customers tweak.
  • Users can have a different profile per tenant. The same person can be a team member on one Project and the project manager on another, each with the role, channels, and properties that fit that tenant. If sub-tenants represent different apps of the same customer, each app tenant stores that user’s push token and channel identity for that app, while their name, email, and other shared properties come from the tenant above.

Key concepts

How it works

Create a sub-tenant by passing parent_id in the tenant upsert API — the tenant becomes a child of that parent. Skip parent_id (or send null) to create a root tenant.
cURL
parent_id is the only field you send. SuprSend maintains and returns two more fields on every tenant response to describe its place in the tree:
  • ancestors — the chain of parent IDs from the root down to the tenant’s immediate parent, root first. Does not include the tenant itself.
  • level — the tenant’s position in the tree. 0 = root, 4 = deepest allowed.

How inheritance resolves

At trigger time, SuprSend walks the ancestor chain to figure out which value applies. Almost everything resolves bottom up — the closest tenant to where the notification fired wins. The one exception is channel and category blocks set at the tenant level, which cascade the other way and can’t be undone.

How inheritance flows across a sub-tenant tree

Here’s how each field type resolves:

Tenant properties

Tenant’s custom and reserved properties (logo, colors, preference_page_url, social_links) are shallow merged from parent to child.
  • Inheritance is per key. Overriding support_email on a child still lets company_name, region, etc. inherit from above.
  • id and name are never inherited; each tenant has its own.
📘 Create / Update Tenants → | Update Tenant Properties →

Tenant category-level preferences

For each notification category, a tenant can either set default preferences that descendants override key by key, or hard-block the category — or specific channels within it — so no descendant can re-enable them. Update a category via the Update Tenant Category Preference endpoint:
cURL
Each key inherits independently — send only the ones you want to change: Here’s an example flow
📘 Update Tenant Category Preference → | Get Tenant Category Preference → | Tenant Preferences →

Tenant blocked channels

Set blocked_channels on the tenant itself (not scoped to any category) to globally turn a channel off for every user of that tenant, across every category — including root categories and ones marked cant_unsubscribe. Update it via the same Create / Update Tenants endpoint:
cURL
This is a locked cascade — a channel blocked at any ancestor stays blocked for every tenant below. Descendants can add more blocks but never remove an ancestor’s.
📘 Create / Update Tenants →

Per-tenant user preference

A user can set their own preferences on any tenant they belong to. Every user preference — category or channel — is always saved against a tenant; there is no workspace-wide user preference. At trigger time, walk up from where the notification fired; the first value the user has explicitly set wins. Update via the Update User Category Preference or Update User Channel Preference endpoint — the tenant_id query param scopes each write to a specific tenant and is required:
cURL
  • User Preference supports 2 granularitiescategory preferences (opt in/out per notification category, and per channel within a category) and channel preferences (restrict or allow a channel across every category the user is subscribed to).
  • Category preferences fall back to the tenant’s resolved default — if the user hasn’t set one anywhere in the chain, the tenant’s default for that category applies.
  • Channel preferences have no tenant default — set is_restricted: true on a channel to restrict, false to allow. If the user has never set it anywhere in the chain, the channel is treated as unrestricted. Mandatory channels on cant_unsubscribe categories still fire when restricted.
  • Channel preferences apply as a group — once a child sets its own channel_preferences, it replaces the parent’s list entirely; the parent’s restrictions on other channels no longer apply at that child.
  • Within a category, each key inherits independently — changing preference doesn’t detach digest_schedule or condition properties; each keeps inheriting from the closest ancestor until the user sets their own.
  • cant_unsubscribe at the triggered tenant beats a user’s opt-out — when the tenant’s resolved preference for a category is cant_unsubscribe at the triggered tenant, the notification still sends on that tenant’s effective mandatory_channels even if the user opted out anywhere in the ancestor chain. Only the triggered tenant’s mandatory channels apply — ancestors’ mandatory channel lists don’t stack.
Here’s an example flow:
📘 Update User Category Preference → | Update User Channel Preference → | Get User Full Preference → | User Preferences →

Tenant vendors

For each channel (email, SMS, push, WhatsApp, …), the closest tenant in the chain that configured a vendor wins. Vendors cascade through the full tenant tree today.
  • Per channel, per vendor — overriding the SMS vendor on a child still lets it inherit the parent’s email vendor.
  • Default tenant vendor as final fallback — if no tenant in the chain configures a vendor for a channel, the default tenant vendor for that channel applies.
The resolved vendor for a tenant + channel is cached for up to 5 minutes. Adding a new vendor or re-parenting a tenant mid-flight won’t interrupt notifications already in progress — the new vendor picks up on the next resolution after the cache expires.
📘 Tenant Vendors →

Tenant templates (coming soon)

For a workflow triggered at any tenant, SuprSend will walk the ancestor chain and pick the closest tenant that has a matching template variant — the same rule as vendors.
  • Per channel — a child that overrides only the email template keeps inheriting the parent’s SMS template.
  • Default tenant fallback — if no tenant in the chain has a variant for a channel, the default tenant’s template applies.
  • Locale fallback within each tenant — at every tenant along the chain, SuprSend will try the recipient’s locale from most specific to least specific (e.g. es-MXes → template default like en) before moving to the next tenant.
  • Tenant proximity beats locale specificity — a closer tenant’s en variant wins over a farther tenant’s exact-locale variant. The selector picks the closest tenant that has any matching variant for the channel, then applies locale fallback within it.
Today, a workflow triggered at any tenant uses that tenant’s template variant if one exists on that exact tenant; otherwise it falls back to the default tenant’s variant. It does not yet walk the ancestor chain.
📘 Template Variants → | Tenant Templates →

Per-tenant user profile (coming soon)

A user’s effective profile at trigger time will be their global profile merged with each subscribed tenant’s per-tenant profile, layered from root down. The lowest tenant in the chain wins on shared fields. Users won’t need a subscription at every level — skipped levels contribute nothing.
Today, a user’s effective profile at trigger time is their global profile merged with the per-tenant profile at the triggered tenant only — profiles on ancestor tenants do not layer in.
📘 User-Tenant Mapping → | Upsert User Tenant Profile → Trigger a workflow at sprint-42 and SuprSend takes logo and primary_color from acme, support_email from project-1, and the milestone property from sprint-42 itself. SMS stays blocked on every send because acme locks it at the root — no descendant can turn it back on.

Adding a sub-tenant

  1. Go to Tenants in the side nav and open the tenant you want to add a sub-tenant under.
  2. Switch to the Sub-tenants tab and click Add Sub-tenant.

Sub-tenants tab — empty state

  1. In the modal, enter a Tenant ID (unique across the workspace, ≤ 64 chars, characters [a-z0-9_.-]) and a Name. The parent is prefilled from the tenant you opened; if that tenant already has sub-tenants of its own, you can pick any tenant in its subtree as the parent instead. Click on Create Tenant.

Add Sub-tenant modal

The new sub-tenant starts inheriting branding, properties, vendors, and preferences from the parent immediately; Click on it to override the properties or preference for this sub-tenant.

Sub-tenants tab on the tenant details page

Once at least one sub-tenant exists in your workspace, the Add Tenant action on the Tenants listing page also exposes a Parent tenant picker — so you can create a new tenant anywhere in the hierarchy without opening its parent first.
Change the parent_id on an existing tenant to move it (and its entire subtree) somewhere else in the tree, or delink it from its parent so it becomes a root again. On the API it’s a single field — set parent_id to a new tenant to re-parent, or send null (or an empty string) to delink. On the dashboard the two flows are surfaced as separate actions.
On the Sub-tenants tab, click the three-dot menu next to a sub-tenant. You’ll see the options to Change Parent or Delink Parent.

Change Parent and Delink Parent actions on a sub-tenant row

Change Parent — pick a new parent from the selector. Any parent that would push the moved subtree past level 4 is disabled. Confirm to move the tenant (and every tenant below it) under the new parent. Inherited values are recomputed against the new chain.

Change Parent modal

Delink Parent — detaches the tenant from its current parent and promotes it to a root. Its subtree comes along; local values stay as they are; values and preferences that were being inherited from the parent chain stop applying.

Delink Parent modal

A re-parent that would push any tenant in the moved subtree past level 4 is rejected with a 400. So is any move that would create a loop (moving a tenant under one of its own descendants).

Delete a tenant

Deleting a tenant is permanent — its settings, preferences, and per-tenant profiles are gone, and creating a new tenant with the same ID doesn’t restore any previous settings. Notifications already in flight are not affected — they finish with the settings that were in effect when they were triggered. When the tenant has children, you decide what happens to them with children_action.
Open the tenant’s detail page and click the three-dot menu at the top-right corner, then choose Delete.

Delete option on a sub-tenant row

If the tenant has children, the confirmation modal gives you three options — reassign to a specific tenant, leave children as roots, or delete them all. If reassign is selected, you can choose the new parent from the dropdown.

Delete modal with children handling options

Once sub-tenants exist, the Tenants listing adds two columns to every row — Parent and Level — so you can see each tenant’s place in the tree at a glance. On each row, a small dot next to logo, brand colors, or social_links means that value is inherited from an ancestor; no dot means it’s set locally on this tenant.

Tenant listing page after sub-tenants are added

Use the filter icon at the top of the listing to narrow the view:
  • Ancestor — pick a tenant and the listing shows every tenant in its subtree, at any depth.
  • Level — restrict to specific depths (0 = roots, 4 = deepest).
Combine them to narrow further — e.g. ancestor acme + level 1 returns every project directly under acme.

Key behaviors and constraints

  • 5 levels max. Root = level 0, deepest allowed = level 4. Every create and re-parent is checked — moves that would go past the limit are rejected with a 400.
  • One parent per tenant. Every non-root tenant has exactly one parent_id. There’s no multi-parent or DAG model.
  • id is unique across the workspace. A tenant can’t share its id with its parent, sibling, or any other tenant. IDs are ≤ 64 chars and accept letters, digits, ., _, and -.
  • Live inheritance. Editing an ancestor’s field takes effect right away for every tenant below that hasn’t overridden it. There’s no snapshot at create time.
  • Properties are merged per inner key. Override social_links.website on a child and other inner keys (twitter, linkedin, …) keep inheriting from the parent.
  • Category preferences merge per setting. Within a category, preference, digest_schedule, and conditions each inherit independently — overriding one doesn’t mean that other settings will also stop inheriting.
  • Tenant Preference — blocked channels and categories. Tenant blocked_channels, per-category blocked_channels, and category enabled_for_tenant: false cascade to every descendant and can’t be reversed.
  • Re-parenting is safe. Local values on the moved tenant stay put; inherited values resolve from the new chain at the next send. Notifications in flight finish with the settings that were in effect at trigger time.
  • The default tenant is unchanged. Every workspace still ships with the default tenant. It behaves as a root tenant with no children until you give it any.

FAQ

5 levels total. Root is level 0; the deepest allowed level is 4. Creates and re-parents that would go past this are rejected with a 400 that names the tenant and level that busted the limit.
Every tenant below it that hasn’t overridden primary_color picks up the new value right away — the next workflow triggered at any of those tenants uses the new value. Tenants that set their own primary_color aren’t affected.
Yes. A tenant with parent_id: null is a root and behaves exactly like flat tenants do today. You can use sub-tenants for one part of your customer base and leave the rest flat.
Not in this phase. Only properties, brand fields, tenant-level blocked_channels, and the category-level block settings (enabled_for_tenant, visible_to_subscriber, per-category blocked_channels) cascade. Everything else applies to the triggered tenant only. Hierarchy support for the rest ships in a later phase — reach out to support@suprsend.com if you have a use case that needs it sooner.
Yes. The whole subtree moves with it. SuprSend checks that the moved subtree still fits within the 5-level limit under the new parent, and rejects the move with a 400 if it doesn’t. Local values on every tenant in the subtree stay as they are; inherited values resolve from the new chain at the next send.
Yes. Deletes are soft — the old record is marked deleted and no longer reachable by id. Creating a new tenant with the same id starts a brand-new record; it does not bring back the deleted tenant’s users, subscriptions, or history.
No. Send-time resolution is optimized and doesn’t add measurable latency, even at the maximum depth of 5 levels.
Inheritance is resolved live at send time, so a workflow triggered after the parent update should already reflect the new value. If it doesn’t, check that (a) the write on the parent actually returned 200, (b) you’re triggering at the right child (parent_id and ancestors match what you expect), and (c) the field isn’t overridden on the child — a local value always wins over an inherited one.
A blocked_channels block at any level flows down and can’t be undone. The descendant’s effective list at send time is the union across the chain — every ancestor’s blocks plus its own. If you were trying to unblock a channel by removing it from a child’s blocked_channels, that isn’t possible — an ancestor’s block always wins. Remove it at the level where it was originally set.
No. Every user preference — category or channel — is always saved against a tenant. Preference writes require a tenant_id query param; without one, the API rejects the request. If your workspace uses just the built-in default tenant, all writes land there and behave workspace-wide in practice, but there is no global-across-all-tenants preference on a user.If you need a workspace-wide default preference that individual tenants can then override per user, model your tenants as sub-tenants of the default tenant. For example, keep the Company at default, nest Projects under it, and nest Cycles under each Project. The user’s preference at default acts as the global default; a preference set on a Project (or a Cycle) wins for triggers scoped to that subtree.
Re-parent each customer tenant so it becomes a child of the default tenant. default then acts as the global layer, and each customer stays its own tenant where users can override.
  1. Re-parent every customer tenant to default (or create new ones with parent_id: "default"). Existing users, subscriptions, and history stay intact.
  2. Save the user’s global preference by writing to tenant_id=default.
  3. Save a customer-specific override by writing to tenant_id=<customer_tenant_id>.
  4. At trigger time, pass the customer’s tenant_id. SuprSend walks up the chain — the customer’s preference wins if the user set one there; otherwise the default preference applies; if neither is set, the customer tenant’s own default preference for the category applies.
Sub-tenants are available on the Enterprise plan and as an add-on for the Business plan, matching how multi-tenancy is available today.

Next steps

Tenant preferences

Set admin defaults and lock down channels or categories for an entire subtree.

Tenant templates

Use inherited brand values and custom properties in your template content with $tenant.* variables.

Trigger a tenant workflow

Pass the leaf tenant’s id at trigger time and let SuprSend resolve the merged profile.

Tenant API reference

Full request/response schemas for every tenant endpoint, including the new hierarchy fields.