Contact us
Guides

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.

Endpoints used