LLM-friendly URL

Custom fields per tenant

Use custom fields per tenant when your customers share one data model, but each customer's table or query has its own extra columns. You build dashboards once on a base dataset. Each customer's embed tokens query that customer's own source, see the shared columns plus their own extra columns, and never see another customer's columns.

This guide walks through the full setup with the Core API: creating a tenant, routing it to its own source, creating an embed token, and using and editing the tenant's own fields.

How it works

  • A tenant represents one of your customers. Its identifier is the value you use as suborganization (or tenant ) in that customer's embed tokens.

  • The base dataset is the dataset your dashboards use. Its source columns without tenant assignments are the shared fields .

  • A tenant dataset source links a tenant and a base dataset to a connection and to the tenant's own table or SQL query. Queries of the tenant's embed tokens on the base dataset run on that table or query instead of the base dataset's own source.

  • The tenant's table or query must contain every shared field, with the same source_name , type and source_type . Its other columns are the tenant's own fields.

  • A column or formula assigned to a tenant is a tenant field . Only embed tokens of that tenant can see it. Tokens without a tenant see all fields.

The tenant's own fields go through two stages:

  1. Discovered : when an embed token of the tenant retrieves the base dataset with its columns, Luzmo reads the extra columns of the tenant's table or query and returns them with the other columns. They are not saved yet.

  2. Saved : the first time the tenant uses a discovered field in a data query, or in the expression of a derived column or formula, Luzmo saves it as a column of the base dataset, with the same id , assigned to the tenant.

⚠️

A chart that uses a tenant's own field only works for that tenant. For other tenants, its query fails, because their table or query has no such column. Build dashboards that several tenants use on shared fields only.

ℹ️

Connection overrides change the connection properties of a single embed token, and all tenants keep the same columns. A tenant dataset source is stored per tenant and dataset, and lets each tenant have its own columns.

Who can do what

A main-organization user is a user of your own Luzmo organization, such as you or your colleagues, as opposed to an embed user of one of your customers. Technically, it is an organization member who is not in a suborganization. API tokens of such users, and embed tokens created with suborganization set to null , are main-organization callers. Embed tokens of a tenant are not.

Operation Who
Create, retrieve, update and delete tenants Main-organization users
Create, retrieve, update and delete tenant dataset sources Main-organization users with modify rights on the dataset. Create and update also need Use rights on the connection.
Assign columns and formulas to tenants Main-organization users with modify rights on the dataset
Use the tenant's own fields in dashboards and queries Embed tokens of the tenant
Create, edit and delete the tenant's own fields Embed tokens of the tenant, depending on tenant_fields_rights

Tenant visibility and dataset access rights are two separate checks:

  • Tenant visibility decides which fields a caller can see. An embed token of a tenant sees the shared fields and its own tenant's fields, whatever its access rights are. A caller without a tenant sees all fields.

  • Access rights decide what the caller can do with the fields it sees. They come from the token's access and from the dataset access of the embed user and its groups.

When tenants and groups are created

  • Every embed token with a suborganization (or tenant ) links to the tenant with that identifier . If the tenant doesn't exist yet, Luzmo creates it. If you already create embed tokens with a suborganization , a tenant already exists for each suborganization you used.

  • Creating a tenant with the API does not create a group.

  • The first embed token for a username creates a group for its suborganization, if that group doesn't exist yet, and adds the user to it.

  • An embed token for an existing username with a different suborganization moves the user to the group of the new suborganization. The previous group is deleted when it has no users left.

  • Preview tokens don't create or change groups.

The suborganization group matters because dataset access given to the group applies on top of the embed token's access . If the group has "modify" access to the base dataset, the tenant's users can modify all its fields, whatever the token's rights and tenant_fields_rights are. Keep dataset access off the suborganization group if you use tenant_fields_rights .

Create a tenant explicitly when you want to set up its tenant dataset source before its first embed token, which is the flow in this guide.

Before you start

You need:

  • An API key and token of a main-organization user. Create them in your profile settings .

  • A base dataset on a database connection or a plugin, with modify rights. Create it with the dataprovider endpoint . Datasets without a connection, such as datasets created with the data endpoint, cannot have tenant dataset sources.

  • A connection that holds the tenant's table or query, on the same provider as the base dataset's connection, with Use rights. This can be the base dataset's own connection. See Retrieving IDs to find the connection id.

