Multi-tenant provisioning
Onboard a customer through the API: a security group, a library folder, row-level data security, and users with the right roles — so each tenant sees only their own people, content and data.
This guide covers the recommended structure for serving many customers from one Yurbi instance, and the API calls that create it.
The shape is simple: one security group per tenant, one library folder scoped to that group, and a data security policy that constrains every query to the tenant's own rows. Users belong to exactly one tenant group, which is what keeps tenants invisible to one another.
Before you start
Two settings belong to the instance rather than to any tenant.
Turn on Tenant Mode. Under Settings → Server Settings → Application
Settings, enable Tenant Mode. It restricts Builder and Architect users to the
All Users group, so they cannot reach across tenants when saving or sharing.
Confirm it from the API with GetAppSettings,
which returns TENANT_MODE_ENABLED.
Keep shared folders on All Users only. Any folder everyone should see — shared templates, for example — carries the All Users group and nothing else. With Tenant Mode on, users can view that content but cannot save, edit or delete within it.
Give your provisioning account the access it needs. Only the built-in admin
account bypasses application and group permissions. If you provision with a
purpose-made service account, grant it a role on each application it will assign
to users. See
Permissions and the super-admin account.
Onboarding a tenant
1. Create the tenant's security group
curl -X POST "https://your-yurbi-server.com/api/Group/SaveSecurityGroup" \
-H "Content-Type: application/json" \
-d '{
"sessionToken": "YOUR_SESSION_TOKEN",
"group": {
"GroupId": null,
"GroupName": "Tenant A",
"GroupDescription": "All users for Tenant A",
"GroupStatus": 0, "AllUsers": [], "AllRoles": []
}
}'
The response carries the new GroupId. Keep it: the folder, the data tag and the
policy all reference it.
Never create a group that spans tenants. All Users is the only group tenants have in common. Because each user belongs to just their own tenant group, the people picker when scheduling a report, and the group picker when saving one, show that user only their own colleagues.
2. Create the tenant's library folder
Start from NewLibraryFolder, set fname, and
scope the folder by giving it a single permission entry pointing at the tenant
group:
{
"sessionToken": "YOUR_SESSION_TOKEN",
"isShared": true,
"currentUserId": "",
"libraryfolder": {
"id": 0,
"fname": "Tenant A",
"isParent": false, "isHidden": false, "InheritPermissions": false,
"Permissions": [
{ "LeftID": "", "RightID": "12",
"RelationshipType": "fld_grp", "RelationType": "fld_grp",
"PermissionTypeEnum": 2 }
]
}
}
RightID is the tenant's GroupId. A folder scoped this way is visible only to
members of that group — it will not appear in
GetAllLibraryTree for anyone else, including
other administrators.
Within their folder, a tenant's Builder can save reports and share them with colleagues, and nothing they do is visible to another tenant.
3. Create the data tag that carries the tenant's identity
A data tag holds a value that varies per group. Row-level security then compares a column against that value, so one report serves every tenant.
{
"sessionToken": "YOUR_SESSION_TOKEN",
"datatag": {
"ID": "", "Label": "tenantid", "TagGroup": "Tenancy",
"DataTypeEnum": 1, "isGroup": true, "isUser": false,
"isActive": true, "DefaultValue": "NONE", "Contacts": [], "Index": 0,
"SecurityGroups": [
{ "LeftID": "", "RightID": "12",
"RelationshipType": "tag_grp", "RelationType": "tag_grp",
"Option1": "TENANT-A",
"PermissionTypeEnum": 0 }
]
}
}
RightID is the tenant's group and Option1 is that tenant's value. Create the
tag once, then add a SecurityGroups entry per tenant as you onboard them.
DefaultValue applies to anyone with no assigned value. Setting it to something
that matches no rows means a misconfigured user sees nothing rather than
everything.
Alternative: profile tags. Instead of a data tag you can put the value
directly on the user, in Tag1 through Tag4, and reference it as /#tag1#/ in
the constraint. That suits per-user values, and it can be set at user creation
time in the same call — see step 5.
4. Create the data security policy
The policy applies a constraint to an application and report type, comparing a
column to the tag value. Build it from
NewPolicy so its collections are initialised, then
save it:
{
"sessionToken": "YOUR_SESSION_TOKEN",
"policy": {
"id": null,
"Name": "Tenant row-level security",
"Description": "Constrains every query to the caller's tenant",
"Constraints": [], "Groups": [], "Users": [], "isActive": false
},
"isDeepSave": true,
"groups": [], "users": []
}
In the constraint, compare the tenant column to the tag — /#tenantid#/ for a
data tag, or /#tag1#/ for a profile tag.
Assign the policy to All Users. Every account is then constrained, and the tag
decides what each one sees. Assigning to individual tenant groups also works, but
means remembering to attach each new group. Set isActive true once you have
verified the constraint.
Steps 1 to 4 are per tenant, and only step 3's group entry and step 1 repeat as you add more.
5. Create each user
One call sets identity, credentials, profile tags and application access:
{
"sessionToken": "YOUR_SESSION_TOKEN",
"withpin": true,
"user": {
"ID": null,
"LoginName": "jsmith",
"FirstName": "Jane", "LastName": "Smith",
"EmailAddress": "[email protected]",
"Company": "Tenant A",
"Pin": "GENERATED_PASSWORD",
"AuthType": "PIN", "twofa": "none",
"Tag1": "TENANT-A",
"timezone": -5, "timezonename": "Eastern Standard Time",
"SecurityGroups": [],
"UserApplications": [
{ "ApplicationID": "1001", "ApplicationName": "Sales",
"ApplicationRoleID": "6", "ApplicationRoleName": "Agent",
"ApplicationRoleType": "0", "applicationUserDataSourceID": "" }
]
}
}
UserApplications grants application access, and the role there decides what the
user can do with data:
| Role | ID | The user can |
|---|---|---|
| Agent | 6 | View and run content |
| Builder | 5 | Build reports and dashboards |
| Architect | 7 | Build applications and report types |
These roles consume licence seats.
6. Add the user to their tenant group
{
"sessionToken": "YOUR_SESSION_TOKEN",
"GroupId": "12",
"ContactId": "639234395265102835",
"RoleId": "3"
}
The group role decides what the user may do with content in the tenant's
folder: 2 View, 3 Modify, 4 Delete.
The two roles answer different questions. The application role is what a user can do with data; the group role is what they can do with the tenant's saved content.
7. Confirm the result
GetContactById returns the user with their
groups and application assignments, and
GetSecurityGroupById confirms
membership.
Creating a user for an existing tenant
Once a tenant exists, adding a person is two calls:
SaveContact -> create the user, set Tag1, assign applications
AddUser -> add to the tenant group with a library role
Sessions and end users
Yurbi maintains one session per user account. A new login ends that account's previous session, so give every end user their own Yurbi account rather than sharing one, and cache each user's token on your server instead of logging in per page view. See Authentication & sessions.
This is why provisioning a user per end user matters even when your application handles its own authentication: the Yurbi account is what carries the tenant's data constraint and the session behind an embed.
Offboarding
Remove in the reverse order of creation, since deletes take the whole object:
GetContactList -> DeleteContact (each user)
GetAllLibraryTree -> DelFolder (the tenant folder)
GetAllDataTags -> SaveDataTag (drop the tenant's SecurityGroups entry)
GetAllSecurityGroups -> DeleteSecurityGroup (the tenant group)
Deleting users first releases their licence seats.