Skip to Content
TutorialsBuild a Multi-Tenant App with Scopes

Build a Multi-Tenant App with Scopes

This tutorial builds a working multi-tenant setup: a Tenant → Department → Team hierarchy, an orders collection that is automatically filtered per tenant, and sample records that prove the isolation. See Scopes for the reference of every field and option used here.

Concepts

ConceptDescription
Scope TypeA level of the hierarchy — e.g. Tenant, Department, Team
Scope ItemA concrete instance of a level — e.g. Acme Corp, Sales, North Region
URI pathThe auto-generated, immutable address of a scope item (/typeId:itemId/...)
Collection ConfigMarks a collection as scope-enabled and sets how it is filtered
X-Resource-UriRequest header carrying the active scope; data is filtered to it

The goal: an order created in Acme → Sales → North Region should be invisible to Globex, while an Acme manager looking at the Sales department sees every order beneath it.

Define the scope types

Open /scopes in the DaaS Studio, go to the Scope Types tab, and create three types in order — each one’s parent is the level above:

NameParent Type
Tenant— (top-level)
DepartmentTenant
TeamDepartment

A type’s parent is immutable after creation. Build the hierarchy top-down so the URI paths come out right.

Create the scope items

Switch to the Scope Items tab and build the tree. Pick the scope type, then the parent item (required once the type has a parent):

Acme Corp (Tenant) ├─ Sales (Department) │ ├─ North Region (Team) │ └─ South Region (Team) └─ Engineering (Department) └─ Platform (Team) Globex (Tenant) └─ Operations (Department)

Each item receives an immutable uri_path like /<tenantTypeId>:<acmeId>/<deptTypeId>:<salesId>/<teamTypeId>:<northId>.

Create the collection to scope

Create a normal collection — for this tutorial, orders with these fields:

FieldTypeNotes
customer_namestringrequired
totalinteger
statusstringpending / shipped / delivered
resource_uristringthe scope field — set it readonly; the scope system fills it in

Enable scope filtering

Back on /scopes, open the Collection Config tab and click Add Config:

  • Collection: orders
  • Field Name: resource_uri
  • Missing URI Mode: strict (a request with no scope is treated as the root scope)
  • Inheritance Mode: down (a scope sees its own records and all descendants’)

Saving this also wires a foreign key from orders.resource_uri to the scope tree and indexes it.

Choose down when parent scopes should roll up their children’s data (a manager view). Choose exact when each scope must only ever see its own rows.

Create records in different scopes

Send the active scope on each write via the X-Resource-Uri header. The scope system injects that URI into resource_uri automatically — any client-supplied value is ignored.

# Create an order in Acme → Sales → North Region curl -X POST https://your-domain.com/api/items/orders \ -H "Authorization: Bearer <token>" \ -H "Content-Type: application/json" \ -H "X-Resource-Uri: /<tenantTypeId>:<acmeId>/<deptTypeId>:<salesId>/<teamTypeId>:<northId>" \ -d '{"customer_name":"Acme North — Widget order","total":1500,"status":"pending"}' # Create an order in Globex → Operations curl -X POST https://your-domain.com/api/items/orders \ -H "Authorization: Bearer <token>" \ -H "Content-Type: application/json" \ -H "X-Resource-Uri: /<tenantTypeId>:<globexId>/<deptTypeId>:<opsId>" \ -d '{"customer_name":"Globex Ops — Supplies","total":450,"status":"pending"}'

Seed a couple of orders under North Region, one under South Region, and one under Globex → Operations.

Verify the isolation

Read orders with different scopes and watch the results change.

# At the Sales department scope (down inheritance) → # returns BOTH North Region and South Region orders, but no Globex curl https://your-domain.com/api/items/orders \ -H "Authorization: Bearer <token>" \ -H "X-Resource-Uri: /<tenantTypeId>:<acmeId>/<deptTypeId>:<salesId>" # At the Globex → Operations scope → returns only the Globex order curl https://your-domain.com/api/items/orders \ -H "Authorization: Bearer <token>" \ -H "X-Resource-Uri: /<tenantTypeId>:<globexId>/<deptTypeId>:<opsId>"

The Acme Sales request rolls up every order beneath Sales; the Globex request never sees Acme data. That is tenant isolation with zero filtering logic in your queries.

How filtering is applied

Read at scopeinheritance_mode: down returnsinheritance_mode: exact returns
Acme → Sales → North RegionNorth Region ordersNorth Region orders
Acme → Sales (department)North + South Region ordersonly orders tagged exactly at Sales
Acme Corp (tenant)every Acme orderonly orders tagged exactly at Acme
Globex → OperationsGlobex Operations ordersGlobex Operations orders

Next Steps

  • Give users access per tenant in the follow-on tutorial, Scope-Aware Roles & Access Control — scoped role assignment, role inheritance, reject mode, and a scope switcher.
  • Assign roles at a scope so a user only operates within their tenant — see Roles & Policies. Users inherit role assignments from ancestor scopes.
  • Set Missing URI Mode to reject on sensitive collections to block any request that arrives without a scope, instead of treating it as root.
  • Scope additional collections in the follow-on tutorial, Scope Additional & Related Collections — custom field names, per-collection inheritance, and nested scope propagation. Each collection chooses its own field_name and inheritance mode.
  • Build a scope switcher in the follow-on tutorial, Build a Scope Switcher — fetch GET /api/scope/available, set the daas_resource_uri cookie, and filter the whole app to the chosen scope.
Last updated on