The examples use a burritos dataset:

Table Columns
Base dataset type_of_burrito , number_burritos_savoured
Tenant table type_of_burrito , number_burritos_savoured , salsa_level , delivery_minutes

type_of_burrito and number_burritos_savoured are the shared fields. salsa_level and delivery_minutes are the tenant's own fields.

Create a tenant

Create the tenant with the identifier you will use as suborganization in its embed tokens. The identifier is unique within your organization and cannot be changed later.

createTenant.js
Shell
Node
Java
.NET
Python
PHP
import Luzmo from '@luzmo/nodejs-sdk';
const client = new Luzmo({
  api_key: '<your Luzmo API key>',
  api_token: '<your Luzmo API token>',
  host: 'https://api.luzmo.com'
});

const response = await client.create('tenant',
  {
    identifier: "< a suborganization name >",
    name: {
      en: "< tenant name >"
    }
  }
);

This returns the tenant, including the tenant id you need in the next step:

Response
json
{
  "id": "< tenant id >",
  "identifier": "< a suborganization name >",
  "name": {
    "en": "< tenant name >"
  },
  "organization_id": "< your organization id >"
  // ...
}

Route the tenant to its own source

Create a tenant dataset source that links the tenant and the base dataset to the tenant's table. Use source_sheet for a table, or source_query for a SQL query. Set exactly one of them.

createTenantDatasetSource.js
Shell
Node
Java
.NET
Python
PHP
import Luzmo from '@luzmo/nodejs-sdk';
const client = new Luzmo({
  api_key: '<your Luzmo API key>',
  api_token: '<your Luzmo API token>',
  host: 'https://api.luzmo.com'
});

const response = await client.create('tenantdatasetsource',
  {
    tenant_id: "< tenant id >",
    securable_id: "< burritos dataset id >",
    account_id: "< connection id >",
    source_sheet: "< schema >.< tenant table >"
  }
);

Luzmo reads the schema of the tenant's table or query and checks it against the base dataset before it saves the tenant dataset source. For example, if the tenant table has no type_of_burrito column, the request fails with the error below. Error messages call the tenant's table or query the "tenant source", and the base dataset the "logical dataset".

Response
json
{
  "type": {
    "code": 400,
    "description": "Bad Request"
  },
  "message": "Shared field 'type_of_burrito' was not found in the tenant source."
}

A tenant can have one tenant dataset source per dataset. A second one for the same tenant and dataset fails with 409 . See Create TenantDatasetSource for all validation errors.

Creating a tenant dataset source does not create any columns.

Generate an Embed token for the tenant

Create an embed token with the tenant's identifier as suborganization . You can set tenant instead, or both: when both are set, they must be equal.

createEmbedToken.js
Shell
Node
Java
.NET
Python
PHP
import Luzmo from '@luzmo/nodejs-sdk';
const client = new Luzmo({
  api_key: '<your Luzmo API key>',
  api_token: '<your Luzmo API token>',
  host: 'https://api.luzmo.com'
});

const response = await client.create('authorization',
  {
    type: "embed",
    username: "< A unique and immutable identifier for your user >",
    name: "< user name >",
    email: "< user email >",
    suborganization: "< a suborganization name >",
    access: {
      datasets: [
        {
          id: "< burritos dataset id >",
          rights: "use"
        }
      ]
    }
  }
);

This returns the embed key and token, and the tenant the token is linked to:

Response
json
{
  "type": "embed",
  "id": "< the embed authorization key >",
  "token": "< the embed authorization token >",
  "suborganization": "< a suborganization name >",
  "tenant": "< a suborganization name >",
  "tenant_id": "< tenant id >"
  // ...
}

Use the embed key and token in your embedded dashboard as usual. The requests in the next sections are the requests that the embedded dashboard and editor make with the token.

Discover the tenant's fields

Retrieve the base dataset with its columns, using the embed key and token:

