> ## Documentation Index
> Fetch the complete documentation index at: https://basedash.com/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Multi-tenant embedding

> Give each of your customers their own embedded Basedash organization, provisioned and kept in sync through the public API

> [!NOTE]
>
> Available on the [Enterprise plan](https://basedash.com/docs/administration/pricing.md). Embedding and billing accounts are Enterprise features.

This recipe is for software companies that want to embed Basedash in their product for many customers (tenants) at once, where:

* Every tenant gets its own Basedash organization, so their chats, dashboards, insights, and members stay separate.
* All tenants share the same schema in one warehouse or database, with a column such as `tenant_id` saying which rows belong to whom.
* You provision organizations and maintain a standard set of dashboards and models ("templates") from your own code, using the [public API](https://basedash.com/docs/api-reference/overview.md).

> **[Give this recipe to your AI agent](https://basedash.com/docs/recipes/agents/multi-tenant-embedding.md)**
>
> The agent brief is a Markdown spec your coding agent can follow to scaffold
> the provisioning and sync scripts. Point it at
> `https://www.basedash.com/docs/recipes/agents/multi-tenant-embedding.md`.

## How it fits together

```mermaid theme={"dark"}
flowchart LR
    subgraph You["Your backend"]
        P["Provisioning script"]
        T["Templates (in your repo)"]
        S["Tenant → org mapping"]
        J["JWT signer"]
    end
    subgraph Basedash
        BA["Billing account"]
        O1["Org: Tenant A"]
        O2["Org: Tenant B"]
    end
    W[("Shared warehouse")]

    P -- "create / update orgs" --> O1 & O2
    O1 & O2 -. "billed through" .-> BA
    T -- "apply models, dashboards, charts" --> O1 & O2
    P --> S
    J -- "SSO into the tenant's org" --> O1 & O2
    O1 -- "Tenant A credentials" --> W
    O2 -- "Tenant B credentials" --> W
```

You own three things: a provisioning script that creates and updates organizations, a set of templates checked into your repo, and a mapping from your tenant IDs to Basedash organization IDs. Basedash holds one organization per tenant, all attached to a single billing account.

## Decide how tenant data is isolated

This is the most important decision in the recipe. With [full app embedding](https://basedash.com/docs/developers/embedding.md#full-app-embedding), your tenants' users and the Basedash AI write their own SQL, so a filter in a dashboard is not a security boundary. **Isolation has to be enforced by the database**, through the credentials each organization connects with.

How you do that depends on where your data lives. Pick your data source; the rest of this page follows your choice.

**Redshift**

The `basedash.groups` session variable that powers Basedash [row level security](https://basedash.com/docs/administration/row-level-security.md) is only set for Postgres, so on Redshift each tenant connects with its own database user.

For each tenant, create a database user and a schema of views that only return that tenant's rows. Give the user access to that schema and nothing else, so Basedash only ever sees the tenant's own data, both when running queries and when reading the schema.

```sql theme={"dark"}
-- Run once per tenant, as a Redshift admin
CREATE USER bd_tenant_acme PASSWORD 'generated-strong-password';
CREATE SCHEMA tenant_acme;

CREATE VIEW tenant_acme.orders AS
  SELECT * FROM analytics.orders WHERE tenant_id = 'acme';
CREATE VIEW tenant_acme.customers AS
  SELECT * FROM analytics.customers WHERE tenant_id = 'acme';

GRANT USAGE ON SCHEMA tenant_acme TO bd_tenant_acme;
GRANT SELECT ON ALL TABLES IN SCHEMA tenant_acme TO bd_tenant_acme;
-- No grants on the analytics schema itself.
```

> [!WARNING]
>
> Use regular views, not late-binding views (`WITH NO SCHEMA BINDING`). A
> late-binding view checks permissions on the underlying tables as the
> person querying it, so the tenant user would need access to every
> tenant's rows.

Since every tenant's schema has the same tables and columns, your templates can use a placeholder such as `{{schema}}.orders` that the provisioning script replaces with `tenant_acme` before sending the SQL.

If you prefer to keep a single schema, Redshift's own [row-level security policies](https://docs.aws.amazon.com/redshift/latest/dg/t_rls.html) can restrict each tenant user to its rows. Then template SQL needs no placeholder. Test the policies with each tenant user before relying on them.

Snowflake, BigQuery, and other warehouses follow the same pattern: one user, role, or service account per tenant that can only read that tenant's rows.

**Postgres**

On Postgres and Supabase, every organization can connect with the same read-only credentials. Basedash sets the `basedash.groups` session variable to the querying user's group names on every query, and your [row level security](https://basedash.com/docs/administration/row-level-security.md) policies compare it to `tenant_id`.

```sql theme={"dark"}
ALTER TABLE orders ENABLE ROW LEVEL SECURITY;

CREATE POLICY orders_tenant_isolation ON orders
  FOR SELECT
  USING (
    current_setting('basedash.groups', true) IS NULL
    OR tenant_id = ANY (
      string_to_array(current_setting('basedash.groups', true), ',')
    )
  );
```

Each organization then needs a group named after the tenant ID, and every user in it needs to belong to that group. The steps below set this up.

Template SQL is identical for every tenant.

> [!TIP]
>
> If you only embed dashboards, not the full app, viewers can't write SQL. A
> shared dashboard with a tenant filter locked by [secure
> filtering](https://basedash.com/docs/developers/embedding.md#secure-filtering) is simpler and works with
> any data source.

## Before you start

* **A service account.** Create a Basedash user for automation, such as `basedash-admin@yourcompany.com`, and an [API key](https://basedash.com/docs/api-reference/overview.md#creating-an-api-key) for it. An API key acts as its user, and that user becomes an admin of every organization it creates, so keep it separate from a person's account.
* **A billing account.** Ask Basedash support to create one and attach it to your main organization. Organizations created with its `billingAccountId` are billed through it, with no trial. The service account must be an admin of the account's owner organization.
* **A home for the mapping.** A table in your own database with your tenant ID, the Basedash organization ID, and the IDs of the data source, dashboards, charts, and models created from templates.
* **Your app's origins**, such as `https://app.yourcompany.com`, for the embed allow list.

## Provision a tenant

**1. Create the organization**

```bash theme={"dark"}
curl -X POST "https://charts.basedash.com/api/public/organizations?include=jwtSecret" \
  -H "Authorization: Bearer $BASEDASH_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Acme Coffee",
    "slug": "yourcompany-acme",
    "billingAccountId": "bacct_...",
    "skipOnboarding": true,
    "fullEmbedEnabled": true,
    "embedAllowedOrigins": ["https://app.yourcompany.com"],
    "defaultDashboardAccessAudience": "EVERYONE",
    "defaultDashboardAccessLevel": "VIEW",
    "insightsFrequency": "WEEKLY"
  }'
```

Save the returned `id` and `jwtSecret` with the tenant. Each organization has its own JWT secret, which you can read again later with `GET /organizations/{id}?include=jwtSecret`.

Use a slug derived from your tenant ID. If the request fails partway, a retry that gets `409 SlugAlreadyExists` tells you the organization already exists, so look it up instead of creating a second one.

**2. Connect the tenant's data source**

**Redshift**

Create the tenant's Redshift user and views first (see [above](#decide-how-tenant-data-is-isolated)), then create the data source with that user's credentials:

```bash theme={"dark"}
curl -X POST "https://charts.basedash.com/api/public/organizations/$ORG_ID/data-sources" \
  -H "Authorization: Bearer $BASEDASH_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "type": "direct",
    "dialect": "REDSHIFT",
    "displayName": "Analytics",
    "host": "your-cluster.abc123.us-east-1.redshift.amazonaws.com",
    "port": 5439,
    "databaseName": "analytics",
    "username": "bd_tenant_acme",
    "password": "...",
    "sslEnabled": true
  }'
```

**Postgres**

Create the data source with your shared read-only credentials:

```bash theme={"dark"}
curl -X POST "https://charts.basedash.com/api/public/organizations/$ORG_ID/data-sources" \
  -H "Authorization: Bearer $BASEDASH_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "type": "direct",
    "dialect": "POSTGRES",
    "displayName": "Analytics",
    "host": "db.yourcompany.com",
    "port": 5432,
    "databaseName": "analytics",
    "username": "basedash_readonly",
    "password": "...",
    "sslEnabled": true
  }'
```

Then create the tenant's group with `POST /organizations/{orgId}/groups`, named after the tenant ID, and add the API key's user to it with `POST /organizations/{orgId}/groups/{id}/members`. Insights and automations created through the API run with the creator's groups, so without this they return no rows.

`POST /organizations/{orgId}/data-sources/verify` takes the same connection fields and tests them without saving anything.

**3. Apply your templates**

Create models first, then dashboards, then the charts on each dashboard, all pointing at the new data source ID. Store each created ID in your mapping, keyed by the template's own stable key, so later syncs update the same objects instead of creating duplicates.

See [Keep tenants in sync](#keep-tenants-in-sync) for how to structure templates.

**4. Sign users in**

When a tenant's user opens analytics in your app, sign a JWT with **that tenant's** organization ID and JWT secret, and load it in the iframe or [React SDK](https://basedash.com/docs/developers/embedding-react-sdk.md):

**Redshift**

```javascript theme={"dark"}
const token = jwt.sign(
  {
    email: user.email,
    orgId: tenant.basedashOrgId,
    role: "MEMBER",
  },
  tenant.basedashJwtSecret,
  { expiresIn: "10m" },
);
```

**Postgres**

Include the tenant's group, so the user's queries pass your RLS policies:

```javascript theme={"dark"}
const token = jwt.sign(
  {
    email: user.email,
    orgId: tenant.basedashOrgId,
    role: "MEMBER",
    groups: [tenant.id],
  },
  tenant.basedashJwtSecret,
  { expiresIn: "10m" },
);
```

Basedash creates the member on first sign-in. See [Embedding](https://basedash.com/docs/developers/embedding.md#full-app-embedding) for the options that control which features are visible.

## Keep tenants in sync

The API doesn't copy dashboards between organizations, so your repo is the source of truth for templates. A simple layout:

```text theme={"dark"}
templates/
  models/orders.json
  dashboards/overview.json      # dashboard settings and its charts
```

Each template has a stable `key` (for example `dashboards/overview`) that never changes, plus the fields the API accepts. A sync job, run on deploy or on a schedule, does this for every tenant:

1. Look up the Basedash ID for each template key in your mapping.
2. If there is none, create the object and save the new ID.
3. If there is one, update it with `PATCH`. A `404` means someone deleted it, so create it again.
4. Delete objects whose template you removed.

Only change what templates own. Dashboards your tenants' users build themselves are never in the mapping, so the sync leaves them alone.

Roll out template changes to a few tenants first, check them, then the rest.

## Operate at scale

* **Rate limits.** API keys are rate limited. A `429` response includes a `Retry-After` header; wait that long and retry. Run tenants a few at a time rather than all at once.
* **Bulk changes.** For a one-off change across hundreds or thousands of organizations, such as a new setting or a shared AI instruction, [contact support](mailto:support@basedash.com). It's often faster for us to apply it in one go.
* **Insights.** Every organization generates [insights](https://basedash.com/docs/features/insights.md) on its own schedule. Set `insightsFrequency` deliberately (`NEVER` if you generate insights through the API), since thousands of daily runs add up in AI usage and warehouse load.
* **AI usage limits.** `aiUsageOverageLimitCents` caps each tenant's AI spend within the billing account's pooled credits.
* **Warehouse load.** Each organization runs its own queries and schema syncs. Size your warehouse for your tenant count, and route Basedash users to their own queue or pool (for example a Redshift WLM queue) so analytics can't starve your production workloads.
* **Offboarding.** `DELETE /organizations/{id}` removes the organization. On Redshift, drop the tenant's database user and views at the same time.

## Checklist

* [ ] Each tenant can read only its own rows. On Redshift, test with a query as the tenant's user; on Postgres, as a user in the tenant's group.
* [ ] Organizations are created with `billingAccountId`, so none end up on a trial.
* [ ] Embed JWTs are signed server-side with the tenant's own `orgId` and secret.
* [ ] `embedAllowedOrigins` is set on every organization.
* [ ] Your mapping stores every ID created from a template, and the sync is safe to run twice.
* [ ] Your scripts retry `429` responses after `Retry-After`.

## Related

> **[Embedding](https://basedash.com/docs/developers/embedding.md)**
>
> JWT claims, iframe options, and allowed origins.

> **[Row level security](https://basedash.com/docs/administration/row-level-security.md)**
>
> Postgres policies keyed on Basedash groups.

> **[API overview](https://basedash.com/docs/api-reference/overview.md)**
>
> Authentication and response formats.

> **[Agent brief](https://basedash.com/docs/recipes/agents/multi-tenant-embedding.md)**
>
> This recipe as a spec for your coding agent.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.