discoverTenantFields.js
Shell
Node
Java
.NET
Python
PHP
import Luzmo from '@luzmo/nodejs-sdk';
const client = new Luzmo({
  api_key: '< the embed authorization key >',
  api_token: '< the embed authorization token >',
  host: 'https://api.luzmo.com'
});

const response = await client.get('securable',
  {
    where: {
      id: "< burritos dataset id >"
    },
    attributes: [
      "id",
      "name"
    ],
    include: [
      {
        model: "Column",
        attributes: [
          "id",
          "name",
          "source_name",
          "type"
        ]
      }
    ]
  }
);

The columns contain the shared fields and the tenant's salsa_level and delivery_minutes columns:

Response
json
{
  "count": 1,
  "rows": [
    {
      "id": "< burritos dataset id >",
      "name": {
        "en": "Burritos"
      },
      "columns": [
        {
          "id": "< type of burrito column id >",
          "name": {
            "en": "type_of_burrito"
          },
          "source_name": "type_of_burrito",
          "type": "hierarchy"
        },
        {
          "id": "< number burritos savoured column id >",
          "name": {
            "en": "number_burritos_savoured"
          },
          "source_name": "number_burritos_savoured",
          "type": "numeric"
        },
        {
          "id": "< salsa level column id >",
          "name": {
            "en": "salsa_level"
          },
          "source_name": "salsa_level",
          "type": "hierarchy"
        },
        {
          "id": "< delivery minutes column id >",
          "name": {
            "en": "delivery_minutes"
          },
          "source_name": "delivery_minutes",
          "type": "numeric"
        }
      ]
    }
  ]
}

salsa_level and delivery_minutes are discovered fields:

  • Their id is deterministic: the same tenant, dataset and source column always give the same id , so you can store it.

  • They are only returned by a Column include without where , limit or offset . Search Column doesn't return them.

  • Updating or deleting them returns 404 until they are saved.

Main-organization callers and embed tokens of other tenants don't see them.

Use a tenant field

Query the dataset with a discovered field, using the embed key and token:

queryTenantField.js
Shell
Node
Java
.NET
Python
PHP
import Luzmo from '@luzmo/nodejs-sdk';
const client = new Luzmo({
  api_key: '< the embed authorization key >',
  api_token: '< the embed authorization token >',
  host: 'https://api.luzmo.com'
});

const response = await client.get('data',
  {
    queries: [
      {
        dimensions: [
          {
            column_id: "< salsa level column id >",
            dataset_id: "< burritos dataset id >"
          }
        ],
        measures: [
          {
            column_id: "< number burritos savoured column id >",
            dataset_id: "< burritos dataset id >",
            aggregation: {
              type: "sum"
            }
          }
        ]
      }
    ]
  }
);

The query runs on the tenant table. The first query that uses salsa_level saves it: from then on it is a regular column of the base dataset, with the same id , assigned to the tenant. Search Column returns it for the tenant and for main-organization callers.

Referencing a discovered field in the expression of a derived column or formula saves it as well. This requires use on the dataset through the token's rights .

Queries on a tenant dataset source don't use the base dataset's acceleration . They always run on the tenant's table or query.

Let the tenant edit its own fields

By default, rights applies to all columns of the dataset. To let a tenant edit its own fields without giving it edit access to the shared fields, set tenant_fields_rights on the dataset in the embed token:

createEmbedTokenTenantFieldsRights.js
Shell
Node
Java
.NET
Python
PHP
import Luzmo from '@luzmo/nodejs-sdk';
const client = new Luzmo({
  api_key: '<your Luzmo API key>',
  api_token: '<your Luzmo API token>',
  host: 'https://api.luzmo.com'
});

const response = await client.create('authorization',
  {
    type: "embed",
    username: "< A unique and immutable identifier for your user >",
    name: "< user name >",
    email: "< user email >",
    suborganization: "< a suborganization name >",
    role: "designer",
    access: {
      datasets: [
        {
          id: "< burritos dataset id >",
          rights: "use",
          tenant_fields_rights: "modify"
        }
      ]
    }
  }
);

With this token, the tenant can rename the saved salsa_level column:

renameTenantField.js
Shell
Node
Java
.NET
Python
PHP
import Luzmo from '@luzmo/nodejs-sdk';
const client = new Luzmo({
  api_key: '< the embed authorization key >',
  api_token: '< the embed authorization token >',
  host: 'https://api.luzmo.com'
});

const response = await client.update('column', '< salsa level column id >',
  {
    name: {
      en: "Salsa level"
    }
  }
);

The same update on the shared type_of_burrito column fails with 403 , because rights is use .

Rules for tenant_fields_rights :

  • It is optional and only allowed in access.datasets[] , with the same values as rights . null is not allowed.

  • It applies to the fields assigned to the token's own tenant. rights applies to all other columns.

  • When it is omitted, rights applies to all columns.

  • It has no effect for tokens without a tenant.

  • The API token that creates the embed token needs access to the dataset equal to or higher than the higher of rights and tenant_fields_rights .

  • Preview tokens only accept read and use .

  • It only narrows the token's access . Dataset access of the embed user or its groups applies to all fields: if the suborganization group has "modify" access to the dataset, the tenant can modify its own fields even with "tenant_fields_rights": "read" .

Levels needed for the tenant's own fields:

Operation Level
Create a derived column or formula (assigned to the tenant automatically) use
Update or delete a derived column or formula assigned only to the tenant use
Update or delete a saved column from the tenant's table or query, or a field also assigned to other tenants modify
Update or delete hierarchy levels, change the currency or timezone modify

A tenant cannot update, delete or associate shared formulas, whatever its rights. See "Access rights for Embed tokens" in Create Authorization for the full rules.

Assign existing columns and formulas to tenants

You can also turn existing columns and formulas of a dataset into tenant fields, for example a derived column that only one customer should see. Assigning a shared field to a tenant hides it from all other tenants. A field can be assigned to several tenants.

Send an assignments array to Associate Tenant to assign several fields in one request. All assignments are applied in one transaction: if one fails, none is applied.

ℹ️

The SDKs associate one resource per call. Use the HTTP request below to send an assignments array.

assignTenantFields.sh
shell
curl https://api.luzmo.com/0.1.0/tenant  -H "Content-Type: application/json" -d @- << EOF
{
  "action": "associate",
  "version": "0.1.0",
  "key": "<your Luzmo API key>",
  "token": "<your Luzmo API token>",
  "assignments": [
    {
      "tenant_id": "< tenant id >",
      "resource": {
        "role": "Columns",
        "id": "< column id >"
      }
    },
    {
      "tenant_id": "< tenant id >",
      "resource": {
        "role": "Formulas",
        "id": "< formula id >"
      }
    }
  ]
}
EOF

Use "action": "dissociate" with the same body to remove the assignments. A field without any remaining assignment becomes a shared field again. To assign a single field, use id and resource instead of assignments , or the Tenants role on Associate Column and Associate Formula .

If the base dataset has a tenant dataset source, a field you assign to a tenant is no longer a shared field, so the tables and queries of other tenants don't need to contain it.

Change or remove a tenant dataset source

Update a tenant dataset source to point it to another connection, table or query. tenant_id and securable_id cannot be changed. To switch between a table and a query, set the one you no longer use to null in the same request:

updateTenantDatasetSource.js
Shell
Node
Java
.NET
Python
PHP
import Luzmo from '@luzmo/nodejs-sdk';
const client = new Luzmo({
  api_key: '<your Luzmo API key>',
  api_token: '<your Luzmo API token>',
  host: 'https://api.luzmo.com'
});

const response = await client.update('tenantdatasetsource', '< tenant dataset source id >',
  {
    source_sheet: null,
    source_query: "SELECT type_of_burrito, number_burritos_savoured, delivery_minutes FROM < schema >.< tenant table >"
  }
);

The update runs the same validation as create. Saved columns of the tenant that are missing from the new table or query are deleted: in this example, salsa_level .

Deleting a tenant dataset source deletes all columns saved from it, and the tenant's queries use the base dataset's source again. Derived columns and formulas of the tenant are not deleted, even when their expression references a deleted column.

Deleting a tenant deletes its tenant dataset sources and the columns saved from them. Its other columns and formulas, including the derived columns and formulas it created, are not deleted: they lose their assignment to the tenant. A field without any remaining assignment becomes a shared field and is visible to every tenant.

Did this page help you?
Yes No