# Metabase Embedding - Complete Reference for AI agents > **This documentation is for Metabase 63 (latest).** > > Table of contents: https://metabase.com/docs/latest/llms.txt ## IMPORTANT: Verify SDK and Metabase Version Compatibility The SDK version MUST match the Metabase instance version. Mismatched versions can cause errors. When looking up documentation, ALWAYS check the Metabase version. **Step 1: Ask the user for their Metabase instance URL** Before proceeding, ask the user where their Metabase instance is located. Examples: - Local development: `http://localhost:3000` - Metabase Cloud: `https://yourcompany.metabaseapp.com` - Self-hosted: `https://metabase.yourcompany.com` **Step 2: Check if SDK is already installed (React SDK / Modular Embedding only)** Skip this step if not using the React SDK (`@metabase/embedding-sdk-react`). ```bash npm list @metabase/embedding-sdk-react ``` If installed, note the version (e.g., `0.58.0` means this is for Metabase 58). **Step 3: Query the Metabase instance version** Using the URL from Step 1: ```bash curl /api/session/properties | jq .version ``` This returns (no authentication required): ```json { "date": "2025-01-10", "tag": "v1.58.0", "hash": "8e44dd8" } ``` If `jq` is not installed, you can grep the version. Extract the major version: `58` from `v1.58.x` or `v0.58.x`. **Step 4: Ensure versions match** - If the versions mismatch, you MUST fetch the version-specific llms.txt documentation that matches the Metabase instance version: `https://metabase.com/docs/v0.{VERSION}/llms.txt` (e.g., `/docs/v0.58/llms.txt` for Metabase 58) - For React SDK, ask the user to install or update their SDK packages if they are mismatched: `npm install @metabase/embedding-sdk-react@{VERSION}-stable` (e.g., `@58-stable` for Metabase 58) **Do NOT guess versions or use versions from your training data. Always verify first.** ## Modular Embedding Deprecations and Gotchas Watch out for these deprecated props and gotchas for Metabase 57 onwards, for modular embedding. 1. `config` prop on MetabaseProvider no longer exist as it is replaced by `authConfig`. 2. `authProviderUri` field no longer exist. 3. `jwtProviderUri` is an optional field that only exists in v58+. This is used to make JWT auth faster by skipping the `GET /auth/sso` discovery request. This field is not required for the initial implementation. 4. Numeric IDs must be integers not strings, e.g. `dashboardId={1}`. When the ID is retrieved from the router as a string AND it is numeric, `parseInt` it before passing it to the SDK. 5. IDs can also be strings for entity IDs, so you should NOT parse all IDs as numbers if entity IDs are also to be expected. 6. `fetchRequestToken` is not needed by default in most implementations. This is only used to customize how the SDK fetches the request token. For example, if the `/sso/metabase` endpoint in the user's backend requires passing custom auth tokens or headers. 7. When using `fetchRequestToken`, you MUST return the token in the shape of `{jwt: ""}`. Example: `return {jwt: await response.json()}`. # AI agent resources for embedding If you use an AI coding agent, you can give the agent Metabase-specific context to help with embedding setup, upgrades, and migrations. ## Agent skills We've developed some [agent skills](https://github.com/metabase/agent-skills) to give your AI agents step-by-step playbooks for specific embedding tasks. ### Available skills | Skill | Description | | ------------------------------- | -------------------------------------------------------------------------------------------- | | [SDK version upgrade](https://skillsmp.com/skills/metabase-agent-skills-skills-metabase-modular-embedding-version-upgrade-skill-md) | Upgrade your modular embedding SDK, including changelog checks and breaking change handling. | | [Full app → modular embedding](https://skillsmp.com/skills/metabase-agent-skills-skills-metabase-full-app-to-modular-embedding-upgrade-skill-md) | Migrate from full app embedding to modular embedding. | | [Modular embedding → SDK (React)](https://skillsmp.com/skills/metabase-agent-skills-skills-metabase-modular-embedding-to-modular-embedding-sdk-upgrade-skill-md) | Migrate from script-based modular embedding to the React SDK. | | [Static → guest embeds](https://skillsmp.com/skills/metabase-agent-skills-skills-metabase-static-embedding-to-guest-embedding-upgrade-skill-md) | Migrate from static (signed) embeds to guest embeds. | | [SSO for embeds](https://skillsmp.com/skills/metabase-agent-skills-skills-metabase-embedding-sso-implementation-skill-md) | Set up SSO authentication for embedded Metabase.| Browse all skills on the [agent skills repo](https://github.com/metabase/agent-skills). ## llms.txt Agents can read the docs you're reading now (and many agents have already read our docs), but we also publish llms.txts so that you can give your AI agent: - [Summary of embedding docs, with links](/docs/llms.txt). - [The full embedding docs](/docs/llms-embedding-full.txt). If you're on a specific version (e.g., v0.58), you can use versioned llms.txt files scoped to that version's docs: - `https://www.metabase.com/docs/v0.58/llms.txt` - `https://www.metabase.com/docs/v0.58/llms-embedding-full.txt` ## Agents are not magic Always review and validate the changes made by agents. Check that your application builds, tests pass, and the embedding works as expected before committing anything. ## Further reading - [Embedding introduction](./introduction) - [Modular embedding SDK](./sdk/introduction) - [Upgrading the modular embedding SDK](./sdk/upgrade) - [Agent skills repo](https://github.com/metabase/agent-skills) --- # Customizing the appearance of modular embeds You can style your embedded [Metabase components](./components) with a **theme**. ![Embed share button](./images/embed-share-button.png) On OSS and Starter plans, embeds come with two theme presets: [light and dark](#dark-mode-and-light-mode). On Pro and Enterprise plans, you can: - [Save reusable themes](#embedding-themes), then pick one in the embed wizard. - Customize individual colors, backgrounds, fonts, and more in your embedding code. See [Advanced theming](#advanced-theming). ## Dark mode and light mode By default, components that you embed using the SDK will use a light mode theme. To use dark mode, set `theme.preset` to `"dark"` in the `defineMetabaseConfig()` function of your [embedding code snippet](./modular-embedding#add-the-embedding-script-to-your-app): ```js defineMetabaseConfig({ theme: { preset: "dark", // or "light" }, }); ``` ## Embedding themes ![Embedding themes](./images/themes.png) A theme is a named set of colors and fonts that the embed wizard can copy into each new embed's config. The embed doesn't reference the theme, the embed carries its own inlined copy of the values. Changing or deleting the theme won't affect existing embeds. ### Manage themes Go to **Admin settings > Embedding > Themes**. Metabase ships with light and dark themes that pick up any appearance settings you've set on your Metabase (though you can tinker with these default themes as well, including removing theme like you can any other custom theme). From the Themes tab, you can: - **Create a theme.** Click **+ New theme**, then set a name, colors, and font. - **Edit a theme.** Click a theme card to open the editor. Changes show up in the live preview. - **Choose what the preview renders.** Pick the dashboard or question used to preview the theme. - **Duplicate a theme.** Handy if you want to vary an existing theme. - **Delete a theme.** This deletion is eternal. ![Theme editor](./images/theme-editor.png) ### Apply a saved theme to a new embed When you create a new embed using the [embed wizard](./modular-embedding#3-customize-your-embed), the last customization step lets you pick: - **A saved theme** from your list of themes. - **Instance theme** to use Metabase's instance defaults. - **Custom** to set colors directly on this embed without saving them as a theme. ![Embed wizard appearance](./images/embed-wizard-appearance.png) The theme you pick is inlined into the generated `defineMetabaseConfig({ theme: ... })` snippet. Because each embed carries its _own_ copy of the values, editing or deleting the theme in Metabase won't change the themes in embeds that have already been generated. Those embeds keep the colors and fonts they were created with. To pick up theme changes in an existing embed, regenerate the snippet from the wizard, or edit the `theme` block in your code by hand. ### Reuse a saved theme in the SDK If you're using the [SDK](./sdk/introduction), pass a theme to `MetabaseProvider` with `defineMetabaseTheme`, which accepts the same theme shape that you configure in the Themes admin UI. You can copy a saved theme's values into your code: ```tsx import { MetabaseProvider, defineMetabaseTheme, } from "@metabase/embedding-sdk-react"; const theme = defineMetabaseTheme({ fontFamily: "Lato", colors: { brand: "#50e397", background: "#11123d", "text-primary": "#f9f9fc", }, }); export function App() { return ( {/* your app */} ); } ``` For the full set of available colors and component overrides, see [Theme options](#theme-options). ## Advanced theming On Pro/Enterprise plan, you can configure granular appearance options, like background colors, font sizes etc. See the [list of all theming options](#theme-options). ### Add an advanced theme to your embed ![Behavior and appearance](./images/behavior-and-appearance.png) If you have a [saved theme](#embedding-themes), you can pick it directly in the embed wizard. The rest of this section covers further customization on top of (or instead of) a saved theme. Some appearance options like brand, text, and background color are configurable in the [embed wizard](./modular-embedding#create-a-new-embed). For other appearance settings, use the `theme` parameter with `preset` in the `defineMetabaseConfig()` function in your [embedding code snippet](./modular-embedding#add-the-embedding-script-to-your-app). For example: ```js defineMetabaseConfig({ theme: { colors: { background: "#FFFFFF", "text-primary": "hsla(204, 66%, 8%, 0.84)", brand: "hsla(208, 72%, 60%, 1.00)", "background-hover": "rgb(236, 236, 236)", "background-disabled": "rgb(231, 231, 231)", "background-secondary": "rgb(233, 233, 233)", "background-light": "rgb(233, 233, 233)", "text-secondary": "rgba(9, 30, 44, 0.84)", "text-tertiary": "rgba(11, 37, 54, 0.84)", "brand-hover": "rgb(185, 216, 244)", "brand-hover-light": "rgb(238, 245, 252)", }, }, instanceUrl: "http://localhost:3000", }); ``` ### Theme options For advanced theme configuration options, you can configure the `theme` object in the object passed to the `defineMetabaseConfig` function, see [Add an advanced theme to your embed](#add-an-advanced-theme-to-your-embed). Here's the list of all options available for customization: ```json { // Specify a font to use from the set of fonts supported by Metabase. // You can set the font to "Custom" to use the custom font // configured in your Metabase instance. "fontFamily": "Lato", // Override the base font size for every component. // This does not usually need to be set, as the components // inherit the font size from the parent container, such as the body. "fontSize": "16px", // Override the base line height for every component. "lineHeight": 1.5, // Match your application's color scheme "colors": { // The primary color of your application "brand": "#9B5966", // Lighter variation of the brand color. Used for hover and accented elements. "brand-hover": "#DDECFA", // Lightest variation of the brand color. Used for hover and accented elements. "brand-hover-light": "#EEF6FC", // The color of text that is most prominent "text-primary": "#4C5773", // The color of text that is less prominent "text-secondary": "#696E7B", // The color of text that is least prominent "text-tertiary": "#949AAB", // Default background color "background": "#FFFFFF", // Light background color for some control backgrounds. // Defaults are derived from `background` (slightly darker in light mode, much lighter in dark mode). "background-light": "#F0F2F5", // Slightly muted background color. "background-secondary": "#EDF2F5", // Slightly darker background color used for hover and accented elements "background-hover": "#F9FBFC", // Muted background color used for disabled elements, such as disabled buttons and inputs. "background-disabled": "#F3F5F7", // Color used for borders "border": "#EEECEC", // Color used for filters context "filter": "#7172AD", // Color used for aggregations and breakouts context "summarize": "#88BF4D", // Color used to indicate successful actions and positive values/trends "positive": "#BADC58", // Color used to indicate dangerous actions and negative values/trends "negative": "#FF7979", /** Color used to outline elements in focus */ "focus": "#CAE1F7", /** Color used for popover shadows */ "shadow": "rgba(0,0,0,0.08)", // Overrides the chart colors. Supports up to 8 colors // Limitation: this does not affect charts with custom series color "charts": [ // can either be a hex code "#9B59B6", // or a color object. tint and shade represents lighter and darker variations // only base color is required, while tint and shade are optional { "base": "#E74C3C", "tint": "#EE6B56", "shade": "#CB4436" } ] }, "components": { // Dashboard "dashboard": { // Background color for all dashboards "backgroundColor": "#2F3640", // Border color of the dashboard grid, shown only when editing dashboards. // Defaults to `colors.border` "gridBorderColor": "#EEECEC", "card": { // Background color for all dashboard cards "backgroundColor": "#2D2D30", // Apply a border color instead of shadow for dashboard cards. // Unset by default. "border": "1px solid #EEECEC" } }, // Question "question": { // Background color for all questions "backgroundColor": "#2E353B", // Toolbar of the default interactive question layout "toolbar": { "backgroundColor": "#F3F5F7" } }, // Tooltips "tooltip": { // Tooltip text color. "textColor": "#FFFFFF", // Secondary text color shown in the tooltip, e.g. for tooltip headers and percentage changes. "secondaryTextColor": "#949AAB", // Tooltip background color. "backgroundColor": "#2E353B", // Tooltip background color for focused rows. "focusedBackgroundColor": "#0A0E10" }, // Data table "table": { "cell": { // Text color of cells, defaults to `text-primary` "textColor": "#4C5773", // Default background color of cells, defaults to `background` "backgroundColor": "#FFFFFF", // Font size of cell values, defaults to ~12.5px "fontSize": "12.5px" }, "idColumn": { // Text color of ID column, defaults to `brand` "textColor": "#9B5966", // Background color of ID column, defaults to a lighter shade of `brand` "backgroundColor": "#F5E9EB" } }, // Number chart "number": { // Value displayed on number charts. // This also applies to the primary value in trend charts. "value": { "fontSize": "24px", "lineHeight": "21px" } }, // Cartesian chart "cartesian": { // Padding around the cartesian charts. // Uses CSS's `padding` property format. "padding": "4px 8px" }, // Pivot table "pivotTable": { "cell": { // Font size of cell values, defaults to ~12px "fontSize": "12px" }, // Pivot row toggle to expand or collapse row "rowToggle": { "textColor": "#FFFFFF", "backgroundColor": "#95A5A6" } }, "collectionBrowser": { "breadcrumbs": { "expandButton": { "textColor": "#8118F4", "backgroundColor": "#767D7C", "hoverTextColor": "#CE8C8C", "hoverBackgroundColor": "#69264B" } } }, // Popover are used in components such as click actions in interactive questions. "popover": { // z-index of the popover. Defaults to 200. "zIndex": 200 } } } ``` ## Can't see a modal? Bump the z-index If your app renders its own modal, drawer, or backdrop at a high z-index, SDK-internal overlays (like a save dialog) may be hidden behind your overlay. The SDK defaults popovers to `zIndex: 200`, which may be lower than your app's overlay settings. All you need to do in this case is raise the SDK popover z-index above your overlay via `defineMetabaseTheme`: ```tsx import { MetabaseProvider, defineMetabaseTheme, } from "@metabase/embedding-sdk-react"; const theme = defineMetabaseTheme({ components: { //... Other theme settings popover: { // Set above your overlay's z-index. // For example, if your modal is at 1000, use 1001. zIndex: 1001, }, }, }); export function App() { return ( {/* your app */} ); } ``` ## Limitations - CSS variables aren't yet supported. If you'd like Metabase to support CSS variables, please upvote this [feature request](https://github.com/metabase/metabase/issues/59237). - Colors set in the visualization settings for a question will override theme colors. --- # Modular embedding - authentication For using modular embedding with SSO in production, you'll need to set up authentication. If you're developing locally, you can also set up authentication with [API keys](#authenticating-locally-with-api-keys). You can set up SSO with JWT or SAML. ## Setting up JWT SSO To set up JWT SSO, you'll need [a Metabase Pro or Enterprise license](/pricing/). Here's a high-level overview: 1. [Enable JWT SSO in your Metabase](#1-enable-jwt-sso-in-your-metabase) 2. [Add a new endpoint to your backend to handle authentication](#2-add-a-new-endpoint-to-your-backend-to-handle-authentication) 3. [Wire your frontend to your new endpoint](#3-wire-your-frontend-to-your-new-endpoint) ### 1. Enable JWT SSO in your Metabase 1. Configure JWT by going to **Admin** > **Settings** > **Authentication** and clicking on **JWT** 2. Enter the JWT Identity Provider URI, for example `http://localhost:9090/sso/metabase`. This is a new endpoint you will add in your backend to handle authentication. 3. Generate a key and copy it to your clipboard. ### 2. Add a new endpoint to your backend to handle authentication You'll need to add a library to your backend to sign your JSON Web Tokens. For Node.js, we recommend jsonwebtoken: ``` npm install jsonwebtoken --save ``` Next, set up an endpoint on your backend (e.g., `/sso/metabase`) that uses your Metabase JWT shared secret to generate a JWT for the authenticated user. **This endpoint must return a JSON object with a `jwt` property containing the signed JWT.** For example: `{ "jwt": "your-signed-jwt" }`. This example code for Node.js sets up an endpoint using Express: ```js ``` Example using Next.js App Router: ```typescript ``` Example using Next.js Pages Router: ```typescript ``` #### Handling full app and SDK embeds with the same endpoint If you have an existing backend endpoint configured for full app embedding and want to use the same endpoint for SDK embedding, you can differentiate between the requests by checking for the `response=json` query parameter that the SDK adds to its requests. - For SDK requests, you should return a JSON object with the JWT (`{ jwt: string }`). - For full app embedding requests, you would proceed with the redirect. Here's an example of an Express.js endpoint that handles both: ```typescript ``` ### 3. Wire your frontend to your new endpoint Update the config in your frontend code to point to your backend's authentication endpoint. ```js ``` (Optional) If you use headers instead of cookies to authenticate calls from your frontend to your backend, you'll need to use a [custom fetch function](#customizing-jwt-authentication). ## If your frontend and backend don't share a domain, you need to enable CORS You can add some middleware in your backend to handle cross-domain requests. ```js ``` ## Customizing JWT authentication You can customize how the SDK fetches the request token by specifying the `fetchRequestToken` function with the `defineMetabaseAuthConfig` function: ```typescript ``` The response should be in the form of `{ jwt: "{JWT_TOKEN}" }` ## Setting up SAML SSO To use SAML single sign-on with modular embedding, you'll need to set up SAML in both your Metabase and your Identity Provider (IdP). See the docs on [SAML-based authentication](../people-and-groups/authenticating-with-saml). Once SAML is configured in Metabase and your IdP, you can configure the SDK to use SAML by setting the `preferredAuthMethod` in your `MetabaseAuthConfig` to `"saml"`: ```typescript ``` Using SAML authentication with modular embedding will typically involve redirecting people to a popup with your Identity Provider's login page for authentication. After successful authentication, the person will be redirected back to the embedded content. Due to the nature of redirects and popups involved in the SAML flow, SAML authentication may not work seamlessly in all embedding contexts, particularly within iframes, depending on browser security policies and your IdP's configuration. We recommend testing auth flows in your target environments. Unlike JWT authentication, you won't be able to implement a custom `fetchRequestToken` function on your backend when pairing SAML with modular embedding. ## If both SAML and JWT are enabled, modular embedding will default to SAML You can override this default behavior to prefer the JWT authentication method by setting `preferredAuthMethod="jwt"` in your authentication config: ```typescript authConfig: { metabaseInstanceUrl: "...", preferredAuthMethod: "jwt", // other JWT config... } ``` ## Getting Metabase authentication status You can query the Metabase authentication status using the `useMetabaseAuthStatus` hook. This is useful if you want to completely hide Metabase components when the user is not authenticated. This hook can only be used within components wrapped by `MetabaseProvider`. ```jsx ``` ## Authenticating locally with API keys > Modular embedding only supports JWT authentication in production. Authentication with API keys is only supported for local development and evaluation purposes. For developing locally to try out modular embedding, you can authenticate using an API key. First, create an [API key](../people-and-groups/api-keys). Then you can then use the API key to authenticate with Metabase in your application. All you need to do is include your API key in the config object using the key: `apiKey`. ```typescript ``` ## Security warning: each end-user _must_ have their own Metabase account Each end-user _must_ have their own Metabase account. The problem with having end-users share a Metabase account is that, even if you filter data on the client side via modular embedding, all end-users will still have access to the session token, which they could use to access Metabase directly via the API to get data they're not supposed to see. If each end-user has their own Metabase account, however, you can configure permissions in Metabase and everyone will only have access to the data they should. In addition to this, we consider shared accounts to be unfair usage. Fair usage of modular embedding involves giving each end-user of the embedded analytics their own Metabase account. ## Upgrade guide for JWT SSO setups on SDK version 54 or below If you're upgrading from an SDK version 1.54.x or below and you're using JWT SSO, you'll need to make the following changes. **Frontend changes**: - [Remove `authProviderUri` from all `defineMetabaseAuthConfig` calls](#remove-authprovideruri-from-your-auth-config) - **If using custom `fetchRequestToken`:** [Update function signature and hardcode authentication endpoint URLs](#update-the-fetchrequesttoken-function-signature) **Backend changes**: - [Update backend endpoint to return `{ jwt: "token" }` JSON response for SDK requests](#update-your-jwt-endpoint-to-handle-sdk-requests). Additionally, if you have SAML set up, but you'd prefer to use JWT SSO, you'll need to set a [preferred authentication method](#if-both-saml-and-jwt-are-enabled-modular-embedding-will-default-to-saml). ### Remove `authProviderUri` from your auth config `defineMetabaseAuthConfig` no longer accepts an `authProviderUri` parameter, so you'll need to remove it. **Admin setting changes in Metabase**: In **Admin** > **Authentication** > **JWT SSO**, set the `JWT Identity Provider URI` to the URL of your JWT SSO endpoint, e.g., `http://localhost:9090/sso/metabase`. **Before:** ```jsx const authConfig = defineMetabaseAuthConfig({ metabaseInstanceUrl: "https://your-metabase.example.com", authProviderUri: "http://localhost:9090/sso/metabase", // Remove this line }); ``` **After:** ```jsx const authConfig = defineMetabaseAuthConfig({ metabaseInstanceUrl: "https://your-metabase.example.com", }); ``` The SDK now uses the JWT Identity Provider URI setting configured in your Metabase Admin (Admin > Settings > Authentication > JWT). ### Update the `fetchRequestToken` function signature The `fetchRequestToken` function no longer receives a URL parameter. You must now specify your authentication endpoint directly in the function. **Before:** ```jsx const authConfig = defineMetabaseAuthConfig({ fetchRequestToken: async (url) => { // Remove url parameter const response = await fetch(url, { method: "GET", headers: { Authorization: `Bearer ${yourToken}` }, }); return await response.json(); }, metabaseInstanceUrl: "http://localhost:3000", authProviderUri: "http://localhost:9090/sso/metabase", // Remove this line }); ``` **After:** ```jsx const authConfig = defineMetabaseAuthConfig({ fetchRequestToken: async () => { // No parameters const response = await fetch("http://localhost:9090/sso/metabase", { // Hardcode your endpoint URL method: "GET", headers: { Authorization: `Bearer ${yourToken}` }, }); return await response.json(); }, metabaseInstanceUrl: "http://localhost:3000", }); ``` ### Update your JWT endpoint to handle SDK requests Your JWT endpoint must now handle both SDK requests and full app embedding requests. The SDK adds a `response=json` query parameter to distinguish its requests. For SDK requests, return a JSON object with the JWT. For full app embedding, continue redirecting as before. If you were using a custom `fetchRequestToken`, you'll need to update the endpoint to detect `req.query.response === "json"` for SDK requests. ```jsx app.get("/sso/metabase", async (req, res) => { // SDK requests include 'response=json' query parameter const isSdkRequest = req.query.response === "json"; const user = getCurrentUser(req); const token = jwt.sign( { email: user.email, first_name: user.firstName, last_name: user.lastName, groups: [user.group], exp: Math.round(Date.now() / 1000) + 60 * 10, }, METABASE_JWT_SHARED_SECRET, ); if (isSdkRequest) { // For SDK requests, return JSON object with jwt property res.status(200).json({ jwt: token }); } else { // For full app embedding, redirect as before const ssoUrl = `${METABASE_INSTANCE_URL}/auth/sso?token=true&jwt=${token}`; res.redirect(ssoUrl); } }); ``` ## Embedding Metabase in a different domain This section applies only to **authenticated embeds**. Guest embeds work cross-domain without additional configuration. If you want to embed Metabase in another domain (say, if Metabase is hosted at `metabase.yourcompany.com`, but you want to embed Metabase at `yourcompany.github.io`), you'll need to [allow your domain in CORS](#allow-your-domain-in-cors). ### Allow your domain in CORS Go to **Admin** > **Embedding** > **Modular embedding** and add your embedding domain under **Cross-Origin Resource Sharing (CORS)** (such as `https://*.example.com`). ### Configure session cookies when testing locally When you use `useExistingUserSession: true` during development on a different domain, the browser must send the existing Metabase session cookie cross-origin into the iframe. To allow this, you'll need to set the session cookie's SameSite value to "none". You can set session cookie's SameSite value in **Admin** > **Embedding** > **Security** > **SameSite cookie setting**. SameSite values include: - **Lax** (default): Allows Metabase session cookies to be shared on the same domain. Used for production instances on the same domain. - **None (requires HTTPS)**: Use "None" when your app and Metabase are hosted on different domains. Incompatible with Safari and iOS-based browsers. - **Strict** (not recommended): Does not allow Metabase session cookies to be shared with embedded instances. Use this if you do not want to enable session sharing with embedding. You can also set the [`MB_SESSION_COOKIE_SAMESITE` environment variable](../configuring-metabase/environment-variables#mb_session_cookie_samesite). If you're using Safari, you'll need to [allow cross-site tracking](https://support.apple.com/en-tj/guide/safari/sfri40732/mac). Depending on the browser, you may also run into issues when viewing embedded items in private/incognito tabs. Learn more about [SameSite cookies](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie/SameSite). --- # Modular embedding components There are different components you can embed, each with various options. > While you can use component parameters to show or hide parts of the embedded component, these parameters are _not_ a substitute for [permissions](../permissions/start). Even if you hide stuff, people could still grab their token from the frontend and use it to query the Metabase API. This page covers what you can embed. For theming your embeds, see [Appearance](./appearance). > Depending on the framework you're using, you may need to stringify attributes before passing them to the embedded components. ## Dashboard To render a dashboard: ```html ``` ### Attributes For all modular embeds, you can also set a `locale` in your page-level configuration to [translate embedded content](./translations), including content from translation dictionaries. If you surround your attribute value with double quotes, make sure to use single quotes: ```html ``` If you surround your attribute value with double quotes, make sure to use single quotes: ```html ``` ## Question To render a question (chart): ```html ``` You can also use the question component to create new questions: - `question-id="new"` — opens the visual query builder. - `question-id="new-native"` — opens the SQL editor. For example, to embed the SQL editor: ```html ``` ### Attributes ## Browser Browser component is only available for authenticated modular embeds. It's unavailable for [Guest embeds](./guest-embedding). To render a collection browser so people can navigate a collection and open dashboards or questions: ```html ``` ### Attributes ## AI chat AI chat component is only available for authenticated modular embeds. It's unavailable for [Guest embeds](./guest-embedding). To render the AI chat interface: ```html ``` If you're using the SDK, you can use either the [`MetabotQuestion`](./sdk/ai-chat#example) component or the [`useMetabot`](./sdk/ai-chat#building-custom-ai-chat-uis-with-usemetabot) hook for a custom UI. ### Attributes ## Customizing loader and error components If you're using the [modular embedding SDK](./sdk/introduction), you can provide your own components for loading and error states by specifying `loaderComponent` and `errorComponent` as props to `MetabaseProvider`. ```tsx ``` ## Further reading - [Appearance](./appearance) - [Modular embedding SDK](./sdk/introduction). --- Attributes for the `` web component. Embeds a collection browser so people can navigate collections and open dashboards or questions. Only available for authenticated (SSO) modular embeds. ## Remarks Pro/Enterprise ## Properties | Property | Type | Description | | :------------------------------------------------------------------- | :------------------- | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `collection-entity-types` | `string`[] | An array of entity types to show in the collection browser: `collection`, `dashboard`, `question`, `model`.
---
Optional
Possible values: `"model"`, `"collection"`, `"dashboard"`, `"question"` | | `collection-page-size` | `number` | How many items to show per page in the collection browser.
---
Optional | | `collection-visible-columns` | `string`[] | An array of columns to show in the collection browser: `type`, `name`, `description`, `lastEditedBy`, `lastEditedAt`, `archive`.
---
Optional
Possible values: `"type"`, `"name"`, `"description"`, `"lastEditedBy"`, `"lastEditedAt"`, `"archive"` | | `data-picker-entity-types` | `string`[] | An array of entity types to show in the question's data picker: `model`, `table`.
---
Optional
Possible values: `"model"`, `"table"` | | `enable-entity-navigation` | `boolean` | Whether to enable internal entity navigation (links to dashboards/questions).
---
Optional
Default: `false` | | `initial-collection` | `string` \| `number` | Which collection to start from. Use a collection ID (e.g., `14`) to start in a specific collection, or `"root"` for the top-level "Our Analytics" collection. | | `read-only` | `boolean` | Whether the content manager is in read-only mode. When `true`, people can interact with items (filter, summarize, drill-through) but can't save. When `false`, they can create and edit items.
---
Optional
Default: `true` | | `with-new-dashboard` | `boolean` | Whether to show the "New dashboard" button. Only applies when `read-only` is `false`.
---
Optional
Default: `true` | | `with-new-question` | `boolean` | Whether to show the "New question" button.
---
Optional
Default: `true` | --- Attributes for the `` web component. Embeds a Metabase dashboard. Provide either `dashboard-id` (for SSO embeds) or `token` (for guest embeds), plus optional display configuration. ## Properties | Property | Type | Description | | :--------------------------------------------------------------- | :------------------- | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `auto-refresh-interval` | `number` | Auto-refresh interval in seconds. For example, `60` refreshes the dashboard every 60 seconds.
---
Optional
Available in Pro/Enterprise and Guest embed. | | `custom-context` | `string` | Optional custom context string passed through to the guest token endpoint.
---
Optional
Available in Guest embed. | | `dashboard-id` | `string` \| `number` | The ID of the dashboard to embed. Can be a regular ID or an [entity ID](/docs/latest/installation-and-operation/serialization#entity-ids-work-with-embedding). Only for SSO embeds — guest embeds set the ID with `token`. | | `drills` | `boolean` | Whether to enable drill-through on the dashboard.
---
Optional
Default: `true`
Available in Pro/Enterprise. | | `enable-entity-navigation` | `boolean` | Whether to enable internal entity navigation (links to dashboards/questions). Requires `drills` to be `true`
---
Optional
Default: `false`
Available in Pro/Enterprise. | | `hidden-parameters` | `string`[] | List of filter names to hide from the dashboard, e.g. `['productId']`.
---
Optional
Available in Pro/Enterprise. | | `initial-parameters` | `object` | Default values for dashboard filters, e.g. `{ 'productId': '42' }`.
---
Optional
Available in Pro/Enterprise and Guest embed. | | `parameters` | `object` | Controlled dashboard filters values, e.g. `{ 'productId': '42' }`. Setting this attribute supersedes `initial-parameters` as the seed and stays in sync with subsequent mutations. Pair with the `parameters-change` DOM event to track edits.
---
Optional
Available in Pro/Enterprise and Guest embed. | | `token` | `string` | The token for guest embeds. Set automatically by the guest embed flow.
---
Optional
Available in Guest embed. | | `with-downloads` | `boolean` | Whether to show the button to download the dashboard as PDF and download question results.
---
Optional
Default: `true` on OSS/Starter, `false` on Pro/Enterprise
Available in Guest embed. | | `with-subscriptions` | `boolean` | Whether to let people set up [dashboard subscriptions](/docs/latest/dashboards/subscriptions). Subscriptions sent from embedded dashboards exclude links to Metabase items.
---
Optional
Available in Pro/Enterprise. | | `with-title` | `boolean` | Whether to show the dashboard title in the embed.
---
Optional
Default: `true`
Available in Guest embed. | --- Attributes for the `` web component. Embeds the AI chat interface. Only available for authenticated (SSO) modular embeds. ## Remarks Pro/Enterprise ## Properties | Property | Type | Description | | :------------------------------------------------- | :------------------- | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `is-save-enabled` | `boolean` | Whether the save button is enabled.
---
Optional
Default: `false` | | `layout` | `string` | How should the browser position the visualization with respect to the chat interface. `auto` uses `stacked` on mobile and `sidebar` on larger screens.
---
Optional
Default: `"auto"`
Possible values: `"auto"`, `"sidebar"`, `"stacked"` | | `target-collection` | `string` \| `number` | The collection to save a question to.
---
Optional | --- Attributes for the `` web component. Embeds a Metabase question (chart). Provide either `question-id` (for SSO embeds) or `token` (for guest embeds), plus optional display configuration. Use `question-id="new"` to embed the query builder exploration interface. Use `question-id="new-native"` to embed the SQL editor interface. ## Properties | Property | Type | Description | | :----------------------------------------------------------- | :------------------- | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `custom-context` | `string` | Optional custom context string passed through to the guest token endpoint.
---
Optional
Available in Guest embed. | | `drills` | `boolean` | Whether to enable drill-through on the question.
---
Optional
Default: `true`
Available in Pro/Enterprise. | | `entity-types` | `string`[] | Which entity types to show in the question's data picker, e.g. `["model", "table"]`.
---
Optional
Possible values: `"model"`, `"table"`
Available in Pro/Enterprise and Guest embed. | | `hidden-parameters` | `string`[] | List of parameter names to hide from the question.
---
Optional
Available in Pro/Enterprise. | | `initial-sql-parameters` | `object` | Default values for SQL parameters, only applicable to native SQL questions, e.g. `{ "productId": "42" }`.
---
Optional
Available in Pro/Enterprise and Guest embed. | | `is-save-enabled` | `boolean` | Whether the save button is enabled.
---
Optional
Default: `false`
Available in Pro/Enterprise. | | `question-id` | `string` \| `number` | The ID of the question to embed. Can be a regular ID or an [entity ID](/docs/latest/installation-and-operation/serialization#entity-ids-work-with-embedding). Use `"new"` to embed the query builder, or `"new-native"` to embed the SQL editor. Only for SSO embeds — guest embeds use `token`. | | `sql-parameters` | `object` | Controlled SQL parameter values, e.g. `{ "productId": "42" }`. Setting this attribute supersedes `initial-sql-parameters` as the seed and stays in sync with subsequent mutations. Pair with the `sql-parameters-change` DOM event to track edits.
---
Optional
Available in Pro/Enterprise and Guest embed. | | `target-collection` | `string` \| `number` | The collection to save a question to. Values: regular ID, entity ID, `"personal"`, `"root"`.
---
Optional
Available in Pro/Enterprise. | | `token` | `string` | The token for guest embeds. Set automatically by the guest embed flow.
---
Optional
Available in Guest embed. | | `with-alerts` | `boolean` | Whether to show the alerts button.
---
Optional
Default: `false`
Available in Pro/Enterprise. | | `with-downloads` | `boolean` | Whether to show download buttons for question results.
---
Optional
Default: `true` on OSS/Starter, `false` on Pro/Enterprise
Available in Guest embed. | | `with-title` | `boolean` | Whether to show the question title in the embed.
---
Optional
Default: `true`
Available in Guest embed. | --- Types for modular embedding web-components. This is the source of truth for all attributes accepted by modular embedding components. ## Interfaces | Interface | Description | | :------------------------------------------------------------ | :------------------------------------------------------- | | [MetabaseBrowserAttributes](MetabaseBrowserAttributes) | Attributes for the `` web component. | | [MetabaseDashboardAttributes](MetabaseDashboardAttributes) | Attributes for the `` web component. | | [MetabaseMetabotAttributes](MetabaseMetabotAttributes) | Attributes for the `` web component. | | [MetabaseQuestionAttributes](MetabaseQuestionAttributes) | Attributes for the `` web component. | --- # Full app embedding quickstart > If you are just starting out with Metabase embedding, consider using [Modular embedding](./modular-embedding) - an improved, more customizable option for embedding Metabase components. You'll embed the full Metabase application in your app. Once logged in, people can view a Metabase dashboard in your web app, and be able to use the full Metabase application to explore their data, and only their data. ## Prerequisites - You have an app that you can embed Metabase in. - You have a Pro or Enterprise subscription of Metabase. If you're unsure where to start, sign up for a free trial for [Pro On-Prem](https://store.metabase.com/checkout/embedding). If you have Docker Desktop installed, you can just search for "metabase-enterprise" to find the Docker image and run it. Alternatively, you can follow [these instructions](../installation-and-operation/running-metabase-on-docker#pro-or-enterprise-quick-start). The code featured in this guide can be found in our [sample repo](https://github.com/metabase/metabase-nodejs-express-interactive-embedding-sample). ## Set up SSO and full app embedding in Metabase ### Have a dashboard ready to embed You'll first need a dashboard to embed. If you don't have one yet, you can use the Example Dashboard Metabase includes on new instances or you can generate one using x-rays. Visit that dashboard and make a note of its URL, e.g. `/dashboard/1-e-commerce-insights`. You'll need to put this relative URL in your app, as you'll use the dashboard as the first page that logged-in people will see when they visit the analytics section in your app. It's enough to include the ID only and omit the rest of the URL, e.g. `/dashboard/1`. You could also use the dashboard's [Entity ID](../installation-and-operation/serialization#metabase-uses-entity-ids-to-identify-metabase-items). On the dashboard, click on the **info** button. On the **Overview** tab, look for the dashboard's **Entity ID**. Copy that Entity ID. You'll use that Entity ID in the iframe's `src` URL: (e.g., `src=/dashboard/entity/[Entity ID]`). ### Enable full app embedding In Metabase, click the **grid** icon in the upper right and go to **Admin > Embedding** and toggle on **Enable full app embedding**. Under **Authorized origins**, add the URL of the website or web app where you want to embed Metabase. If you're running your app locally, you can add localhost and specify the port number, e.g. `http://localhost:8080`. #### SameSite configuration If you're embedding Metabase in a different domain, you may need to [set the session cookie's SameSite value to `none`](./full-app-embedding#embedding-metabase-in-a-different-domain). ### Set up SSO with JWT in your Metabase #### Enable authentication with JWT While still in the **Embedding settings** section, scroll down and click on **Authentication** under **Related settings**. On the card that says **JWT**, click the **Setup** button (you may have to scroll down to view the JWT card). ![Admin settings: Authentication > JWT setup.](./images/jwt-setup.png) #### Set JWT Identity provider URI In your app, you'll create a route for SSO at `/sso/metabase`. In the **JWT IDENTITY PROVIDER URI** field, enter the URL of your SSO route. For example, our sample app runs on port 8080, so in that case this JWT IDENTITY PROVIDER URI could be `http://localhost:8080/sso/metabase`. #### Generate a JWT signing key Click on the **Generate key** button to generate a signing key. Keep this key a secret. You'll use it on your server. If you generate another key, you'll overwrite the existing key, so you'll need to update the key in your app as well. Copy this key, as you'll need it in the next section. ### Save and enable JWT authentication We'll set up group synchronization later, but for now, be sure to click the **Save and enable** button to activate JWT authentication. ## Set up SSO with JWT in your app's server ### Add the signing key and Metabase site URL to your app Here you'll need to input some values for your SSO to work. You'll want to declare up to two constants in your app: - `METABASE_JWT_SHARED_SECRET`, paste the JWT signing key that you got from your Metabase here. - `METABASE_SITE_URL`, which points to your Metabase instance's root path. ```javascript const METABASE_JWT_SHARED_SECRET = "YOURSIGNINGKEY"; const METABASE_SITE_URL = "https://your-domain.metabaseapp.com"; ``` The signing key should preferably be setup as an environment variable, to avoid accidentally committing your key to your app's repo. ### Add a JWT library to your app's server Add a JWT library to your app. For example, if you're using a Node backend with JavaScript, we recommend using [jsonwebtoken](https://github.com/auth0/node-jsonwebtoken). In your terminal: ```sh npm install jsonwebtoken --save ``` And in your app, require the library: ```javascript ``` ### Restricting access to certain routes Presumably, your app already has some way of making sure some routes are only accessible after having signed in. Our examples use a simple helper function named `restrict` that protects these routes: ```javascript ``` ### Add a function to sign users We need to write a function to sign user JWTs, using the JWT library. ```javascript ``` ### Add a `sso/metabase` route You'll need to add a route to sign people in to your Metabase via SSO using JWT. If the person isn't signed in to your app yet, your app should redirect them through your sign-in flow. In the code below, this check and redirection is handled by the `restrict` function we introduced earlier. ```javascript ``` Metabase creates an account for first-time sign-ins. ### CHECKPOINT: sign in to your Metabase using SSO Make sure you are signed out of your Metabase. From the Metabase sign-in page, click on "Sign in with SSO". You should be redirected to your app. Log in to your app. Your app should redirect you to your Metabase welcome page. If the person doesn't yet have a Metabase account, Metabase should create an account for them. ## Embed Metabase in your app Now to embed your Metabase in your app. You'll want to set up a route to serve your embedded analytics. Let's call it `/analytics`. Note that we're using the `restrict` helper function (defined above) because this page should only be viewable after people sign in to your app. In this route, we need to render an iframe that will load your Metabase. The `src` attribute of the iframe should point to the relative path of the SSO endpoint of your app. Once the person signs in to your app (and therefore in to your Metabase), we add the query string parameter `return_to` so that the iframe displays the requested dashboard. `METABASE_DASHBOARD_PATH` should be pointing to the relative path of the dashboard you created at the beginning of this guide (`/dashboard/[ID]`, or if you used the dashboard's Entity ID: `/dashboard/entity/[Entity ID]`). ```javascript ``` The `METABASE_DASHBOARD_PATH` is just the first thing people will see when they log in, but you could set that path to any Metabase URL. And since you're embedding the full Metabase, people will be able to drill through the data and view other questions, dashboards, and collections. ### CHECKPOINT: view a Metabase dashboard in your app People using your app should now be able to access `/analytics` and view your embedded Metabase dashboard. How to test: Sign in to your app and visit the `/analytics` route. You should see the Metabase dashboard. > If you're using the Safari browser, and you're serving Metabase and your app from different domains, you may need to go to Safari's settings and turn off [Prevent cross-site tracking](https://support.apple.com/guide/safari/prevent-cross-site-tracking-sfri40732/mac). ## Set up a group in Metabase Now that you have SSO and full app embedding set up, it's time to set up groups so that you can apply permissions to your embedded Metabase entities (questions, dashboards, collections, and so on). ### Add a `groups` key to your token Recall the `signUserToken` function used to create the JWTs. Add a `groups` key to the signed token that maps to an array. Metabase will look at the values in that array to see if any of the values map to a group in Metabase (We'll walk through mapping groups in a bit). ```javascript ``` ### Create a group in Metabase In Metabase, click the **grid** icon and go to **Admin** > **People** > **Groups**. Click the **Create a group** button. Add a group that corresponds with a group in your app. If you're using the sample app, add a group called `Customer Acme`. ### Synchronize groups between Metabase and your app You'll map this string in the `groups` key to a Metabase group, so that when the person signs in via SSO, Metabase automatically assigns them to the appropriate Metabase group. In Metabase's admin section, go to **Authentication > JWT** and click **Edit**. In the **Group schema** section, toggle on **Synchronize group memberships**. If the names of groups in the `groups` array match Metabase group names exactly (e.g. both are `"Customer Acme"`), then the groups will be mapped automatically. If the JWT group names and Metabase group names don't match, then for each group you want to sync, add a group mapping. When you click **New mapping**, enter "Customer-Acme", the string that you included in the `groups` array in your JWT payload. You can then associate that group name with the Metabase group "Customer Acme" that we created earlier. ![Mapping user attributes to groups.](./images/sync-groups.png) Be sure to **Save changes**. ### CHECKPOINT: verify that Metabase assigns people to groups when they log in First, sign out of Metabase and sign in using SSO. Then sign out and sign in to your Metabase as an admin and go to **Admin** > **People** section and verify that Metabase added the person to the appropriate group. Note: only Metabase admins and group managers are aware of groups. Basic users have no concept of groups, and no way of knowing which groups they're a part of. ## Set permissions Now to apply permissions to that group so that people only see data specific to their accounts. ### Reset permissions for the All Users group Metabase ships with two initial groups: "Admins" and "All Users". By default, Metabase gives the "All Users" group access to connected data sources. And since Metabase grants people the privileges of their most permissive group, you'll want to restrict what the "All Users" groups can see before you add them to groups with limited or no access to data sources and collections. To reset permissions for the All users group, click the **grid** icon and go to **Admin** > **Permissions**. Under the **Data** tab, go to **Groups** and select **All Users**. For the **Sample Database** in the **View data** column, select "Blocked". Click **Save changes** and a modal will pop up summarizing what you're changing. Click **Yes**. ![Resetting permissions of the All Users group to](./images/all-users.png) ### Allow view access to the automatically generated dashboards collection Still in the **Permissions** tab, click on the **Collections** sub-tab, then on the **Automatically generated dashboards** collection, and set the **Collection access** permissions for the **All Users** group to **View**. Click **Save changes**, then **Yes**. ### Add a user attribute to the token You can include user attributes in the JSON web token. Metabase will pick up any keys from the JWT payload and store them as user attributes. Among other use cases, you can use these user attributes to set row-level permissions on tables, so people can only see results tied to their accounts. If you're using our sample app, edit the `signUserToken` function used to create the JWT by adding a key `account_id` with value `28`. ```javascript ``` That user ID will correspond to the `Account ID` column in the Sample Database's Invoices table. We'll use this `account_id` user attribute for row and column security on the Invoices table, so people will only see rows in that table that contain their account ID. Note that to persist the user attribute in Metabase, you'll need to log in. Log in to your app as a non-admin, and visit the page with your embedded Metabase. ### Set row-level permissions In Metabase, go to **Admin** > **Permissions**. Under the **Data** tab on the left, click on a group. For "Sample Database", change its **Data access** column to **Granular**. Metabase will display a list of the tables in the database. Next, change **View data access** for the "Invoices" table to **Row and column security**. ![Adding row and column security to a table.](./images/secured-invoices-table.png) Next, Metabase will prompt you with a modal to associate a column in that table with a user attribute. Leave the **Filter by a column in a table** option checked, and associate the "Account ID" column in the Invoices table with the user attribute `account_id`. (Note that Metabase will only display the user attributes if the user has signed in through SSO before.) Click **Save** to confirm your select. Then click the **Save changes** button in the upper right. Metabase will ask if you're sure you want to do this. You _are_ sure. ### CHECKPOINT: view secured dashboard Make sure you've logged out of your previous session. Log in to your app, navigate to `/analytics`. The dashboard will now present different information, since only a subset of the data is visible to this person. Click on **Browse Data** at the bottom of the left nav. View your secured **Invoices** table, and you should only see rows in that table that are associated with the person's account. ## Hiding Metabase elements You can decide to [show or hide various Metabase elements](./full-app-embedding#showing-or-hiding-metabase-ui-components), like whether to show the nav bar, search or the **+New** button, and so on. For example, to hide the logo and the top navigation bar of your embedded Metabase, you'd append the query string parameters `?logo=false&top_nav=false` to the `return_to` URL that you include in the SSO redirect. In the handler of your `/sso/metabase` path, add the query parameters: ```javascript ``` ### CHECKPOINT: verify hidden UI elements Sign out and sign in to your app again and navigate to `/analytics`. Your embedded Metabase should not include the logo or the top navigation. ## Next steps You can [customize how Metabase looks](../configuring-metabase/appearance) in your app: fonts, colors, and logos. --- # Full app embedding **Full app embedding** lets you embed the entire Metabase app in an iframe. Full app embedding integrates your [permissions](../permissions/introduction) and [SSO](../people-and-groups/start#authentication) to give people the right level of access to [query](../questions/query-builder/editor) and [drill-down](../questions/visualizations/drill-through) into your data. > If you are just starting out with Metabase embedding, consider using [Modular embedding](./modular-embedding) instead of full app embedding - it's an improved, more customizable option for embedding individual Metabase components. ## Full app embedding demo To get a feel for what you can do with full app embedding, check out our [Full app embedding demo](/embedding-demo). To see the query builder in action, click on **Reports** > **+ New** > **Question**. ## Quick start Check out the [Full app embedding quick start](./full-app-embedding-quick-start-guide). ## Prerequisites for full app embedding 1. Make sure you have a [license token](../installation-and-operation/activating-the-enterprise-edition) for a [Pro or Enterprise plan](https://store.metabase.com/checkout/login-details). 2. Organize people into Metabase [groups](../people-and-groups/start). 3. Set up [permissions](../permissions/introduction) for each group. 4. Set up [SSO](../people-and-groups/start#authentication) to automatically apply permissions and show people the right data upon sign-in. In general, **we recommend using [SSO with JWT](../people-and-groups/authenticating-with-jwt)**. If you're dealing with a [multi-tenant](/learn/metabase-basics/embedding/multi-tenant-self-service-analytics) situation, check out our recommendations for [Configuring permissions for different customer schemas](../permissions/embedding). If you have your app running locally, and you're using the Pro Cloud version, or hosting Metabase and your app in different domains, you'll need to set your Metabase environment's session cookie SameSite option to "none". ## Enabling full app embedding in Metabase 1. Go to **Admin > Embedding**. 2. Click **Enable Full app embedding**. 3. Under **Authorized origins**, add the URL of the website or web app where you want to embed Metabase (such as `https://*.example.com`). ## Setting up embedding on your website 1. Create an iframe with a `src` attribute set to: - the [URL](#pointing-an-iframe-to-a-metabase-url) of the Metabase page you want to embed, or - an [authentication endpoint](#pointing-an-iframe-to-an-authentication-endpoint) that redirects to your Metabase URL. 2. Optional: Depending on the way your web app is set up, set [environment variables](../configuring-metabase/environment-variables) to: - [Add your license token](../configuring-metabase/environment-variables#mb_premium_embedding_token). - [Embed Metabase in a different domain](#embedding-metabase-in-a-different-domain). - [Secure your embed](#securing-full-app-embeds). 3. Optional: Enable communication to and from the embedded Metabase using supported [`postMessage`](https://developer.mozilla.org/en-US/docs/Web/API/Window/postMessage) messages: - [From Metabase](#supported-postmessage-messages-from-embedded-metabase) - [To Metabase](#supported-postmessage-messages-to-embedded-metabase) 4. Optional: Set parameters to [show or hide Metabase UI components](#showing-or-hiding-metabase-ui-components). Once you're ready to roll out your full app embed, make sure that people **allow** browser cookies from Metabase, otherwise they won't be able to log in. ### Pointing an iframe to a Metabase URL Go to your Metabase and find the page that you want to embed. For example, to embed your Metabase home page, set the `src` attribute to your [site URL](../configuring-metabase/settings#site-url), such as: ``` src="https://metabase.yourcompany.com/" ``` To embed a specific Metabase dashboard, you'll want to use the dashboard's Entity ID URL `/dashboard/entity/[Entity ID]`. ``` src="https://metabase.yourcompany.com/dashboard/entity/[Entity ID]" ``` To get a dashboard's Entity ID, visit the dashboard and click on the **info** button. In the **Overview** tab, copy the **Entity ID**. Then set your iframe's `src` attribute to: ``` src=https://metabase.yourcompany.com/dashboard/entity/Dc_7X8N7zf4iDK9Ps1M3b ``` If your dashboard has more than one tab, select the tab you want people to land on and copy the Tab's ID. Add the tab's ID to the URL: ``` src=https://metabase.yourcompany.com/dashboard/entity/Dc_7X8N7zf4iDK9Ps1M3b?tab=YLNdEYtzuSMA0lqO7u3FD ``` You _can_ use a dashboard's sequential ID, but you should prefer the Entity ID, as Entity IDs are stable across different Metabase environments (e.g., if you're testing on a staging environment, the Entity IDs will remain the same when [exporting the data and importing it](../installation-and-operation/serialization) into a production environment). If you want to point to a question, collection, or model, visit the item, click on its info, grab the item's Entity ID and follow the url structure: `/[Item type]/entity/[Entity-Id]`. Examples: - `/collection/entity/[Entity ID]` - `/model/entity/[Entity ID]` - `/question/entity/[Entity ID]` ### Pointing an iframe to an authentication endpoint Use this option if you want to send people directly to your SSO login screen (i.e., skip over the Metabase login screen with an SSO button), and redirect to Metabase automatically upon authentication. You'll need to set the `src` attribute to your auth endpoint, with a `return_to` parameter pointing to the encoded Metabase URL. For example, to send people to your SSO login page and automatically redirect them to `https://metabase.yourcompany.com/dashboard/1`: ``` https://metabase.example.com/auth/sso?return_to=http%3A%2F%2Fmetabase.yourcompany.com%2Fdashboard%2F1 ``` If you're using [JWT](../people-and-groups/authenticating-with-jwt), you can use the relative path for the redirect (i.e., your Metabase URL without the [site URL](../configuring-metabase/settings#site-url)). For example, to send people to a Metabase page at `/dashboard/1`: ``` https://metabase.example.com/auth/sso?jwt=&return_to=%2Fdashboard%2F1 ``` You must URL encode (or double encode, depending on your web setup) all of the parameters in your redirect link, including parameters for filters (e.g., `filter=value`) and [UI settings](#showing-or-hiding-metabase-ui-components) (e.g., `top_nav=true`). For example, if you added two filter parameters to the JWT example shown above, your `src` link would become: ``` https://metabase.example.com/auth/sso?jwt=&redirect=%2Fdashboard%2F1%3Ffilter1%3Dvalue%26filter2%3Dvalue ``` ## Cross-browser compatibility To make sure that your embedded Metabase works in all browsers, put Metabase and the embedding app in the same top-level domain (TLD). The TLD is indicated by the last part of a web address, like `.com` or `.org`. Note that your full app embed must be compatible with Safari to run on _any_ browser in iOS (such as Chrome on iOS). ## Embedding Metabase in a different domain > Skip this section if your Metabase and embedding app are already in the same top-level domain (TLD). If you want to embed Metabase in another domain (say, if Metabase is hosted at `metabase.yourcompany.com`, but you want to embed Metabase at `yourcompany.github.io`), you can tell Metabase to set the session cookie's SameSite value to "none". You can set session cookie's SameSite value in **Admin** > **Embedding** > **Security** > **SameSite cookie setting**. SameSite values include: - **Lax** (default): Allows Metabase session cookies to be shared on the same domain. Used for production instances on the same domain. - **None (requires HTTPS)**: Use "None" when your app and Metabase are hosted on different domains. Incompatible with Safari and iOS-based browsers. - **Strict** (not recommended): Does not allow Metabase session cookies to be shared with embedded instances. Use this if you do not want to enable session sharing with embedding. You can also set the [`MB_SESSION_COOKIE_SAMESITE` environment variable](../configuring-metabase/environment-variables#mb_session_cookie_samesite). If you're using Safari, you'll need to [allow cross-site tracking](https://support.apple.com/en-tj/guide/safari/sfri40732/mac). Depending on the browser, you may also run into issues when viewing embedded items in private/incognito tabs. Learn more about [SameSite cookies](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie/SameSite). ## Securing full app embeds Metabase uses HTTP cookies to authenticate people and keep them signed into your embedded Metabase, even when someone closes their browser session. If you enjoy diagrammed auth flows, check out [Full app embedding with SSO](./securing-embeds). To limit the amount of time that a person stays logged in, set [`MAX_SESSION_AGE`](../configuring-metabase/environment-variables#max_session_age) to a number in minutes. The default value is 20,160 (two weeks). For example, to keep people signed in for 24 hours at most: ```sh MAX_SESSION_AGE=1440 ``` To automatically clear a person's login cookies when they end a browser session: ```sh MB_SESSION_COOKIES=true ``` To manually log someone out of Metabase, load the following URL (for example, in a hidden iframe on the logout page of your application): ```sh https://metabase.yourcompany.com/auth/logout ``` ## Supported postMessage messages _from_ embedded Metabase To keep up with changes to an embedded Metabase URL (for example, when a filter is applied), set up your app to listen for "location" messages from the embedded Metabase. If you want to use this message for deep-linking, note that "location" mirrors "window.location". ```json { "metabase": { "type": "location", "location": LOCATION_OBJECT_OR_URL } } ``` To make an embedded Metabase page (like a question) fill up the entire iframe in your app, set up your app to listen for a "frame" message with "normal" mode from Metabase: ```json { "metabase": { "type": "frame", "frame": { "mode": "normal" } } } ``` To specify the size of an iframe in your app so that it matches an embedded Metabase page (such as a dashboard), set up your app to listen for a "frame" message with "fit" mode from Metabase: ```json { "metabase": { "type": "frame", "frame": { "mode": "fit", "height": HEIGHT_IN_PIXELS } } } ``` ## Supported postMessage messages _to_ embedded Metabase To change an embedding URL, send a "location" message from your app to Metabase: ```json { "metabase": { "type": "location", "location": LOCATION_OBJECT_OR_URL } } ``` ## Group strategies for row and column security If you want multiple people from a single customer account to collaborate on questions and dashboards, you'll need to set up one [group](../people-and-groups/managing#groups) per customer account. You can handle [row and column security](../permissions/row-and-column-security) with a single, separate group. For example, each person could be part of a customer group that sets up data permissions with row and column security via a certain attribute that applies to everyone across all your customer accounts. Additionally, each person within a single customer account could also be a member of a group specific to that customer account. That way they can collaborate on collections with other people in their organization, without seeing stuff created by people from other customers' accounts. ## Showing or hiding Metabase UI components See [full app UI components](./full-app-ui-components). For more granular control over embedded components, consider using [Modular embedding](./modular-embedding) instead. ## Metabot in full-app embeds See [Embedded Metabot settings](../ai/settings#enable-metabot). ## Reference apps To build a sample full app embed using SSO with JWT, see our reference apps: - [Node.js + Express](https://github.com/metabase/metabase-nodejs-express-interactive-embedding-sample) (with [quick start guide](./full-app-embedding-quick-start-guide)) - [Node.js + React](https://github.com/metabase/sso-examples/tree/master/app-embed-example) ## Further reading - [Full app embedding quick start](./full-app-embedding-quick-start-guide) - [Strategies for delivering customer-facing analytics](/learn/metabase-basics/embedding/overview). - [Permissions strategies](/learn/metabase-basics/administration/permissions/strategy). - [Customizing Metabase's appearance](../configuring-metabase/appearance). --- # Full app embedding UI components To change the interface of your full app embed, you can add parameters to the end of your embedding URL. If you want to change the colors or fonts in your embed, see [Customizing appearance](../configuring-metabase/appearance). > For more granular control of embedded components, consider using [Modular embedding](./modular-embedding) instead of full app embedding - it's an improved, more customizable option for embedding Metabase elements. For example, you can disable Metabase's [top nav bar](#top_nav) and [side nav menu](#side_nav) like this: ``` your_embedding_url?top_nav=false&side_nav=false ``` ![Top nav and side nav disabled](./images/no-top-no-side.png) Here's an example using the URL constructor to add parameters to the URL for the iframe: ```tsx const mods = "logo=false&top_nav=true&search=true&new_button=true"; app.get("/sso/metabase", restrict, (req, res) => { const ssoUrl = new URL("/auth/sso", METABASE_SITE_URL); ssoUrl.searchParams.set("jwt", signUserToken(req.session.user)); ssoUrl.searchParams.set("return_to", `${req.query.return_to ?? "/"}?${mods}`); res.redirect(ssoUrl); }); ``` Parameters include: - [Action buttons](#action_buttons) - [Additional info](#additional_info) - [Breadcrumbs](#breadcrumbs) - [Entity types](#entity_types) - [Header](#header) - [Locale](#locale) - [Logo](#logo) - [New button](#new_button) - [Search](#search) - [Side nav](#side_nav) - [Top nav](#top_nav) > To make sure that query parameters are preserved when using [click behavior](../dashboards/interactive#customizing-click-behavior), configure the [Site URL](../configuring-metabase/settings#site-url) Admin setting to be your Metabase server URL. ## `action_buttons` Visible by default on question pages when the [header](#header) is enabled. To hide the action buttons such as **Filter**, **Summarize**, the query builder button, and so on: ``` header=false&action_buttons=false ``` ![Action buttons](./images/action-buttons.png) ## `additional_info` Visible by default on question and dashboard pages when the [header](#header) is enabled. To hide the gray text "Edited X days ago by FirstName LastName", as well as the breadcrumbs with collection, database, and table names: ``` header=false&additional_info=false ``` ![Additional info](./images/additional-info.png) ## `breadcrumbs` Shown by default in the top nav bar. Collection breadcrumbs show the path to the item (i.e., the collection(s) the item is in). This does not affect Data breadcrumbs if the user has Query Builder permissions. To hide the breadcrumbs: ``` breadcrumbs=false ``` ## `data_picker` `data_picker` controls the menu for selecting data sources in questions. ![Simple data picker](./images/data-picker.png) The default behavior for the data picker is: - Show tables and models. - Exclude metrics and questions. - Display a simple dropdown menu. If there are 100 or more items, Metabase will display a souped-up data picker. You can opt for the full data picker by setting `data_picker=staged`: ![Full data picker](./images/full-data-picker.png) The above data picker has three entity types selected: ``` data_picker=staged&entity_types=table,model,question ``` ## `entity_types` You can show or hide different entity types in the data picker, sidebar, and the New button menu. For example, you may only want to show tables: ``` entity_types=table ``` If only tables are allowed, the sidebar won't show models: ![Sidebar without models](./images/sidebar-without-models.png) Available entity types are: - `table` - `model` - `question` (only works with `data_picker=staged`) You can separate entity types with a comma: ``` entity_types=table,model ``` ## `header` Visible by default on question and dashboard pages. To hide a question or dashboard's title, [additional info](#additional_info), and [action buttons](#action_buttons): ``` header=false ``` ## `locale` You can change the language of the user interface via a parameter. For example, to set the locale to Spanish: ``` locale=es ``` Read more about [localization](../configuring-metabase/localization). ## `logo` Whether to show the logo that opens and closes the sidebar nav. Default is true. The logo's behavior depends on the `side_nav` setting: | `logo` | `side_nav` | Result | | ------ | ---------- | --------------------------------------------------------------------- | | true | true | Shows your configured logo in the sidebar | | true | false | No sidebar or logo functionality | | false | true | Shows a generic sidebar icon (gray when normal, brand color on hover) | | false | false | No sidebar or logo, with breadcrumbs aligned to the left edge | ## `new_button` Hidden by default. To show the **+ New** button used to create queries or dashboards: ``` top_nav=true&new_button=true ``` ## `search` Hidden by default. To show the search box in the top nav: ``` top_nav=true&search=true ``` ## `side_nav` The navigation sidebar is shown on `/collection` and home page routes, and hidden everywhere else by default. To allow people to minimize the sidebar: ``` top_nav=true&side_nav=true ``` ![Side nav](./images/side-nav.png) ## `top_nav` Shown by default. To hide the top navigation bar: ``` top_nav=false ``` ![Top nav bar](./images/top-nav.png) The `top_nav` parameter controls the visibility of the entire top navigation bar. When `top_nav` is set to `false`, all child elements (`search`, `new_button`, and `breadcrumbs`) are automatically hidden. When `top_nav` is set to `true`, you can individually control the visibility of these child elements. --- # Guest embeds Guest embeds are a way to embed basic Metabase components in your app without requiring you to create a Metabase account for each person viewing the charts and dashboards. But not logging people in to your Metabase has some major tradeoffs: see [limitations](#guest-embed-limitations). "Guest" refers to the authentication approach: Metabase doesn't create a session for each person. Authentication has nothing to do with data freshness. Dashboards and charts in guest embeds always show live data from your database. Even though you're not using SSO, guest embeds are still secure: Metabase will only load the embed if the request has a JWT signed with the secret shared between your app and your Metabase. The JWT also includes a reference to the resource to load (like the ID of the embedded item), and any values for parameters. To restrict data in guest embeds for specific people or groups, use [locked parameters](#locked-parameters). ## Turning on guest embedding in Metabase The path to embedding settings depends on your Metabase version: - **OSS**: **Admin > Embedding** - **Starter/Pro/Enterprise**: **Admin > Embedding > Guest embeds** Toggle **Enable guest embeds**. ## Creating a guest embed ![Sharing button to embed dashboard](./images/sharing-embed.png) To create a guest embed: 1. Go to the item that you want to embed in your website. You can also open a command palette with Ctrl/Cmd+K and type "New embed". 2. Click the **sharing icon**. 3. Select **Embed**. 4. Under **Authentication**, select **Guest**. 5. Optional: [customize the appearance of the embed](./appearance) 6. Optional: [Add parameters to the embed](./components). 7. Click **Publish**. 8. Get the code snippet that the wizard generates and add it to your app. ![Guest embed settings](./images/guest-embed-settings.png) ## Notes on the code the wizard generates You can edit the code (see [components](./components) and [appearance](./appearance)). But here's an overview of the code the wizard generates, and where to put it. ### Client-side code Add the embed script and configuration to your HTML: ```html ``` Then add the component for the item you want to embed: ```html ``` > Never hardcode JWT tokens directly in your HTML. Always fetch the token from your backend and pass the token to the web component programmatically. ### Server-side code Your server generates signed JWT tokens that authenticate the embed request. Here's an example using Node.js: ```javascript const jwt = require("jsonwebtoken"); const METABASE_SECRET_KEY = "YOUR_METABASE_SECRET_KEY"; const payload = { resource: { dashboard: 10 }, // or { question: 5 } for questions params: {}, exp: Math.round(Date.now() / 1000) + 10 * 60, // 10 minute expiration }; const token = jwt.sign(payload, METABASE_SECRET_KEY); ``` Replace `YOUR_METABASE_SECRET_KEY` with your [embedding secret key](#regenerating-the-embedding-secret-key). ### Component attributes You can set different attributes to enable/disable UI. Here are some example attributes: | Attribute | Description | | ----------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `token` | Required. The signed JWT token from your server. | | `with-title` | Show or hide the title. Values: `"true"` or `"false"`. | | `with-downloads`\* | Enable or disable downloads. Values: `"true"` or `"false"`. | | `initial-parameters` | JSON string of initial parameter values (uncontrolled). Example: `'{"category":["Gizmo"]}'`. See [Modular embedding parameters](./parameters#pass-parameter-values-to-embedded-components). | | `parameters` | JSON string of parameter values (controlled). Example: `'{"category":["Gizmo"]}'`. See [Modular embedding parameters](./parameters#pass-parameter-values-to-embedded-components). | | `auto-refresh-interval` | Dashboards only. Auto-refresh interval in seconds. | | `custom-context` | Forwarded to your [`guestEmbedProviderUri`](#refreshing-or-initializing-the-jwt-from-your-server) endpoint as `customContext`. Either a string (e.g., `"gadgets-tab"`), or a JSON-stringified object like `initial-parameters` (e.g., `'{"tab":"gadgets","region":"us-east"}'`). | \* Disabling downloads is only available on [Pro](/product/pro) and [Enterprise](/product/enterprise) plans. Attributes will differ based on the type of thing you're embedding. Guest embeds have fewer options than embeds that use SSO. See more on [components and their attributes](./components). ### Customizing appearance of guest embeds Appearance settings available for guest embeds depend on your Metabase plan. If you're running Metabase OSS/Starter, you can select light or dark theme. If you're running Metabase Pro/Enterprise, you'll have access to granular customization options, see [Appearance](./appearance). ## Configuring parameters Parameters are disabled by default, which also makes them hidden from end-users. You can configure each parameter to be: - **Disabled**: Hidden from end-users. Can't be set. This is the default. - **[Editable](#editable-parameters)**: End-users can see and modify the parameter values. - **[Locked](#locked-parameters)**: Hidden from end-users, set by your server (not the end-users) via the JWT. To configure parameters: 1. Go to your embedded question or dashboard. 2. Click the **sharing icon** and select **Embed**. 3. Under **Parameters**, select the visibility option for each parameter, and optionally default value(s). 4. Click **Publish**. ### Editable parameters When you set Editable parameters, you can set default values for the filters, but users can change these when viewing the question or dashboard. **Server code** On the server, you pass an empty `params` object: ```javascript // you will need to install via 'npm install jsonwebtoken' or in your package.json const jwt = require("jsonwebtoken"); const METABASE_SECRET_KEY = "YOUR_METABASE_SECRET_KEY"; const payload = { resource: { dashboard: 10 }, params: {}, exp: Math.round(Date.now() / 1000) + 10 * 60, // 10 minute expiration }; const token = jwt.sign(payload, METABASE_SECRET_KEY); ``` **Client code** You set default parameters on the client side with the `initial-parameters` key. ```html ``` See [Modular embedding parameters](./parameters#pass-parameter-values-to-embedded-components) for controlled parameters documentation. ### Locked parameters Locked parameters let you filter data without exposing the filter to the end-user. Locking parameters is useful for restricting data based on who's viewing the embed (for example, showing each customer only their own data). ![Locked parameters](./images/locked-parameters.png) To use locked parameters, you need to: 1. Set the parameter to **Locked** in the embed settings. 2. Include the parameter value in your JWT token on the server. 3. Publish the item. Here's an example of the server and client code that Metabase will generate if you lock a "category" parameter: **Server code (Node.js)** You set the locked parameter on the server, passing it in the token. ```javascript // Install via 'npm install jsonwebtoken' const jwt = require("jsonwebtoken"); const METABASE_SECRET_KEY = "YOUR_METABASE_SECRET_KEY"; const payload = { resource: { dashboard: 10 }, params: { category: ["Gadget"], // Set the locked parameter value to Gadget }, exp: Math.round(Date.now() / 1000) + 10 * 60, // 10 minute expiration }; const token = jwt.sign(payload, METABASE_SECRET_KEY); ``` **Client code (HTML)** The parameter is set by the JWT: ```html ``` The end user won't see the "category" filter, but the dashboard will only show data for the "Gadget" category (or whatever you passed to the "category" array in the `params` object in the server payload you used to sign the JWT). ## Updating a locked parameter Things to keep in mind if you need to make changes to your locked parameters. ### Include all locked parameters in your JWT Once you publish a question or dashboard with a locked parameter, you _must_ include the name of the locked parameter in the `params` object when you sign the JWT. If you leave the parameter out, Metabase will refuse the request and log: `You must specify a value for : in the JWT`. For example, if your locked parameter is `category`, the error will read `You must specify a value for :category in the JWT`. ### Pass an empty array to turn off a locked parameter If you don't want the locked filter to apply for a given token, pass an empty array, `[]`, as the value for the parameter in the JWT: ```javascript const payload = { resource: { dashboard: 10 }, params: { category: [], // locked filter is bypassed for this token }, exp: Math.round(Date.now() / 1000) + 10 * 60, }; ``` This is handy when you want to reuse the same dashboard or question across contexts, and conditionally skip a locked filter in some of them. ### Filter name should match the locked parameter name If you rename a dashboard filter that's used as a locked parameter, update the matching key in your JWT `params` object. Locked parameters that are connected to a [SQL variable](../questions/native-editor/sql-parameters) don't need to be renamed on the server. ### Multiple locked parameters or multiple values The values for a locked parameter in your JWT should match your filter's values exactly. The best way to set multiple locked parameters, or pass multiple values to a single locked parameter, is to pick a filter value under **Preview locked parameters** in the embed wizard and copy the server code that Metabase generates. Multiple locked parameters combine with `AND`, not `OR`. If you only want to apply a subset of the locked parameters for a given token, pass `[]` for the ones you want to skip (see [pass an empty array](#pass-an-empty-array-to-turn-off-a-locked-parameter)). ## Locked parameters limit the values available to other editable parameters Because locked parameters filter data _before_ the results are displayed in the embed, locked parameters also limit the values available to any editable filter widgets on the same item. For example, say you're embedding a dashboard with two filters, State and City. If you lock the State parameter with the value "Vermont", the City filter will only show cities in Vermont in its dropdown. You don't need to wire the filters together explicitly — they'll behave like [linked filters](../dashboards/filters#linking-filters). ## Locked parameters on dashboards with SQL questions If a locked parameter is linked to a dashboard filter that's in turn linked to a SQL question, you'll only be able to pass a _single_ value for that locked parameter in the JWT. For example, if you have a dashboard filter called "Breakfast" with the values "Hash browns", "Muffin", and "Waffles", and the filter is linked to _any_ SQL question on the dashboard, you can only pass one of those options as the locked parameter value. ## Using locked parameters to power custom widgets in your app Because Metabase doesn't render locked parameters as filter widgets, you can use them to power custom filter widgets that you build yourself. You may want to build your own filter widget(s) to: - Match the widget to the look and feel of your app. - Add custom logic, like remembering recently used values. - Reuse one dashboard in different ways in different parts of your app. For example, a sales dashboard that's locked by "region" in one place and by "team" in another. When the end-user changes a value in your custom widget, re-sign a new JWT on your server with the updated `params` and swap it onto the web component's `token` attribute. The embed will re-request the data with the new locked value. ## Refreshing or initializing the JWT from your server JWTs that you sign for guest embeds have an expiration (`exp`). Once a token expires, the embed can't load fresh data, and any filter selections the viewer made will reset on the next request. To keep the embed alive without reloading the page, you can configure a guest token endpoint on your server to hand out fresh JWTs on demand. The endpoint can serve two flows: - **Refreshing tokens**: when the embed's current JWT is about to expire, the embed POSTs to your endpoint to get the new JWT, and swaps it in. - **Initializing with a token** (optional): if you don't want to pre-render a JWT in the HTML at all, the embed can call the same endpoint on load to fetch that first JWT. ### Setting the endpoint URL in the guest embed Add `guestEmbedProviderUri` to your `metabaseConfig`. The value is a path (or full URL) to an endpoint in your app: ```html ``` When the embed needs a token, it sends a `POST` request to `guestEmbedProviderUri` with a JSON body, which includes cookies, so you can authenticate the request with your app's existing session. Request: ```json { "entityType": "dashboard", "entityId": 10, "customContext": "..." } ``` | Field | Description | | --------------- | ------------------------------------------------------------------------- | | `entityType` | `"dashboard"` or `"question"`. | | `entityId` | The ID of the dashboard or question being embedded. | | `customContext` | Optional. The string or object you set on the `custom-context` attribute. | Response: a JSON object with a single `jwt` field: ```json { "jwt": "YOUR_NEWLY_SIGNED_JWT" } ``` ### Refresh flow Pre-render an initial JWT on the component (just like a regular guest embed) and configure `guestEmbedProviderUri`. When the JWT expires, the embed will call your endpoint to get a fresh one and swap it in. ```html ``` ### Initialize the embed without a JWT in the HTML If you don't want to render the JWT in the HTML at all, omit the `token` attribute and use `dashboard-id` (or `question-id`) instead. If you've set the `guestEmbedProviderUri`, then the embed will call that endpoint on load to fetch the first JWT. ```html ``` This way you can keep all your token-issuing logic in one place on your server. ### Example endpoint (Node.js / Express) ```javascript const jwt = require("jsonwebtoken"); const METABASE_SECRET_KEY = "YOUR_METABASE_SECRET_KEY"; app.post("/api/metabase-guest-token", (req, res) => { // Authenticate using your app's existing session. const user = req.session?.user; if (!user) { return res.status(403).json({ error: "Not signed in" }); } const { entityType, entityId, customContext } = req.body; const payload = { resource: { [entityType]: entityId }, params: paramsFor(user, customContext), exp: Math.round(Date.now() / 1000) + 10 * 60, // 10 minute expiration }; res.json({ jwt: jwt.sign(payload, METABASE_SECRET_KEY) }); }); ``` Because the embed's request includes your app's session cookie, your endpoint can: - Refuse to issue a JWT (with a `403`) for visitors who aren't signed in to your app. - Compute different `params` (i.e., locked filter values) per visitor. ### Sending custom context When you embed the same dashboard or question more than once on a page, you can use the `custom-context` attribute to tell your endpoint which copy is requesting a token. The value you pass is forwarded to your endpoint as `customContext`. For example, two copies of the same dashboard scoped to different categories: ```html ``` You can also pass a JSON-stringified object (the embed parses it before forwarding it on, so your endpoint receives a real object): ```html ``` Your endpoint can switch on `customContext` to set different locked parameters, like so: ```javascript function paramsFor(user, customContext) { switch (customContext) { case "gadgets-tab": return { category: ["Gadget"] }; case "doohickeys-tab": return { category: ["Doohickey"] }; default: return {}; } } ``` ## Disabling embedding for a question or dashboard 1. Visit the embeddable question or dashboard. 2. Click the **sharing icon** (square with an arrow pointing to the top right). 3. Select **Embed**. 4. Select **Guest embedding** 5. Click **Unpublish**. Admins can find a list of embedded items in **Admin > Embedding** (on Pro and Enterprise plans, check the **Guest embeds** tab). ## Removing the "Powered by Metabase" banner ![Powered by Metabase](./images/powered-by-metabase.png) The banner appears on guest embeds created with Metabase's open-source version. To remove the banner, you'll need to upgrade to a [Pro](/product/pro) or [Enterprise](/product/enterprise) plan. ## Regenerating the embedding secret key Your embedding secret key is used to sign JWTs for all of your embeds. 1. Go to **Admin > Embedding**. On Pro and Enterprise plans, check the **Guest embeds** tab. 2. Under **Regenerate secret key**, click **Regenerate key**. This key is shared across all guest embeds. Whoever has access to this key could get access to all embedded artifacts, so keep this key secure. If you regenerate this key, you'll need to update your server code with the new key. ## Custom destinations on dashboards in guest embeds You can only use the **URL** option for [custom destinations](../dashboards/interactive#custom-destinations) on dashboards with guest embedding. External URLs will open in a new tab or window. You can propagate filter values into the external URL, unless the filter is locked. ## Translating embeds To translate an embed, set the `locale` in `window.metabaseConfig`: ```html ``` The `locale` setting works for all modular embeds (guest, SSO, and SDK). Metabase will automatically translate UI elements (like menus and buttons). To also translate content like dashboard titles and filter labels, you'll need to upload a [translation dictionary](./translations). ## How guest embedding works Guest embeds use web components (`` and ``) that communicate with your Metabase instance. Each embed request requires a JWT token signed with your secret key. When a visitor views your page: 1. Your server generates a signed JWT token containing the resource ID (dashboard or question) and any locked parameters. 2. The web component sends the token to Metabase. 3. Metabase verifies the JWT signature using your secret key. 4. If valid, Metabase returns the embedded content. 5. If you've configured [JWT refresh](#refreshing-or-initializing-the-jwt-from-your-server), the embed will fetch a fresh JWT from your endpoint when it next needs to make a data request after the current token has expired — not on a background timer. An idle embed makes no refresh requests. (Optionally, the embed can also fetch the very first JWT from your endpoint on load.) The embed keeps working without a page reload. For interactive filters, you can pass initial parameter values via the `initial-parameters` attribute. When a visitor changes a filter, the web component handles the update automatically. The signed JWT is generated using your [Metabase secret key](#regenerating-the-embedding-secret-key). The secret key tells Metabase that the request can be trusted. Note that this secret key is shared for all guest embeds, so whoever has access to that key will have access to all embedded artifacts. If you want to embed charts with additional interactive features, like [drill-down](../questions/visualizations/drill-through) and [self-service querying](../questions/query-builder/editor), see [Modular embedding](./modular-embedding). ## Using guest embeds with the SDK If you're using the [Modular Embedding SDK](./sdk/introduction), and you also want to embed a question or dashboard using guest authentication, you'll still need to visit the item in your Metabase and publish the item. You can ignore the code the wizard generates, but in order for Metabase to know it's okay to serve the item, you need to publish it. One limitation, however, is that you can only have one type of authentication per page of your app. For example, on a single page in your app, you can't have both one question using guest authentication and another question using SSO. ## Guest embed limitations Because guest embeds don't require you to create a Metabase account for each person via SSO, Metabase can't know who is viewing the embed, and therefore can't give them access to all their data and all the cool stuff Metabase can do. Guest embeds can't take advantage of: - [Row and column security](../permissions/row-and-column-security) - [Database routing](../permissions/database-routing) - [Drill-through](../questions/visualizations/drill-through) - [Usage analytics](../usage-and-performance-tools/usage-analytics) - [Query builder](../questions/query-builder/editor) - [AI chat](./sdk/ai-chat) For those features, check out [Modular embedding with SSO](./modular-embedding). ## Further reading - [Reference apps repo](https://github.com/metabase/embedding-reference-apps). - [Strategies for delivering customer-facing analytics](/learn/metabase-basics/embedding/overview). - [Publishing data visualizations to the web](/learn/metabase-basics/embedding/charts-and-dashboards). - [Customizing Metabase's appearance](../configuring-metabase/appearance). --- # Embedding introduction You can embed Metabase tables, charts, and dashboards—even Metabase's query builder—in your website or application. Here are the different ways you can embed Metabase: - [Modular embedding](#modular-embedding) - [Full app embedding](#full-app-embedding) - [Public links](#public-links-and-embeds) ## Modular embedding With [modular embedding](./modular-embedding), you can embed individual Metabase components in your web app. You can use guest embeds for basic functionality, or use SSO to take full advantage of Metabase. You can use two different ways to authenticate modular embeds: - [SSO](#modular-embedding) - [Guest](#guest-embedding) Here's a basic breakdown of what each auth type enables: | Component | SSO | Guest | | ----------------------------------------------------- | --- | ----- | | Chart | ✅ | ✅ | | Chart with drill-through | ✅ | ❌ | | Dashboard | ✅ | ✅ | | Dashboard with drill-through | ✅ | ❌ | | [Query builder](../questions/query-builder/editor) | ✅ | ❌ | | Browser to navigate collections | ✅ | ❌ | | Metabot AI chat | ✅ | ❌ | ### SSO embeds With SSO, Metabase can know who's viewing what, which unlocks a lot of power. You can automatically apply [data permissions](../permissions/embedding), which means you can give people access to all the cool tools Metabase provides, and everyone will only ever see the data they're allowed to. **When to use SSO**: You want to offer multi-tenant, self-service analytics, or you want to include the query builder, AI chat, drill-through, or a collection browser. If you're building a SaaS product with embedded analytics for multiple customers, you can keep customer data isolated with [Tenants](./tenants). ### Guest embedding [Guest embeds](./guest-embedding) are a secure way to embed charts and dashboards. Guest embedding works on all Metabase plans, including OSS and Starter. **When to use guest embeds**: simple embedding use cases where you don't want to offer ad-hoc querying or chart drill-through. To filter data relevant to the viewer, you can use guest embeds with [locked parameters](./guest-embedding#locked-parameters). ## Public links and embeds If you'd like to share your data with the good people of the internet, admins can create a [public link](./public-links) or embed a question or dashboard directly in your website. **When to use public links and embeds**: One-off charts and dashboards. Admins can use public links when you just need to show someone a chart or dashboard without giving people access to your Metabase. And you don't care who sees the data; you want to make the item available to everyone. ## Full app embedding [Full app embedding](./full-app-embedding) allows you to embed the entire Metabase app in an iframe, and integrate Metabase SSO with your app's authentication. ## Comparison of embedding types | Action | [Modular SDK](./sdk/introduction) | [Modular SSO](./modular-embedding) | [Modular Guest](./guest-embedding) | [Full app](./full-app-embedding) | [Public](../embedding/public-links) | | ------------------------------------------------------------------------------------------------------------ | ------------------------------------ | ------------------------------------- | ------------------------------------- | ----------------------------------- | -------------------------------------- | | Charts and dashboards | ✅ | ✅ | ✅ | ✅ | ✅ | | [Filter widgets](/glossary/filter-widget) | ✅ | ✅ | ✅ | ✅ | ✅ | | Export results\* | ✅ | ✅ | ✅ | ✅ | ✅ | | [Locked filters](./static-embedding-parameters#restricting-data-in-a-static-embed-with-locked-parameters) | ❌ | ❌ | ✅ | ❌ | ❌ | | [Data segregation](../permissions/embedding) | ✅ | ✅ | ❌ | ✅ | ❌ | | [Drill-through menu](../questions/visualizations/drill-through) | ✅ | ✅ | ❌ | ✅ | ❌ | | [Query builder](../questions/query-builder/editor) | ✅ | ✅ | ❌ | ✅ | ❌ | | [Basic appearance customization](../configuring-metabase/appearance)\*\* | ✅ | ✅ | ✅ | ✅ | ✅ | | [Advanced theming](./appearance) | ✅ | ✅ | ❌ | ❌ | ❌ | | [Usage analytics](../usage-and-performance-tools/usage-analytics) | ✅ | ✅ | ❌ | ✅ | ❌ | | Embed individual Metabase components | ✅ | ✅ | ❌ | ❌ | ❌ | | Manage access and interactivity per component | ✅ | ✅ | ❌ | ❌ | ❌ | | Custom layouts | ✅ | ❌ | ❌ | ❌ | ❌ | | Customize behavior with [plugins](./sdk/plugins) | ✅ | ❌ | ❌ | ❌ | ❌ | | [Custom visualizations](../questions/visualizations/custom) | ✅ | ❌ | ❌ | ❌ | ❌ | | AI chat | ✅ | ✅ | ❌ | ✅ | ❌ | \* Each embedding type allows data downloads by default, but only [Pro and Enterprise](/pricing/) plans can disable data downloads. \*\* Requires a [Pro and Enterprise](/pricing/) plan for any embedding type. ### Should you use the Modular embedding SDK? If your app uses React, you can go with the modular embedding SDK, but you don't need to. The modular embeds that you can set up in the [in-app wizard](./modular-embedding) are built on top of the Modular embedding SDK. Using the SDK just gives you slightly more customization (see the table above), but your app has to use React. You can always start with modular embedding, then move to the SDK if you really need that extra customization. Both support SSO and Guest embeds. ## Resources for AI agents If you're using an AI agent to help you embed Metabase in your app, check out [AI agent resources](./ai-agent-resources). ## Tracking embed usage [Usage Analytics](../usage-and-performance-tools/usage-analytics) tracks embed usage, including embedding context, authentication methods, hostname, and other metadata. Check out the [Embedding usage dashboard](../usage-and-performance-tools/usage-analytics-reference#embedding-usage). For information about the anonymous usage data Metabase collects from embedded components, see [Embedding telemetry](../installation-and-operation/information-collection#embedding-telemetry). ## Embedding limitations - Currently, you can't embed [documents](../documents/introduction) (though you can create [public documents](./public-links)). - Only the [Modular embedding SDK](./sdk/introduction) renders [custom visualizations](../questions/visualizations/custom), and only ones you allowlist with the [`allowedCustomVisualizations` prop](./sdk/config#custom-visualizations). In other embedding types, any card that uses a custom visualization falls back to the default visualization for the query's results. ## Further reading - [Strategies for delivering customer-facing analytics](/learn/metabase-basics/embedding/overview). - [Publishing data visualizations to the web](/learn/metabase-basics/embedding/charts-and-dashboards). - [Multi-tenant self-service analytics](/learn/metabase-basics/embedding/multi-tenant-self-service-analytics). - [Customizing Metabase's appearance](../configuring-metabase/appearance). - [Securing embedded Metabase](./securing-embeds). --- # Modular embedding ![Modular embedding wizard](./images/modular-embedding-wizard.png) Modular embedding lets you embed and customize Metabase [components](./components) (like dashboards, the query builder, AI chat, and more) into your own application. You don't need to write embedding code on your own - just use the wizard to create a code snippet and paste it into your app. If you're using React, check out the [Modular embedding SDK](./sdk/introduction). > **Want guest embeds?** For modular embedding without authentication (available on all plans), see [Guest Embeds](./guest-embedding). ## Enable modular embedding 1. In Metabase, go to **Admin > Embedding**. 2. Toggle on **Enable modular embedding**. **For guest embeds**: You're done! Skip to [Create a new embed](#create-a-new-embed) or see the [Guest embedding guide](./guest-embedding) for detailed setup instructions. **For authenticated embeds** (Pro/Enterprise only), some more steps: 3. Under **Cross-Origin Resource Sharing (CORS)**, add the URLs of the websites where you want to embed Metabase (such as `https://*.example.com`). For testing embeds, you can use `localhost` which is always included in CORS policy. 4. If embedding on a different domain, see [Embedding Metabase in a different domain](./authentication#embedding-metabase-in-a-different-domain). ## Create a new embed You can generate a code snippet for embedding a component by going through the embed wizard. ### 1. Open the embed wizard In your Metabase: 1. Visit the item you want to embed. 2. Click the sharing icon. 3. Select **Embed**. ![Embed share button](./images/embed-share-button.png) You can also open a command palette with Ctrl/Cmd+K, type "New embed". You'll get a wizard to help you set up your embed. You can also go to **Admin > Embedding > Modular embedding** and click **New embed**. ![New modular embed from the admin settings](./images/new-modular-embed-from-admin-settings.png) ### 2. Pick how to authenticate the embed With SSO, Metabase can know who is viewing the embed, and it can unlock all of its bells and whistles (see this [comparison between SSO and guest embeds](./introduction#comparison-of-embedding-types)). This page covers the [SSO setup for your Metabase](./authentication). If you don't need to set up SSO, check out the [guest embed docs](./guest-embedding). ### 3. Customize your embed The exact customization options you see will depend on what type of entity you're embedding and which [Metabase plan](/pricing) you're on. You'll see a live preview of the embed with your customizations. ![Embed flow options for AI chat](./images/embed-flow-options.png) If you're on Metabase OSS/Starter plans, you can select light or dark themes for your components. On Metabase Pro/Enterprise, you can also: - Pick a [saved theme](./appearance#embedding-themes) you've created in **Admin settings > Embedding > Themes**. - Pick specific colors for brand, text, and background in the embed wizard. - Add more [advanced theme options](./appearance#advanced-theming) by editing the generated snippet later. All the customization options you select in this wizard will be reflected in the code snippet that the embed wizard will generate for you, and you'll be able to add more options later. For example, this code defines the font, color, and size for text, background colors, and colors for filters and summaries: ```html ``` For more look-and-feel twiddling, see [appearance](./appearance). Once you're done customizing your embed, click **Next**. ## Add the embedding script to your app If you [create an embed through the built-in embed wizard](#create-a-new-embed), Metabase will generate a code snippet that you can copy and paste into your app. See the example below. You can later modify this code snippet to specify additional appearance options or change the behavior of some components. You'll add this code snippet to your app and refresh the page. The code snippet has three parts: - Loading the modular embedding library from your Metabase instance. - Setting global configuration settings, like the URL of your Metabase and the `theme`. See [Page-level config](#page-level-config). - The component(s) to embed, with their parameters. See [Components](./components). Here's an example snippet: ```html ``` Note the `defer` attribute and the reference to your Metabase URL in the script that loads `embed.js` library. If you're embedding multiple components in a single page, you only need to include the ` ``` The same pattern works for `metabase-question` via the `sqlParameters` property. To switch a component back to uncontrolled mode (leaving the last applied values in place), set the property to `undefined`. #### Clearing parameters To clear a single parameter, set its value to `null`. This strictly clears the parameter and ignores its default value. ```html ``` To clear every parameter, assign an empty object `{}`. ```html ``` #### Observe applied changes with `parameters-change` / `sql-parameters-change` Listen for events to keep your page in sync with what's actually applied: ```html ``` The `event.detail` carries the [dashboard parameter change payload](#dashboard-parameter-change-payload). For SQL questions, listen for `sql-parameters-change` on ``. Its `event.detail` carries the [SQL question parameter change payload](#sql-question-parameter-change-payload). ## How parameter values are resolved These rules apply to all four props — `initialParameters` / `parameters` (dashboards) and `initialSqlParameters` / `sqlParameters` (SQL questions) — and to the matching web component attributes (`initial-parameters`, `parameters`, etc.). For each parameter slug: - **Set a value**: Pass a `string` for a single-option filter, and an array of `string`s for multi-option filters. - **Clear a value:** Set to `null`: the parameter is cleared and its default is not used. - **Reset to the default value**: Omit a value (or set to `undefined`) and the embed will fall back to the parameter's default (or `null` if it has no default). ## Dashboard parameter change payload Delivered to `onParametersChange` (SDK) and as `event.detail` for the `parameters-change` event (web components). `source` indicates why the callback fired: - `initial-state` - first applied snapshot, fired once per dashboard load. - `manual-change` - user edited parameters in UI. - `auto-change` - in the case of auto-updates, e.g. to pass normalized values back to parent. ## SQL question parameter change payload Delivered to `onSqlParametersChange` (SDK) and as `event.detail` for the `sql-parameters-change` event (web components). `source` indicates why the callback fired: - `initial-state` - first applied state, fired once per question load. - `manual-change` - user edited parameters in UI. - `auto-change` - in the case of auto-updates, e.g. to pass normalized values back to parent. --- # Public sharing > Only admins can create public links and iframes. Admins can create and share public links (URLs) for questions, dashboards, and documents. People can view them as standalone destinations (URLs) or as embedded iframes in another page or app. Public items display view-only results of your question, dashboard, or document, so visitors won't be able to drill down into the underlying data on their own. ## Create a public link for a question ![Create a public link for a question](./images/create-a-public-link.png) To create a public link for a question, admins can click on the **Sharing** icon at the top right of a question and select **Create a public link**. Copy the link and test it out by viewing the link in a private/incognito browser session. ## Public link to export question results in CSV, XLSX, JSON This export option is only available for questions, not dashboards. To create a public link that people can use to download the results of a question: 1. Click on the **Sharing** icon for the question. 2. Select **Create a public link**. 3. Click on the file format you want (below the **Public link** URL): CSV, XLSX, or JSON. ![Public export](./images/public-export.png) Open the public link in a new tab to test the download. ## Create a public link for a dashboard To share a dashboard via a public link, admins can click on the **Sharing** button in the top right menu. ![Sharing a dashboard](./images/dashboard-sharing.png) To embed a dashboard, see [guest embedding](./guest-embedding). ## Create a public link for a document To share a document via a public link, admins can click on the **Sharing** button in the top right menu and select **Create a public link**. Public documents are read-only: viewers cannot edit the content or add comments. For charts embedded in the document, viewers can download the results in CSV, XLSX, or JSON format using the **Download results** option in the chart menu. ## Exporting raw, unformatted question results To export the raw, unformatted rows, you'll need to append `?format_rows=false` to the URL Metabase generates. For example, if you create a public link for a CSV download, the URL would look like: ```html https://www.example.com/public/question/cf347ce0-90bb-4669-b73b-56c73edd10cb.csv?format_rows=false ``` By default, Metabase will export the results of a question that include any formatting you added (for example, if you formatted a column with floats to display as a percentage (0.42 -> 42%)). See docs for the [export format endpoint](/docs/latest/api#tag/public/GET/public/card/{uuid}/query/{export-format}). ## Simulating drill-through with public links Metabase's automatic [drill-through](../questions/visualizations/drill-through) won't work on public dashboards because public links don't give people access to your raw data. You can simulate drill-through on a public dashboard by setting up a [custom click behavior](../dashboards/interactive) that sends people from one public link to another public link. 1. Create a second dashboard to act as the destination dashboard. 2. [Create a public link](#create-a-public-link-for-a-dashboard) for the destination dashboard. 3. Copy the destination dashboard's public link. 4. On your primary dashboard, create a [custom destination](../dashboards/interactive#custom-destinations) with type "URL". 5. Set the custom destination to the destination dashboard's public link. 6. Optional: pass a filter value from the primary dashboard to the destination dashboard by adding a query parameter to the end of the destination URL: ``` /public/dashboard/?child_filter_name= ``` For example, if you have a primary public dashboard that displays **Invoices** data, you can pass the **Plan** name (on click) to a destination public dashboard that displays **Accounts** data: ![Public link with custom destination](./images/public-link-custom-destination.png) ## Public embeds ![Public embed](./images/public-embed.png) If you want to embed your question or dashboard as an iframe in a simple web page or app: 1. Click on the **Sharing** icon for your question or dashboard. 2. Click **Embed**. 3. In the bottom of the embedding popup, click on **Get embedding code**. 4. Copy the iframe snippet Metabase generates for you. 5. Paste the iframe snippet in your destination of choice. To customize the appearance of your question or dashboard, you can update the link in the `src` attribute with [public embed parameters](#public-embed-parameters). ## Public embed parameters To apply appearance or filter settings to your public embed, you can add parameters to the end of the link in your iframe's `src` attribute. Note that it's possible to find the public link URL behind a public embed. If someone gets access to the public link URL, they can remove the parameters from the URL to view the original question or dashboard (that is, without any appearance or filter settings). If you'd like to create a secure embed that prevents people from changing filter names or values, check out [guest embedding](./guest-embedding). ## Appearance parameters To toggle appearance settings, add _hash_ parameters to the end of the public link in your iframe's `src` attribute. See [appearance parameters](./static-embedding-parameters#customizing-the-appearance-of-a-static-embed). ## Filter parameters You can display a filtered view of your question or dashboard in a public embed. Make sure you've set up a [question filter](../questions/query-builder/filters) or [dashboard filter](../dashboards/filters) first. To apply a filter to your embedded question or dashboard, add a _query_ parameter to the end of the link in your iframe's `src` attribute, like this: ``` /dashboard/42?filter_name=value ``` For example, say that we have a dashboard with an "ID" filter. We can give this filter a value of 7: ``` /dashboard/42?id=7 ``` To set the "ID" filter to a value of 7 _and_ hide the "ID" filter widget from the public embed: ``` /dashboard/42?id=7#hide_parameters=id ``` To specify multiple values for filters, separate the values with ampersands (&), like this: ``` /dashboard/42?id=7&name=janet ``` You can hide multiple filter widgets by separating the filter names with commas, like this: ``` /dashboard/42#hide_parameters=id,customer_name ``` Note that the name of the filter in the URL should be specified in lower case, and with underscores instead of spaces. If your filter is called "Filter for User ZIP Code", you'd write: ``` /dashboard/42?filter_for_user_zip_code=02116 ``` ## Disable public sharing Public sharing is enabled by default. ![Enable public sharing](./images/enable-public-sharing.png) To disable public sharing: 1. Click the **grid** icon in the upper right. 2. Select **Admin**. 3. In the **Settings** tab, select **Public sharing**. 4. Toggle off **Public sharing**. Once toggled on, the **Public sharing** section will display Metabase questions, dashboards, documents, and actions with active public links. If you disable public sharing, then re-enable public sharing, all your previously generated public links will still work (as long as you didn't deactivate them). ## Deactivating public links and embeds ### Individual question or dashboard links and embeds 1. Visit the question or dashboard. 2. Click on **Sharing** icon. 3. Select **Public link** or **Embed**. 4. Click **Remove public link**. ## Deactivating multiple public links and embeds Admins can view and deactivate all public links for a Metabase. 1. Click the **grid** icon in the upper right. 2. Select **Admin**. 3. Go to the **Settings** tab. 4. Go to the **Public sharing** tab in the left sidebar. 5. For each item you want to deactivate, click on the **X** to revoke its public link. ## See all publicly shared content Admins can see all publicly shared questions, dashboards, documents, and actions in **Admin > Public Sharing**. ![See shared content](./images/see-shared-content.png) ## Further reading - [Publishing data visualizations to the web](/learn/metabase-basics/embedding/charts-and-dashboards). - [Customizing Metabase's appearance](../configuring-metabase/appearance). - [Embedding introduction](../embedding/start). --- # Modular embedding SDK - actions With the `useAction` hook, you can trigger an [action](../../actions/introduction) when someone clicks a button or submits a form in your app. The hook handles the HTTP request, exposes loading and error state as React state, and types the parameters the action expects. Basic CRUD and custom SQL actions are supported; HTTP-type actions are not. Always trigger actions through `useAction` — calling `POST /api/action/:id/execute` directly with `fetch` may be blocked in sandboxed embedding contexts. ## Triggering an action with `useAction` ```ts const { execute, isExecuting, result, error, reset } = useAction< TParameters, TKind // optional — drives the typed `result` shape >(actionId); ``` - `actionId` — the action's numeric id, its `entity_id` string, or `null`. Find the numeric id in Metabase by opening the action editor and copying it from the URL. - `TParameters` — a TypeScript type describing the parameters object that will be passed to `execute`. Keys are the action's parameter slugs (the names shown in the action editor). - `TKind` (optional) — the action's kind literal. Pass one of `"create"`, `"update"`, `"delete"`, `"bulk"`, or `"sql"` to get a typed `result` for that single shape. If you omit `TKind`, `result` defaults to a union of every possible response body (`AnyActionResult`), which you can narrow with `"" in result`. See [Typing the response](#typing-the-response). - `execute(parameters)` — call `execute` from an event handler to trigger the action. The hook doesn't auto-fire on mount. Resolves to the response body on success, throws on failure, or resolves to `null` if `actionId` is `null`, or the SDK isn't yet initialized. You can `await execute(parameters)` or fire and forget. The same error is written to `error` state either way, so a render-time error message will appear even without a `try`/`catch`. - `isExecuting` — `true` between the call and its resolution. Use `isExecuting` to disable the trigger and prevent double-clicks. - `result` — the response body, or `null` before the first call and after `reset()`. - `error` — the last thrown error, or `null`. See [Error handling](#error-handling). - `reset()` — clears `result` and `error`. #### API Reference - [Hook](./api/useAction) - [Return type](./api/UseActionResult) ## Example button to trigger an action This button calls a custom SQL action to apply a discount to an order: ```typescript ``` ## Parameter keys **Send parameters keyed by `slug`**. The parameter's display `name` (e.g. `"Discount"`) won't work; you must use the slug (e.g., `"discount"`). ### Parameter value types You can pass strings, number, and boolean parameters. For dates, pass an ISO 8601 string. Examples: ```typescript ``` #### Dates and timezones When the target column is `TIMESTAMP` without timezone, send the ISO value either _without a timezone offset_, or with the `Z` suffix: ```typescript ``` A timezone-offset value like `"2024-01-15T10:00:00+05:00"` is typically converted to UTC by the database driver, so the stored wall-clock shifts (the example above would store `05:00:00`). Exact behavior varies by warehouse — check your driver if precise timezone handling matters. For `TIMESTAMP WITH TIME ZONE` columns the offset is preserved as the same instant; for `DATE` columns timezone is irrelevant. When the value comes from a browser-local date picker (which often returns the user's local TZ), normalize before sending: ```typescript ``` If the string can't be parsed, the database driver throws and the message surfaces via `error.data.message` (see [Error handling](#error-handling)). ## Typing the response The action's **kind** drives the shape of `result`. Pass it as the second generic to `useAction` and `result` gets typed automatically: | Action kind | What it covers | `result` shape | | ------------------ | ------------------------------------------------ | ------------------------------------------------------------------------------------------------- | | `"create"` | Single-row insert (basic action) | `{ "created-row": Record }` | | `"update"` | Single-row update | `{ "rows-updated": readonly RowValue[] }` | | `"delete"` | Single-row delete | `{ "rows-deleted": readonly RowValue[] }` | | `"bulk"` | Any bulk variant (bulk create / update / delete) | `{ success: boolean; "rows-created"?: number; "rows-updated"?: number; "rows-deleted"?: number }` | | `"sql"` | Custom SQL action | `{ "rows-affected": number }` | #### API Reference - [ActionKind](./api/ActionKind) - [AnyActionResult](./api/AnyActionResult) - [ActionResultForKind](./api/ActionResultForKind) - [ActionResultForCreate](./api/ActionResultForCreate) - [ActionResultForUpdate](./api/ActionResultForUpdate) - [ActionResultForDelete](./api/ActionResultForDelete) - [ActionResultForBulk](./api/ActionResultForBulk) - [ActionResultForSql](./api/ActionResultForSql) Example with a known kind: ```typescript ``` ### Reading the result It's common not to read the result at all (see [After an action succeeds, you must refresh the data](#after-an-action-succeeds-you-must-refresh-the-data)). But if you do read the result, specify `TKind` if you know the action's result up front. If you don't supply `TKind`, the `result` defaults to `AnyActionResult`, which is the union of every possible response body. TypeScript knows the result is one of the five known shapes, just not which one. You can then narrow with the `in` operator: ```typescript ``` The union default catches mistyped reads: if the type system can't prove `result` has a key, it'll error. ## After an action succeeds, you must refresh the data When an action succeeds, you'll need to refresh any data in the UI that the action could have changed, otherwise the data on screen may be stale. There is no automatic refresh. After `execute` resolves successfully, refresh a question by remounting it: keep a `refreshKey` in state, use it as the question's `key`, and bump it after the action. The new `key` gives the question a fresh mount, which re-runs its query. ```typescript ``` If a single action invalidates more than one view, drive every dependent question off the same `refreshKey` so one state bump remounts them all and they re-query together: ```typescript ``` Don't try to drive the state from `result` directly. The response body is for confirmation (a row count, the inserted row's primary key, etc.). You can use the `result` for toasts or detail-view navigation, but you still need to re-read the source to update the data on screen. ## Error handling The hook normalizes whatever the underlying network client throws into a clean, public-facing shape and types `error` accordingly: `error.status` is **optional**: present for HTTP-level failures (4xx / 5xx), absent for transport-layer failures (offline, aborted) where no HTTP response was received. The actionable diagnostic for end users lives at `error.data.message`. `error.data.errors` is a per-field map (`{ : }`) when the backend reports parameter-level validation failures, keyed by the same parameter slugs you pass to `execute`. For whole-request failures (like a foreign-key constraint), it's an empty `{}` and the message lives in `error.data.message`. #### API Reference - [ActionExecuteError](./api/ActionExecuteError) For SQL or driver errors, `error.data.message` often includes a newline and the failing SQL statement on the next line, so render the error message inside an element with `white-space: pre-wrap` (a `
` is fine). A `` collapses the newlines into one wall of text.

The basic example above renders `error.data.message` with a static fallback when no message was provided:

```typescript

```

Display the error message verbatim. Don't replace the message with a generic "Something went wrong". The raw SQL / validation / permission error is what tells the person how to fix their input.

## Related

- [Actions documentation](../../actions/introduction)

---

# Modular embedding SDK - AI chat

![Embedded AI chat](../images/ai-chat.png)



You can embed an AI chat in your application similar to [Metabot](../modular-embedding) in Metabase.

Embedded Metabot is a more focused version of [Metabot](../../ai/metabot) designed to work well in an embedded context. Embedded Metabot can only display ad-hoc questions and metrics; it doesn't know about dashboards.

To help embedded Metabot more easily find and focus on the data you care about most, select the collection containing the models and metrics it should be able to use to create queries.

If you're embedding the Metabot component in an app, you can specify a different collection that embedded Metabot is allowed to use for creating queries.

## Chat preview

You can check out a [demo of the AI chat component](https://embedded-analytics-sdk-demo.metabase.com/admin/analytics/new/ask-metabot) on our Shoppy demo site.

## Example

```typescript

```

## Props



## API reference

- [Component](./api/MetabotQuestion)
- [Props](./api/MetabotQuestionProps)

## Setting up AI chat

To configure your embedded AI chat in your Metabase:

1. Click the **grid** icon in the upper right.
2. Select **Admin**.
3. Click the **AI** tab.
4. In the left sidebar, click **Embedded Metabot**.

When embedding the Metabot component in your app, you should specify a collection that embedded Metabot is allowed to use for creating queries. Embedded Metabot will only have access to that collection.

For tips and more, see [Metabot settings](../../ai/settings).

## Layout

Use the `layout` prop to specify which layout to use for the Metabot component:

- `auto` (default): Metabot uses the `stacked` layout on mobile screens, and a `sidebar` layout on larger screens.
- `stacked`: the question visualization stacks on top of the chat interface.
- `sidebar`: the question visualization appears to the left of the chat interface, which is on a sidebar on the right.

## Building custom AI chat UIs with `useMetabot`

If `MetabotQuestion`'s built-in layouts don't fit your app, use the `useMetabot` hook to read Metabot's conversation state directly and render your own UI. The hook gives you the messages, the chart the agent most recently produced, processing and error state, and actions to submit, cancel, retry, or reset the conversation.

### AI chat with inline charts

![AI chat inline charts](../images/ai-chat-inline-chart.png)

When an agent responds, the message can contain a `Chart` component. You can walk the agent's messages and render charts inline alongside the chat transcript:

```typescript

```

### AI chat with dedicated chart panel

![AI chat dedicated chart](../images/ai-chat-dedicated-chart.png)

The `CurrentChart` component is bound to the latest chart the agent produced. Render `CurrentChart` once, and it will swap in new charts as the agent creates them. You'll want to filter chart messages out of the transcript so they don't render twice:

```typescript

```

### Notes on `useMetabot`

- **Guard against null while waiting for the SDK bundle**: `useMetabot` returns `null` until the SDK bundle has loaded and `` has mounted. Always guard before use. If you don't guard it, the first render will throw `Cannot read properties of null` when you reach for `metabot.messages`, `metabot.submitMessage`, etc., because the SDK ships its Metabot internals via a code-split chunk that isn't available synchronously.
- **Bring your own Markdown renderer**: `MetabotQuestion` renders agent text messages internally, including markdown formatting, transcript scrolling, and input styling. The `useMetabot` hook hands you the raw conversation state, which means you own the rendering. In particular, agent text messages (`message.type === 'text'`) contain **markdown**: links, bold, lists, inline code. The snippets above render `message.message` as plain text for brevity, but production usage should pass the text through a markdown renderer (`react-markdown`, `markdown-to-jsx`, or your own) so links and formatting display correctly.
- **Strip links returned by the agent**: the agent text may include links pointing back to the host Metabase (like a link to a chart it created). Those links require an authenticated Metabase session, so people won't be able to view the links.

---

# Modular embedding SDK - collections



## Embedding a collection browser

You can embed Metabase's collection browser so that people can explore items in your Metabase from your application.

### `CollectionBrowser`

#### API Reference

- [Component](./api/CollectionBrowser)
- [Props](./api/CollectionBrowserProps)

#### Example

```tsx

```

#### Props



## Hide the collection picker and hard code the collection you want people to save stuff to

With static questions, you set a specific collection as the collection people can save items to, so that they don't have to bother picking a collection. To hard-code a collection:

1. Set `isSaveEnabled` to true.
2. Set `targetCollection` to the collection ID you want people to save items to.

For more options, see [Question props](./questions).

---

# Modular embedding SDK - config



## Passing a configuration object to `MetabaseProvider`

To use the SDK in your app, you need to import the `MetabaseProvider` component and provide it with an `authConfig` object.

### `MetabaseProvider`

A component that configures the SDK and provides the Metabase SDK's context and theme.

To pass a theme, use `defineMetabaseTheme`. See [Reuse a saved theme in the SDK](../appearance#reuse-a-saved-theme-in-the-sdk).

#### API Reference

- [Component](./api/MetabaseProvider)
- [Props](./api/MetabaseProviderProps)

#### Example

```typescript

```

#### Props



## Custom visualizations

The SDK can render [custom visualizations](../../questions/visualizations/custom). To allow custom visualizations in the embed, pass an allowlist of the custom visualizations to the `allowedCustomVisualizations` prop on `MetabaseProvider`:

```typescript

```

Only the custom visualizations you list will load. Each entry is the visualization's name (the manifest `name`, which you can find under **Admin** > **Settings** > **Custom visualizations** > **Manage visualizations**), prefixed with `custom:`. For example, a custom visualization named `Calendar Heatmap` becomes `"custom:Calendar Heatmap"`. Names are case-sensitive, so `"custom:calendar heatmap"` won't match a visualization named `Calendar Heatmap`.

Omitting the prop, or passing an empty array, turns off custom visualizations. Cards that use a custom visualization will fall back to the default visualization for the query's results. If you allowlist a name that doesn't match an installed custom visualization, the SDK logs a warning to the console and falls back to the default visualization.


For security, the SDK runs each custom visualization's code in an isolated sandbox, so a visualization can't reach your app or make network requests. The sandbox doesn't block passive image loads, though. A visualization can still trigger outbound requests through `` tags or CSS `url()`. To limit where custom visualizations can load images from, set a Content Security Policy with an `img-src` allowlist in your app (the core Metabase app does this with [Restrict image domains](../../configuring-metabase/settings#restrict-image-domains)). [Only add visualizations you trust](../../questions/visualizations/custom#only-add-visualizations-you-trust).

## Global event handlers

You can listen for events by defining the `eventHandlers` prop for `MetabaseProvider`.

### `SdkEventHandlersConfig`

Accepts an object where each key is an event type and the corresponding value is the event handler function.

#### API Reference

- [Type](./api/SdkEventHandlersConfig)

#### Example

```typescript

```

#### Props



## Reloading Metabase components

In case you need to reload a Metabase component, for example, your users modify your application data and that data is used to render a question in Metabase. If you embed this question and want to force Metabase to reload the question to show the latest data, you can do so by using the `key` prop to force a component to reload.

```typescript

```

---

# Modular embedding SDK - dashboards



You can embed an interactive, editable, or static dashboard.

**Keep in mind that embedding multiple instances of dashboards on the same page is not yet supported.**

## Embedding a dashboard

You can embed a dashboard using one of the dashboard components:

### `StaticDashboard`

A lightweight dashboard component. Use this component when you want to display results without letting people interact with the data.

#### API Reference

- [Component](./api/StaticDashboard)
- [Props](./api/StaticDashboardProps)

#### Props



### `InteractiveDashboard`

A dashboard component with drill downs, click behaviors, and the ability to view and click into questions. Use this component when you want to allow people to explore their data.

#### API Reference

- [Component](./api/InteractiveDashboard)
- [Props](./api/InteractiveDashboardProps)

#### Props



### `EditableDashboard`

A dashboard component with the features available in the `InteractiveDashboard` component, as well as the ability to add and update questions, layout, and content within your dashboard. Use this component when you want to give people the ability to modify your dashboards, for example in an admin panel in your app.

#### API Reference

- [Component](./api/EditableDashboard)
- [Props](./api/EditableDashboardProps)

#### Props



## Example embedded dashboard with `InteractiveDashboard` component

```typescript

```

## Pass parameter values to a dashboard

See [Modular embedding parameters](../parameters#pass-parameter-values-to-a-dashboard).

## Customizing dashboard height

By default, dashboard components take full page height (100vh). You can override this with custom styles passed via `style` or `className` props.

```tsx

```

## Customizing drill-through question layout

When drilling through or clicking on a question card in the dashboard, you will be taken to the question view. By default, the question is shown in the [default layout](./questions#customizing-interactive-questions) for interactive questions.

To customize the question layout, pass a `renderDrillThroughQuestion` prop to the `InteractiveDashboard` component, with the custom view as the child component.

```typescript



```

The questionView prop accepts a React component that will be rendered in the question view, which you can build with namespaced components within the `InteractiveQuestion` component. See [customizing interactive questions](./questions#customizing-interactive-questions) for an example layout.

## Dashboard plugins

### `dashboardCardMenu`

This plugin allows you to add, remove, and modify the custom actions on the overflow menu of dashboard cards. The plugin appears as a dropdown menu on the top right corner of the card.

The plugin's default configuration looks like this:

```typescript

```

`dashboardCardMenu`: can be used in the InteractiveDashboard like this:

```typescript

```

#### Enabling/disabling default actions

To remove the download button from the dashcard menu, set `withDownloads` to `false`. To remove the edit link from the dashcard menu, set `withEditLink` to `false`.

```typescript

```

#### Adding custom actions to the existing menu:

You can add custom actions to the dashcard menu by adding an object to the `customItems` array. Each element can either be an object or a function that takes in the dashcard's question, and outputs a list of custom items in the form of:

```typescript

```

Here's an example:

```typescript

```

#### Replacing the existing menu with your own component

If you want to replace the existing menu with your own component, you can do so by providing a function that returns a React component. This function also can receive the question as an argument.

```typescript

```

### `mapQuestionClickActions`

You can customize what happens when people click on a data point on a dashboard with the `mapQuestionClickActions` plugin. See [mapQuestionClickActions](./questions#mapquestionclickactions).

## Creating dashboards

Creating a dashboard could be done with `useCreateDashboardApi` hook or `CreateDashboardModal` component.

### `useCreateDashboardApi`

Use this hook if you'd like to have total control over the UI and settings.

Until the SDK is fully loaded and initialized, the hook returns `null`.

#### API Reference

- [Hook](./api/useCreateDashboardApi)
- [Options](./api/CreateDashboardValues)

#### Example

```typescript

```

#### Options



### `CreateDashboardModal`

#### API Reference

- [Component](./api/CreateDashboardModal)
- [Props](./api/CreateDashboardModalProps)

#### Example

```typescript

```

#### Props

---

# Modular embedding SDK



With the modular embedding SDK, you can embed individual Metabase components with React (like standalone charts, dashboards, the query builder, and more). You can manage access and interactivity per component, and you have advanced customization for seamless styling.

## Example apps built with the modular embedding SDK

To give you an idea of what's possible with the SDK, we've put together example sites at [metaba.se/sdk-demo](https://metaba.se/sdk-demo). Navigate between different shop websites. Check them out and poke around their products and analytics sections, as well as the New Question and New Dashboard options.

![Pug and play example app built with modular embedding SDK](../images/pug-and-play.png)

Here's the [Shoppy source code](https://github.com/metabase/shoppy).

## Modular embedding SDK playground

![Modular embedding SDK playground](../images/embedding-sdk-playground.png)

Try out the SDK in the [Modular embedding SDK playground](https://sdk-playground.metabase.com/), no setup required. The playground lets you experiment with components, theming, and interactivity in your browser.

## Modular embedding SDK prerequisites

- React application using React 18 or React 19.
- Nodejs 20.x or higher.
- Metabase version 1.52 or higher.

## Quickstarts

The best way to get started with Modular embedding SDK depends on what you already have:

- You have an app and a Metabase instance: go to [main quickstart](./quickstart)
- You have an app but no Metabase: go to [quickstart with CLI](./quickstart-cli)
- You don't have an app: go to [quickstart with a sample React app](./quickstart-with-sample-app)

## Installation

To use the SDK, you'll need to enable the SDK in Metabase, and install the SDK in your React app.

### Enable the SDK in Metabase

1. Enable the Modular embedding SDK by going to **Admin > Embedding**.
2. Toggle on **Modular embedding SDK**.
3. In **Cross-Origin Resource Sharing (CORS)**, enter the origins for your website or app where you want to allow SDK embedding, separated by a space. Localhost is automatically included.

### Install the SDK in your React application

Install the SDK Package matching your Metabase major with the `@{major}-stable` dist-tag, so the package's TypeScript types and exported components stay in sync with your instance's SDK Bundle:

```bash
npm install @metabase/embedding-sdk-react@60-stable
```

or with Yarn:

```bash
yarn add @metabase/embedding-sdk-react@60-stable
```

On **Metabase 56 or earlier**, the SDK major _must_ match your Metabase major. On **Metabase 57 and later**, you can also install without a dist-tag to track the latest published SDK major.

See [SDK versions](./version) for more on compatibility.

### Resolving `@types/react` version mismatches

In rare scenarios, the modular embedding SDK and your application may use different major versions of `@types/react`, causing TypeScript conflicts.

To enforce a single `@types/react` version across all dependencies, add an `overrides` (npm) or `resolutions` (Yarn) section to your `package.json` and specify the `@types/react` version your application uses.

#### npm set @types/react version

```json
{
  "overrides": {
    "@types/react": "..."
  }
}
```

#### Yarn set @types/react version

```json
{
  "resolutions": {
    "@types/react": "..."
  }
}
```

## Architecture

Starting with Metabase 57, the SDK consists of two parts:

- **SDK Package** – The `@metabase/embedding-sdk-react` npm package is a lightweight bootstrapper library. Its primary purpose is to load and run the main SDK Bundle code.
- **SDK Bundle** – The full SDK code, served directly from your self-hosted Metabase instance or Metabase Cloud, and it's a part of Metabase. This ensures that the main SDK code is always compatible with its corresponding Metabase instance.

## Developing with the modular embedding SDK

Start with one of the quickstarts, then see these pages for more info on components, theming, and more.

- [Authentication](../authentication)
- [Questions](./questions)
- [AI chat](./ai-chat)
- [Dashboards](./dashboards)
- [Actions](./actions)
- [Appearance](../appearance)
- [Collections](./collections)
- [Plugins](./plugins)
- [Config](./config)
- [Versioning](./version)
- [Notes on Next.js](./next-js)

## Modular embedding SDK source code

You can find the [Modular embedding SDK source code in the Metabase repo](https://github.com/metabase/metabase/tree/master/enterprise/frontend/src/embedding-sdk).

## Modular embedding SDK on npm

Check out the Metabase Modular embedding SDK on npm: [metaba.se/sdk-npm](https://metaba.se/sdk-npm).

## SDK limitations

The SDK doesn't support:

- Verified content
- Official collections
- Dashboard link cards
- Server-side rendering (SSR)

Other limitations:

- You can only have one dashboard per application page. You can, however, embed multiple questions on the same app page, or use [dashboard tabs](../../dashboards/introduction#dashboard-tabs) to create multiple different card layouts on one dashboard.
- If you have Leaflet 1.x as a dependency in your app, you may run into compatibility issues. You can try using Leaflet 2.x instead.

## Issues, feature requests and support

[Bugs](https://github.com/metabase/metabase/issues/?q=is%3Aissue%20state%3Aopen%20label%3AType%3ABug%20label%3AEmbedding%2FSDK) and [feature requests](https://github.com/metabase/metabase/issues/?q=is%3Aissue%20state%3Aopen%20label%3AEmbedding%2FSDK%20label%3A%22Type%3ANew%20Feature%22) are tracked on GitHub.

You can upvote an existing feature request by leaving a thumbs up emoji reaction on the issue. Feel free to leave comments with context that could be useful. [Read more](/docs/latest/troubleshooting-guide/requesting-new-features).

Before creating new issues, please make sure an issue for your problem or feature request doesn't already exist.
To seek help:

- Paid customers can contact our success team through the usual channels.
- People using the open-source edition can post on our [discussion forums](https://discourse.metabase.com/).

---

# Using the modular embedding SDK with Next.js





Some notes on using the modular embedding SDK with [Next.js](https://nextjs.org/). The SDK is tested to work with Next.js 14, although it may work with other versions.

See a [sample Next.js app that uses the SDK](https://github.com/metabase/metabase-nextjs-sdk-embedding-sample).

## SDK components with Server Side Rendering (SSR) or React Server Components

As of modular embedding SDK v57, SDK components automatically skip server-side rendering (SSR) and render only on the client.

### Compatibility layer for Server Side Rendering (SSR) (DEPRECATED)

As of modular embedding SDK 57, the compatibility layer for server-side rendering (SSR) is deprecated and no longer required. If you use the compatibility layer, change your imports from `@metabase/embedding-sdk-react/next` to `@metabase/embedding-sdk-react`.

## Handling authentication

App Router and Pages Router have different ways to define API routes. If you want to authenticate users from your server with JWT, you can follow the instructions below. But if you want to authenticate with API keys for local development, see [Authenticating locally with API keys](../authentication#authenticating-locally-with-api-keys).

### Using App Router

You can create a Route handler that signs people in to Metabase.

Create a new `route.ts` file in your `app/*` directory, for example `app/sso/metabase/route.ts` that corresponds to an endpoint at /sso/metabase. This route handler should generate a JWT for the authenticated user and return the token in a JSON object with the shape `{ jwt: string }`.

```typescript



```

Then, pass this `authConfig` to `MetabaseProvider`

```typescript

```

### Using Pages Router

You can create an API route that signs people in to Metabase.

Create a new `metabase.ts` file in your `pages/api/*` directory, for example `pages/api/sso/metabase.ts` that corresponds to an endpoint at /api/sso/metabase. This API route should generate a JWT for the authenticated user and return the token in a JSON object with the shape `{ jwt: string }`.

```typescript



```

Then, pass this `authConfig` to `MetabaseProvider`

```ts

```

---

# Modular embedding SDK - plugins



The Metabase modular embedding SDK supports plugins to customize the behavior of components. These plugins can be used in a global context or on a per-component basis.

## Plugin scope

### Global plugins

To use a plugin globally, add the plugin to the `MetabaseProvider`'s `pluginsConfig` prop:

```typescript

```

### Component plugins

To use a plugin on a per-component basis, pass the plugin as a prop to the component:

```typescript

```

See docs for specific components:

- [Interactive question plugins](./questions#interactive-question-plugins)
- [Dashboard plugins](./dashboards#dashboard-plugins)

## Global plugins

### `mapQuestionClickActions`

The plugin `mapQuestionClickActions` lets you customize what happens when people click on a data point on a dashboard or chart. `mapQuestionClickActions` can be used globally, or on component level.

See [`mapQuestionClickActions` plugin](./questions#mapquestionclickactions) for more information and examples.

### `handleLink`

To customize what happens when people click a link in your embedded questions and dashboards, use the global plugin `handleLink`:

```typescript

```

By default, links open in a new tab. Use `handleLink` to intercept link clicks — for example, to open a URL in a modal or navigate within your app using your router.

The function receives a URL string. Return `{ handled: true }` to prevent default navigation, or `{ handled: false }` to open the link in a new tab.

The plugin `handleLink` can only be used [globally](#plugin-scope) on provider level. `handleLink` is also available in [modular embedding](../modular-embedding#page-level-config) via `pluginsConfig` in `defineMetabaseConfig`, with the same API.

To create clickable links in your table columns, set the column's formatting to [display as link](../../data-modeling/formatting#display-as).

### `getNoDataIllustration` and `getNoObjectIllustration`

By default, Metabase displays a sailboat image when a query returns no results. To use a different image, you can use `getNoDataIllustration` and `getNoObjectIllustration` plugins which can accept a custom base64-encoded image:

```typescript

```

The plugins `getNoDataIllustration` and `getNoObjectIllustration` can only be used [globally](#plugin-scope) on provider level.

## Further reading

- [Interactive question plugins](./questions#interactive-question-plugins)
- [Dashboard plugins](./dashboards#dashboard-plugins)

---

# Modular embedding SDK - questions



There are different ways you can embed questions:

- [Static question](#staticquestion). Embeds a chart. Clicking on the chart doesn't do anything.
- [Interactive question](#interactivequestion). Create new questions or edit existing ones with the visual query builder or SQL editor. Clicking on the chart gives you the drill-through menu.

## Embedding a question

You can embed a question using one of the question components:

### `StaticQuestion`

A lightweight question component. Use this component when you want to display results without letting people interact with the data.

![Static question](../images/static-question.png)

The component has a default height, which can be customized by using the `height` prop. To inherit the height from the parent container, you can pass `100%` to the height prop.

#### API Reference

- [Component](./api/StaticQuestion)
- [Props](./api/StaticQuestionProps)

#### Example

```typescript

```

#### Props



### `InteractiveQuestion`

Use this component when you want to allow people to explore their data and customize question layout.

![Interactive question](../images/interactive-question.png)

#### API Reference

- [Component](./api/InteractiveQuestion)
- [Props](./api/InteractiveQuestionProps)

#### Example

```typescript

```

#### Props



## Pass parameters to SQL questions

See [Modular embedding parameters](../parameters#pass-parameters-to-sql-questions).

## Enable alerts on embedded questions

You can let people set up [alerts](../../questions/alerts) on embedded questions by passing the `withAlerts` prop to `StaticQuestion` or `InteractiveQuestion`. Alerts require [email setup](../../configuring-metabase/email), and the question must be saved (not a new question).

Alerts created in an embedded context only send to the logged-in user and exclude links to Metabase items.

```tsx

```

## Questions with natural language

See [AI chat](./ai-chat).

## Customizing interactive questions

By default, the modular embedding SDK provides a default layout for interactive questions that allows you to view your questions, apply filters and aggregations, and access functionality within the query builder.

Here's an example of using the `InteractiveQuestion` component with its default layout:

```typescript

```

To customize the layout, use namespaced components within the `InteractiveQuestion` component. For example:

```typescript

```

### Interactive question components

These components are available via the `InteractiveQuestion` namespace (e.g., ``).

#### API Reference:

- [InteractiveQuestion.BackButton](./api/InteractiveQuestion#backbutton)
- [InteractiveQuestion.Breakout](./api/InteractiveQuestion#breakout)
- [InteractiveQuestion.BreakoutDropdown](./api/InteractiveQuestion#breakoutdropdown)
- [InteractiveQuestion.ChartTypeDropdown](./api/InteractiveQuestion#charttypedropdown)
- [InteractiveQuestion.ChartTypeSelector](./api/InteractiveQuestion#charttypeselector)
- [InteractiveQuestion.Editor](./api/InteractiveQuestion#editor)
- [InteractiveQuestion.EditorButton](./api/InteractiveQuestion#editorbutton)
- [InteractiveQuestion.Filter](./api/InteractiveQuestion#filter)
- [InteractiveQuestion.FilterDropdown](./api/InteractiveQuestion#filterdropdown)
- [InteractiveQuestion.QuestionSettings](./api/InteractiveQuestion#questionsettings)
- [InteractiveQuestion.QuestionSettingsDropdown](./api/InteractiveQuestion#questionsettingsdropdown)
- [InteractiveQuestion.QuestionVisualization](./api/InteractiveQuestion#questionvisualization)
- [InteractiveQuestion.ResetButton](./api/InteractiveQuestion#resetbutton)
- [InteractiveQuestion.SaveButton](./api/InteractiveQuestion#savebutton)
- [InteractiveQuestion.SaveQuestionForm](./api/InteractiveQuestion#savequestionform)
- [InteractiveQuestion.Summarize](./api/InteractiveQuestion#summarize)
- [InteractiveQuestion.SummarizeDropdown](./api/InteractiveQuestion#summarizedropdown)
- [InteractiveQuestion.DownloadWidget](./api/InteractiveQuestion#downloadwidget)
- [InteractiveQuestion.DownloadWidgetDropdown](./api/InteractiveQuestion#downloadwidgetdropdown)
- [InteractiveQuestion.Title](./api/InteractiveQuestion#title)

## Interactive question plugins

You can use [plugins](./plugins) to add custom functionality to your questions.

### `mapQuestionClickActions`

When people click on a data point in the embedded interactive chart, Metabase shows them a menu of actions by default. The plugin `mapQuestionClickActions` allows you to customize this behavior. You can choose to:

- Open the default Metabase menu.
- Add custom actions to that click-through menu.
- Perform immediate action without opening a menu.

Use `mapQuestionClickActions` globally at the provider level, or on individual `InteractiveQuestion` or `InteractiveDashboard` components. For more on provider scope, see [Plugins](./plugins)

The example below shows all the options for click action behavior. This example will:

- Open a menu with custom actions when "Last Name" column is clicked.
- Perform an immediate action (show an alert) when the "Plan" column is clicked.
- Shows the default menu (available as `clickActions`) in all other cases.

The behavior is determined by what `mapQuestionClickActions` returns: array of actions to open a menu, or a single action to trigger an immediate action.

```typescript

```

You can also customize the appearance of custom actions in the click menu. The example below shows an example of a click menu with default actions, a custom action, and a custom action with customized appearance:

```typescript

```

## Prevent people from saving changes to an `InteractiveQuestion`

To prevent people from saving changes to an interactive question, or from saving changes as a new question, you can set `isSaveEnabled={false}`:

```tsx

```

## Create and edit questions with the query builder

![Query builder](../images/query-builder.png)

The `InteractiveQuestion` component supports three modes:

- `questionId="new"` — opens the visual query builder for creating new questions.
- `questionId="new-native"` — opens the SQL editor for creating new questions.
- A numeric `questionId` (e.g., `questionId={42}`) — embeds an existing question. People can click the edit button to modify it, which opens the appropriate editor (visual or SQL) based on the question type.

### Embed the visual query builder

To embed the query builder for creating new questions, pass `questionId="new"` to the `InteractiveQuestion` component. You can use the [`children` prop](#customizing-interactive-questions) to customize the layout.

```tsx

```

### Embed the SQL editor

To embed the SQL editor for creating new questions, pass `questionId="new-native"` to the `InteractiveQuestion` component:

```tsx

```

To customize the question editor's layout, use the `InteractiveQuestion` component [directly with a custom `children` prop](#customizing-interactive-questions).

---

# Modular embedding SDK - CLI quickstart



We built a single command to spin up a Metabase and help you get an embedded dashboard in your app. This setup with API keys won't work in production; it's only intended for you to quickly try out the SDK on your local machine. A production setup requires a Pro/Enterprise license, and SSO with JWT.

## Prerequisites

- Docker (should be up and running on your machine)
- [Node.js 20.x LTS](https://nodejs.org/en) or higher.
- License (Optional - only if you want to try out multi-tenancy).
- Database (you can connect to your app's database).

You don't need a running Metabase; the tool will set up a Metabase for you on Docker.

## The quickstart CLI command

Change into your React application and run:

```sh
npx @metabase/embedding-sdk-react@latest start
```

The CLI tool will walk you through the setup. There are a fair number of pieces to put together, so here's an overview of what the command does:

- [Prereq check](#prereq-check)
- [Database connection (optional)](#database-connection-optional)
- [Metabase setup](#metabase-setup)
- [Permissions setup with multi-tenancy (optional)](#permissions-setup-with-multi-tenancy-optional)
- [React components setup](#react-components-setup)
- [Behold: Metabase is embedded in your app](#behold-metabase-is-embedded-in-your-app)

## Prereq check

The tool will check for the following:

- You've run the command in the top-level directory of your React application.
- You've installed the SDK (if you haven't, the CLI will install the SDK for you and add it as a dependency in your `package.json`).
- You have Docker up and running on your machine.

## Database connection (optional)

The tool will ask if you have a database to connect to. Use the arrow keys to select Yes or No. The tool will use this database to generate an embedded dashboard.

If you answer no, the script will use the Sample Database that ships with Metabase to create a dashboard to embed.

If you select Yes, the tool will prompt you to connect to a database. Pick your database's engine. You'll need to provide database's host, port, username, and password. The tool will connect to the database, and prompt you to select tables from your database to embed. Pick 1-3 tables. If you want to see multi-tenancy in action, pick a table with user IDs in it. Metabase will X-ray these tables to create a dashboard to embed.

## Metabase setup

The tool will ask you for an email address to create the first admin account in Metabase. Doesn't have to be a real email address (the tool doesn't set up a SMTP server); the email address is just required for logging in to the Metabase that the tool will set up.

Next, the tool will spin up a Metabase on Docker. This takes a bit. To see the Docker container's status, use the `docker ps` command. Or use the time to reflect on good choices you've made recently.

Once Metabase is up and running, the tool will create an admin user with the email you provided, and generate an [API key](../../people-and-groups/api-keys) for that Metabase.

The tool will then prompt you to pick 1-3 tables to embed. You can press  to select,  to toggle all,  to invert selection, and  to proceed.

## Permissions setup with multi-tenancy (optional)

If you have a Pro/EE license, the tool can set up permissions. To get a license, sign up for a [free trial of self-hosted Metabase Pro](/pricing/).

If you opted to set up multi-tenancy and connected to your own database, the tool will ask you for the column you want to use to restrict the table (e.g., a user ID column). Metabase will [set row-level security](../../permissions/row-and-column-security) for that table based on the values in that column.

The tool will also set up a mock Express server to handle the JWTs. The tool will ask you where it should save the server code (default: `./mock-server`). It'll install the server's dependencies with `npm install`.

You'll need to start the mock server in another terminal session. Change into the mock server's directory and run:

```sh
npm run start
```

## React components setup

Next, the tool will generate example React components files. By default, the tool will save them in `./src/components/metabase` in your React app, though the tool will prompt you to save them to a different directory if you want (e.g., `./src/analytics`).
It generates a couple of demo components for you to try out theming and user switching:

- `AnalyticsDashboard` - a dashboard component that embeds a Metabase dashboard.
- `AnalyticsPage` - a page that embeds a dashboard with a wrapped provider. In a real application, you must add the `MetabaseProvider` separately to your app's root `App` component (or where you would've added your other providers).
- `ThemeSwitcher` - switch between light and dark themes.
- `UserSwitcher` - switch between fake users.
- `AnalyticsProvider` - a provider that adds the demo state for the example theme switcher and user switcher components.
- `EmbeddingProvider` - a provider that wraps the `MetabaseProvider` with demo themes and auth configuration.

You can delete these files once you've played around with the tool, and are ready to setup your own theming and user management.

## Add the Metabase/React components to your app

You'll need to add the Metabase/React components to your app. Add an import to your client app, like so:

```jsx

```

Make sure the `from` path is valid (depending on your app, you may need to move the components to a new directory).

Then you'll need to add the `` component to a page in your app. Something like:

```jsx

```

## Behold: Metabase is embedded in your app

Start your app, and visit the page where you added the `` component. You should see an embedded dashboard.

You can also check out the Metabase the tool set up. The Metabase should be running at `http://localhost:3366`. You can find your login credentials at `METABASE_LOGIN.json`.

## Further reading

- [Quickstart with sample app and JWT](./quickstart)

---

# Modular embedding SDK - quickstart with sample app



This guide sets up the modular embedding SDK with a [sample React app](https://github.com/metabase/metabase-nodejs-react-sdk-embedding-sample/tree/-stable), but you can follow along with your own application.



## Prerequisites

- [Node.js 20.x LTS or higher](https://nodejs.org/en) (for the sample application).
- [Metabase version v1.52 or higher](https://github.com/metabase/metabase/releases).
- [A Metabase Pro or Enterprise license](/pricing/) (If you don't have a license, check out [this quickstart](./quickstart) that lacks the paid JWT SSO setup.)
- (Optional): [Docker](https://www.docker.com/)

## Clone the sample app repo

1. Clone the [sample React app](https://github.com/metabase/metabase-nodejs-react-sdk-embedding-sample/tree/-stable).

```bash
git clone git@github.com:metabase/metabase-nodejs-react-sdk-embedding-sample.git
```

2. Check out the branch in the [metabase-nodejs-react-sdk-embedding-sample](https://github.com/metabase/metabase-nodejs-react-sdk-embedding-sample/tree/-stable) repo that corresponds to your Metabase version.

```bash
git checkout -stable
```

E.g., if you're running Metabase 1.57 make sure the sample app repo is on the `57-stable` branch. You can find your Metabase version in the Metabase UI by clicking the **grid icon** in the upper right, selecting **Help**, then choosing **About Metabase**.

## Two ways to set up the sample app with Metabase

- [Quick setup with Docker](#quick-setup-with-docker) (includes a sample Metabase)
- [Walkthrough setup](#walkthrough-setup) (bring your own Metabase, or spin up a new one)

## Quick setup with Docker

This setup will run a Docker container with the sample app and a sample Metabase.

1. Copy the environment template file:

   In the cloned directory, run:

```bash
cp .env.docker.example .env.docker
```

2. In the `.env.docker` file, replace `` with your premium embedding token.

3. In the top-level directory, run:

```bash
yarn start
```

This script will:

- Pull a Metabase Docker image and run it in a container.
- Set up [JWT SSO in Metabase](../../people-and-groups/authenticating-with-jwt)
- Build and run the sample application with an embedded question.

4. The app will start on [http://localhost:4400](http://localhost:4400).

That's it!

If you want to log in to the sample Metabase this command set up, visit [http://localhost:4300](http://localhost:4300). You can log in with email and password as Rene Descartes:

- email: rene@example.com
- password: foobarbaz

## Walkthrough setup

We're going to do some setup in Metabase, and then in the sample application. You can also bring your own Metabase, in which case you can skip the installation step.

Here's a quick overview of what you'll be doing:

### Set up Metabase for embedding

1. [Install Metabase Enterprise Edition](#install-metabase-enterprise-edition) (if you haven't already)
2. [Activate your license](#activate-your-license)
3. [Enable embedding](#enable-embedding-in-metabase)
4. [Enable SSO with JWT](#enable-sso-with-jwt)

### Start up the sample application

5. [Set up the application environment](#set-up-the-application-environment).
6. [Run the app server](#set-up-the-application-server) to handle authentication with JWT and serve the embedded Metabase components.
7. [Run the client application](#set-up-the-client-application) that will contain Metabase components built with the SDK.

And then fiddle around with styling.

Let's go.

## Install Metabase Enterprise Edition

You can run Metabase Pro on a Cloud plan with a [free trial](/pricing/).

Or run it locally. Here's a [docker](../../installation-and-operation/running-metabase-on-docker) one-liner:

```bash
docker run -d -p 3000:3000 --name metabase metabase/metabase-enterprise:latest
```

You can also [download the JAR](https://downloads.metabase.com/enterprise/latest/metabase.jar), and run it like so:

```bash
java --add-opens java.base/java.nio=ALL-UNNAMED -jar metabase.jar
```

By default, Metabase will run at `http://localhost:3000`.

If you get stuck, check out our [installation docs](../../installation-and-operation/installing-metabase).

## Activate your license

To enable SSO with JWT when self-hosting, you'll need to [activate your license](../../installation-and-operation/activating-the-enterprise-edition). Metabase Pro plans on Cloud take care of this for you.

## Enable embedding in Metabase

From any Metabase page, click the **grid** icon in the upper right and select **Admin** > **Embedding**.

Turn on:

- Modular embedding SDK

Otherwise, this whole thing is hopeless.

## Enable SSO with JWT

We'll also need to update our JWT Provider URI in Metabase. By default, this URI is where the SDK will redirect login requests.

From any Metabase page, click the **grid** icon in the upper right and select **Admin** > **Settings** > **Authentication**.

On the card that says **JWT**, click the **Setup** button.

### JWT Identity provider URI

In **JWT IDENTITY PROVIDER URI** field, paste

```txt
http://localhost:9090/sso/metabase
```

Or substitute your Cloud URL for `http://localhost`.

### String used by the JWT signing key

Click the **Generate key** button.

Copy the key and paste it in your `.env` file into the env var `METABASE_JWT_SHARED_SECRET`.

The application server will use this key to sign tokens so Metabase knows the application's requests for content are authorized.

## Save and enable JWT

Be sure to hit the **Save and enable** button, or all is void.

## Set up the sample application

## Set up the application environment

[Clone the sample app](#clone-the-sample-app-repo) and `cd` into it.

In the sample app's main directory, copy the `.env.example` template to `.env`.

```sh
cp .env.example .env
```

In `.env`, make sure `VITE_METABASE_INSTANCE_URL` and `METABASE_INSTANCE_URL` point to your Metabase instance URL, e.g., `http://localhost:3000`.

Your `.env` will look something like:

```txt
# FRONTEND
CLIENT_PORT=3100
VITE_METABASE_INSTANCE_URL="http://localhost:3000"

# BACKEND
AUTH_PROVIDER_PORT=9090
METABASE_INSTANCE_URL="http://localhost:3000"
METABASE_JWT_SHARED_SECRET="TODO"
```

## Set up the application server

Change into the `server` directory:

```sh
cd server
```

Install packages:

```sh
npm install
```

Start the server:

```sh
npm start
```

## Set up the client application

In a different terminal, change into the `client` directory:

```sh
cd client
```

Install dependencies:

```sh
npm install
```

This command will install the [Metabase modular embedding SDK](https://www.npmjs.com/package/@metabase/embedding-sdk-react), in addition to the application's other dependencies.

You can also install a [different version of the SDK](./version). Just make sure that the major version of the SDK matches the major version of the Metabase you're using.

Start the client app:

```sh
npm start
```

Your browser should automatically open the app. By default, the app runs on [http://localhost:3100](http://localhost:3100).

## At this point, you should be up and running

In your app, you'll see an embedded `InteractiveQuestion` component.

```javascript

```

![Embedded Metabase components](../images/interactive-question-sample-app.png)

## Next steps

To style the components, try changing some of the `theme` options in the client app at `client/src/App.jsx`. For more on theming, check out [Appearance](../appearance).

---

# Modular embedding SDK - quickstart

This guide walks you through how to set up the Modular embedding SDK in your application with your Metabase using API keys.

This setup:

- Is only for evaluation (so you can see how the SDK works).
- Only works on localhost when developing your app (though your Metabase doesn't need to be running locally).
- Works with both the Enterprise and Open Source editions of Metabase, both self-hosted and on Metabase Cloud.

If you want to use the SDK in production, however, you'll also need to [set up JWT SSO authentication](../authentication), which requires a [Pro](https://store.metabase.com/checkout/embedding) or [Enterprise plan](/pricing/). To enable JWT SSO when you're self-hosting Metabase, you'll need to run the Enterprise Edition Docker image or JAR, and [activate your license](../../installation-and-operation/activating-the-enterprise-edition).

## Prerequisites

- [Metabase](https://github.com/metabase/metabase/releases) version 52 or higher (OSS or EE). See [Installing Metabase](../../installation-and-operation/installing-metabase).
- Make sure your [React version is compatible](./introduction#modular-embedding-sdk-prerequisites). (You could also use the [sample React app](https://github.com/metabase/metabase-nodejs-react-sdk-embedding-sample/tree/-stable).)

If you _don't_ have a Metabase up and running, check out the [Quickstart CLI](./quickstart-cli).

If you _don't_ want to use your own application code, check out our [quickstart with a sample app](./quickstart-with-sample-app).

## Overview

To embed a dashboard in your app using the SDK, you'll need to:

1. [Enable the SDK in Metabase](#1-enable-the-sdk-in-metabase)
2. [Create an API key in Metabase](#2-create-an-api-key-in-metabase)
3. [Install the SDK in your app](#3-install-the-sdk-in-your-app)
4. [Embed SDK components in your app](#4-embed-sdk-components-in-your-app)
5. [View your embedded Metabase dashboard](#5-view-your-embedded-metabase-dashboard)

## 1. Enable the SDK in Metabase

In Metabase, click the grid icon in the upper right and navigate to **Admin > Embedding > Modular** and enable the **SDK for React**.

## 2. Create an API key in Metabase

Still in the Admin console, go to **Settings > Authentication** and click on the **API keys** tab. [Create a new API key](../../people-and-groups/api-keys).

- Key name: "Modular embedding SDK" (just to make the key easy to identify).
- Group: select “Admin” (since this is only for local testing).

## 3. Install the SDK in your app

Install the `@{major}-stable` dist-tag matching your Metabase major, so the package's types and exported components in your client match your SDK bundle served from your Metabase. For Metabase 60:

Via npm:

```
npm install @metabase/embedding-sdk-react@60-stable
```

Via Yarn:

```
yarn add @metabase/embedding-sdk-react@60-stable
```

See [SDK versions](./version) for other install options.

## 4. Embed SDK components in your app

In your app, import the SDK components, like so:

```jsx

```

## 5. View your embedded Metabase dashboard

Run your app and visit the page with the embedded dashboard.

![Embedded example dashboard](../images/embedded-example-dashboard.png)

## Next steps

- Explore [theming to change the look and feel](../appearance).
- Continue by [setting up JWT SSO in Metabase and your app](../authentication) to sign people in, manage permissions, and deploy your app in production.

---

# Upgrading Metabase and the modular embedding SDK

Here's a basic overview of the steps you'll want to take when upgrading your SDK.

> **Using an AI coding agent?** The [agent skills pack](../ai-agent-resources) includes a skill that walks your agent through SDK upgrades step by step.

## 1. Read the release post and changelog for Metabase and the modular embedding SDK

- [Release posts](/releases) give a good overview of what's in each release, and call out breaking changes (which are rare).
- [Metabase changelog](/changelog) lists all Metabase and modular embedding SDK changes.
- [Modular embedding SDK changelog](https://github.com/metabase/metabase/blob/master/enterprise/frontend/src/embedding-sdk-package/CHANGELOG.md) lists changes specific to the SDK's `@metabase/embedding-sdk-react` package.

Check for any relevant changes, especially deprecations or breaking changes that require you to update your application's code. If there are deprecation changes, we'll have docs that'll walk you through what changes you'll need to make and why.

## 2. Test the upgrade

### Spin up the new version of Metabase for testing

You can do this locally or in a dev instance. If your testing setup involves a lot of test user accounts, getting a [development instance](../../installation-and-operation/development-instance) could be more cost-effective.

See [upgrading Metabase](../../installation-and-operation/upgrading-metabase).

### Upgrade the SDK with npm or Yarn

You'll want to test the changes locally first, as there may be breaking changes that require you to upgrade your application code.

Check out a new branch in your application and install the SDK that matches your Metabase major version. For Metabase 60.x:

```bash
npm install @metabase/embedding-sdk-react@60-stable
```

or with Yarn:

```bash
yarn add @metabase/embedding-sdk-react@60-stable
```

To track the latest published SDK major instead, install without a dist-tag (`npm install @metabase/embedding-sdk-react`).

See more on [SDK versions](./version).

### If there are deprecations or breaking changes, make the necessary changes to your application code

Deprecations or breaking changes are rare, but if you do need to make changes to your application code, we'll mention it in the [release notes](/releases) for the new major version and have docs that walk you through the changes.

Update or add tests for any application code changes that you make.

In most cases, a deprecated change becomes a breaking change in the release following its deprecation.
For example, if we plan to remove a prop from an SDK React component, we first mark it as **deprecated**, and then remove it in the next release.

### Deploy to your staging environment

Before deploying your app to your staging environment, make sure you've tested your app locally (manually, as well as running any automated tests).

If all goes well with your local tests, deploy to your staging environment. Check that the Metabase embeds in your staging app are still working as expected, and perform any other testing you normally do with your application with respect to your embedded analytics.

## 3. Deploy to production

If everything is working in staging, you're ready to deploy to production.

### Caching may delay the upgrade by up to a minute

This is intentional. After upgrading, Metabase may still serve the previous, cached version of the SDK Bundle for up to 60 seconds (`Cache-Control: public, max-age=60`). This short cache window helps ensure fast performance while still allowing updates to propagate quickly.

If you don’t see your changes immediately, clear your browser's cache or just wait a minute. After that, the SDK Package will load the newly deployed SDK Bundle.

### If your instance is pinned on Metabase Cloud, you'll need to request an upgrade

If you're on Metabase Cloud, and you've [pinned the version of your Metabase](./version#you-can-pin-instances-to-a-version-on-metabase-cloud), you'll need to request an upgrade by [contacting support](/help-premium).

We'll coordinate with you so that your instance is upgraded when you deploy the changes to your application.

---

# Modular embedding SDK - versions



## Metabase 57 and later

Starting with Metabase 57, the `@metabase/embedding-sdk-react` npm package loads the SDK Bundle from your Metabase.

Install the SDK Package matching your Metabase major with the `@{major}-stable` dist-tag, so the package's TypeScript types and exported components stay in sync with your instance's SDK Bundle:

```sh
npm install @metabase/embedding-sdk-react@60-stable
```

Installing without a dist-tag (`npm install @metabase/embedding-sdk-react`) still works. The bundle loads from your Metabase, but the package's types and exports will track the latest published SDK major, which may drift from your Metabase version.

## Metabase 56 and earlier

For Metabase 56 and earlier, the SDK major version must match the Metabase major version. Use the matching `@{major}-stable` dist-tag. For example, for Metabase 55:

```sh
npm install @metabase/embedding-sdk-react@55-stable
```

On Metabase 55 (`0.55.x`, `1.55.x`), _any_ 0.55.x release of `@metabase/embedding-sdk-react` will be compatible.

## Minimum SDK version

Version 52 is the minimum version supported for the Modular embedding SDK.

## You can pin instances to a version on Metabase Cloud

Metabase Cloud upgrades your instance automatically as new versions roll out. If you're using the SDK with Metabase Cloud, you may want to pin your version so you can upgrade manually.

On Metabase 56 or earlier, pinning also keeps the SDK Package and Metabase majors in lockstep.

### Manually pinning your instance version on Metabase Cloud

To manually pin your version of Metabase:

1. Go to **Admin > Embedding > Modular**.
2. Scroll to **Version pinning** and click **Request version pinning**.

This will open a mailto link to our support team.

---

# Securing embedded Metabase

## Securing embeds with authentication and authorization

There are two basic ways to secure stuff on the internet:

1. **Authentication** looks at _who_ someone is (using standards such as [JWT](../people-and-groups/authenticating-with-jwt) or [SAML](../people-and-groups/authenticating-with-saml)).
2. **Authorization** looks at _what_ someone has access to (using standards such as OAuth 2.0).

In this guide, we'll talk primarily about authentication.

## Public embedding

[Public embedding](public-links#public-embeds) doesn't involve any authentication or authorization. A public embed displays a public link with a unique string at the end, like this:

```plaintext
https://my-metabase.com/public/dashboard/184f819c-2c80-4b2d-80f8-26bffaae5d8b
```

The string (in this example: `184f819c-2c80-4b2d-80f8-26bffaae5d8b`) uniquely identifies your Metabase question or dashboard. Since public embeds don't do any authentication or authorization, anyone with the URL can view the data.

### Example: filters in public links don't secure data

So, how could someone exploit a public embed? Say we have a dashboard that displays Accounts data:

| Account ID | Plan    | Status   |
| ---------- | ------- | -------- |
| 1          | Basic   | Active   |
| 2          | Basic   | Active   |
| 3          | Basic   | Inactive |
| 4          | Premium | Inactive |
| 5          | Premium | Active   |

We want to add a "Status = Active" filter and display the dashboard's public link in an embed:

| Account ID | Plan    | Status |
| ---------- | ------- | ------ |
| 1          | Basic   | Active |
| 2          | Basic   | Active |
| 5          | Premium | Active |

To apply and hide the "Status = Active" filter, we'll add [query parameters](public-links#public-embed-parameters) to the end of the public link in our embed:

```plaintext
https://my-metabase.com/public/dashboard/184f819c-2c80-4b2d-80f8-26bffaae5d8b?status=active#hide_parameters=status
```

Even though we've hidden the filter from the embed, someone could take the public link used in the embed, and remove the query parameter `?status=active`:

```plaintext
https://my-metabase.com/public/dashboard/184f819c-2c80-4b2d-80f8-26bffaae5d8b
```

Loading the public link without the query parameter would remove the "Status = Active" filter from the data. The person would get access to the original Accounts data, including the rows with inactive accounts.

## Guest embeds are authorized with JWT

Guest embedding uses a [JWT authorization flow](#guest-embedding-with-jwt-authorization) to do two things:

- Sign resources (e.g., the URLs of charts or dashboards) to ensure that only your embedding application can ask for data from your Metabase.
- Sign parameters (e.g., dashboard filters) to prevent people from [changing the filters](#example-filters-in-public-links-dont-secure-data) and getting access to other data.

### Guest embeds don't have user sessions

Guest embeds don't authenticate people's identities on the Metabase side, so people can view a guest embed without creating a Metabase account.

Without a Metabase account, however, Metabase won't have a way to remember a user or their session, which means:

- Metabase [permissions](../permissions/introduction) and [row and column security](../permissions/row-and-column-security) won't work --- if you need to lock down sensitive data, you must set up [locked parameters](#example-securing-data-with-locked-parameters-on-a-guest-embed) for _each_ of your guest embeds.
- Any filter selections in a guest embed will reset once the signed JWT expires, _unless_ you configure [JWT refresh](./guest-embedding#refreshing-or-initializing-the-jwt-from-your-server) so the embed swaps in a fresh token without reloading.
- All guest embed usage will show up in [usage analytics](../usage-and-performance-tools/usage-analytics) under "External user".

## Security in guest embedding vs. modular and full app embedding

Guest embedding only guarantees authorized access to your Metabase data (you decide _what_ is accessible).

If you want to secure your guest embeds based on someone's identity (you decide _who_ gets access to _what_), you'll need to set up your own authentication flow and manually wire that up to [locked parameters](#example-sending-user-attributes-to-a-locked-parameter) on each of your guest embeds. Note that locked parameters are essentially filters, so you can only set up **row-level** restrictions in a guest embed.

If you want an easier way to embed different views of data for different customers (without allowing the customers to see each other’s data), learn how [Modular and full app embedding authenticates and authorizes people in one flow](#modular-and-full-app-embedding-auth-with-jwt-or-saml).

### Guest embedding with JWT authorization

![Guest embedding with JWT authorization.](./images/signed-embedding-jwt.png)

This diagram illustrates how an embed gets secured by a signed JWT:

1. **Visitor arrives**: your frontend gets a request to display a Metabase [embedding URL](./static-embedding#adding-the-embedding-url-to-your-website).
2. **Signed request**: your backend generates a Metabase embedding URL with a [signed JWT](./guest-embedding#how-guest-embedding-works). The signed JWT should encode any query [parameters](./static-embedding-parameters) you're using to filter your data.
3. **Response**: your Metabase backend returns data based on the query parameters encoded in the signed JWT.
4. **Success**: your frontend displays the embedded Metabase page with the correct data.
5. **(Optional) Refresh / Initialize**: if you've configured a [`guestEmbedProviderUri`](./guest-embedding#refreshing-or-initializing-the-jwt-from-your-server), the embed will call that endpoint you've set up for a fresh token the next time the embed needs to make a data request after the current token has expired (like when someone changes the filter value). The embed won't automatically fetch a new token. The endpoint can also serve the first token on load, so you can use the endpoint to revoke access by refusing to issue a new token.

### Example: securing data with locked parameters on a guest embed

In the [public embedding example](#example-filters-in-public-links-dont-secure-data), we showed you (perhaps unwisely) how someone could exploit a unique public link by editing its query parameters.

Let's go back to our Accounts example:

| Account ID | Plan    | Status   |
| ---------- | ------- | -------- |
| 1          | Basic   | Active   |
| 2          | Basic   | Active   |
| 3          | Basic   | Inactive |
| 4          | Premium | Inactive |
| 5          | Premium | Active   |

Remember, we can filter the data in a public embed by including a query parameter at the end of the embedding URL:

```plaintext
https://my-metabase.com/public/dashboard/184f819c-2c80-4b2d-80f8-26bffaae5d8b?status=active
```

| Account ID | Plan    | Status |
| ---------- | ------- | ------ |
| 1          | Basic   | Active |
| 2          | Basic   | Active |
| 5          | Premium | Active |

With guest embeds, we can "lock" the filter by encoding the query parameter in a signed JWT. For example, say we set up the "Status = Active" filter as a [locked parameter](./static-embedding-parameters#restricting-data-in-a-static-embed-with-locked-parameters). The `?status=active` query parameter will be encoded in the signed JWT, so it won't be visible or editable from the guest embedding URL:

```plaintext
https://my-metabase.com/dashboard/your_signed_jwt
```

If someone tries to add an (unsigned) query parameter to the end of the guest embedding URL like this:

```plaintext
https://my-metabase.com/dashboard/your_signed_jwt?status=inactive
```

Metabase will reject this unauthorized request for data, so the inactive account rows will remain hidden from the embed.

### Example: sending user attributes to a locked parameter

Let's say that we want to expose the Accounts table to our customers, so that customers can look up a row based on an Account ID.

| Account ID | Plan    | Status   |
| ---------- | ------- | -------- |
| 1          | Basic   | Active   |
| 2          | Basic   | Active   |
| 3          | Basic   | Inactive |
| 4          | Premium | Inactive |
| 5          | Premium | Active   |

If we want to avoid creating a Metabase login for each of our customers, we'll need:

- An [embeddable dashboard](./static-embedding#making-a-question-or-dashboard-embeddable) with the Accounts data.
- A [locked parameter](./static-embedding-parameters) for the Account ID filter.
- A login flow in our embedding application (the web app where we want to embed Metabase).

The flow might look something like this:

1. A customer logs into our web app.
2. Our app backend looks up the customer's `account_id` based on the account email used during login.
3. Our app backend uses Metabase's [secret key](./guest-embedding#regenerating-the-embedding-secret-key) to [generate the embedding URL](./guest-embedding#how-guest-embedding-works) with a signed JWT. The signed JWT encodes the query parameters to filter the Accounts dashboard on `Account ID = account_id`.
4. Metabase returns the filtered dashboard at the guest embedding URL.
5. Our app frontend displays the filtered dashboard in an iframe.

## Modular and full app embedding auth with JWT or SAML



Modular embedding (including using the [SDK](./sdk/introduction)), and [full-app embedding](./full-app-embedding) integrate with SSO (either [JWT](../people-and-groups/authenticating-with-jwt) or [SAML](../people-and-groups/authenticating-with-saml)) to authenticate and authorize people in one flow. The auth integration makes it easy to map user attributes (such as a person's role or department) to granular levels of data access, including:

- [Tables](../permissions/data)
- [Rows](../permissions/row-and-column-security#row-level-security-filter-by-a-column-in-the-table)
- [Columns](../permissions/row-and-column-security#custom-row-and-column-security-use-a-sql-question-to-create-a-custom-view-of-a-table)
- [Other data permissions](../permissions/data), such as data download permissions or SQL access.

![Full app embedding with SSO.](./images/full-app-embedding-sso.png)

This diagram shows you how a full app embed gets secured with [SSO](../people-and-groups/start#sso-for-metabase-pro-and-enterprise-plans):

1. **Visitor arrives**: your frontend gets a request to display all content, including a Metabase component (such as a React component).
2. **Load embed**: your frontend component loads the Metabase frontend using your [embedding URL](./full-app-embedding#pointing-an-iframe-to-a-metabase-url).
3. **Check session**: to display data at the embedding URL, your Metabase backend checks for a valid session (a logged-in visitor).
4. **If there's no valid session**:
   - **Redirect to SSO**: your Metabase frontend redirects the visitor to your SSO login page.
   - **SSO auth**: your SSO flow authenticates the visitor and generates a session based on their identity. The session info should encode user attributes such as group membership and [row and column security](../permissions/row-and-column-security) permissions.
   - **Redirect to Metabase**: your SSO flow redirects the visitor to your Metabase frontend with the session info.
5. **Request**: your Metabase frontend sends the request for data to the Metabase backend, along with the session info.
6. **Response**: your Metabase backend returns data based on the user attributes encoded in the session info.
7. **Success**: your frontend component displays the embedded Metabase page with the correct data for the logged-in visitor.

The mechanics of step 4 will vary a bit depending on whether you use [JWT](../people-and-groups/authenticating-with-jwt) or [SAML](../people-and-groups/authenticating-with-saml) for SSO.

### Example: securing data with SSO and row and column security

In our guest embedding example, we used [locked parameters](#example-securing-data-with-locked-parameters-on-a-guest-embed) to display secure filtered views of the Accounts table.

The nice thing about modular and full app embedding and [SSO](../people-and-groups/start#sso-for-metabase-pro-and-enterprise-plans) integration is that we don't have to manually manage locked parameters for each embed. Instead, we can map user attributes from our identity provider (IdP) to [permissions](../permissions/introduction) and [row and column security](../permissions/row-and-column-security) in Metabase. People can get authenticated and authorized to self-serve specific subsets of data from their very first login.

Let's expand on our Accounts example to include a Tenant ID. The Tenant ID represents the parent org for a group of customers:

| Tenant ID | Account ID | Plan    | Status   |
| --------- | ---------- | ------- | -------- |
| 999       | 1          | Basic   | Active   |
| 999       | 2          | Basic   | Active   |
| 999       | 3          | Basic   | Inactive |
| 777       | 4          | Premium | Inactive |
| 777       | 5          | Premium | Active   |

We still want to expose the Accounts table to our customers, but with a few extra requirements:

- Individual customers can only view the data for their own Account ID.
- Tenants can view all of their child accounts (but not the data of other tenants).

To set up these multi-tenant permissions, we'll need to:

1. Create an `primary_id` attribute in our IdP to uniquely identify all tenants and customers.
2. Create a user attribute in our IdP called `role` and set that to `tenant` or `customer` for each person who will be using Metabase.
3. Create two groups in Metabase: Tenants and Customers.
4. Synchronize group membership between Metabase and our IdP so that:
   - People with `role=tenant` are assigned to the Tenant group.
   - People with `role=customer` are assigned to the Customers group.
5. Set up row-level security on the Accounts table for each group:
   - For the Customers group, the Accounts table will be restricted with `Account ID = primary_id`.
   - For the Tenants group, the Accounts table will be restricted with `Tenant ID = primary_id`.

When Tenant A logs in with SSO for the first time:

- Metabase will create an account for them.
- Our IdP will send the `role=tenant` and `primary_id=999` attributes to Metabase.
- Metabase will automatically assign Tenant A to the Tenant group.
- Tenant A will get the Tenant group's permissions (including row and column security).
- Tenant A will see a restricted view of the Accounts table everywhere in Metabase:

| Tenant ID | Account ID | Plan  | Status   |
| --------- | ---------- | ----- | -------- |
| 999       | 1          | Basic | Active   |
| 999       | 2          | Basic | Active   |
| 999       | 3          | Basic | Inactive |

When Customer 1 logs in, they'll see a different filtered version of the Accounts table based on their `role` and `primary_id` attributes:

| Tenant ID | Account ID | Plan  | Status |
| --------- | ---------- | ----- | ------ |
| A         | 1          | Basic | Active |

## Sample apps

- [Modular embedding demo](https://embedded-analytics-sdk-demo.metabase.com)
- [Modular embedding with SDK reference app](https://github.com/metabase/metabase-nodejs-react-sdk-embedding-sample)
- [Full app embedding demo](https://embedding-demo.metabase.com/)
- [Full app embedding reference app](https://github.com/metabase/sso-examples/tree/master/app-embed-example)
- [Guest embedding reference app](https://github.com/metabase/embedding-reference-apps)

## Further reading

- [Configuring permissions for different customer schemas](../permissions/embedding)

---

# Embedding overview

You can use Metabase as a BI tool for your own team, or embed Metabase in your app so your customers can explore their own data.

## [Introduction](./introduction)

What is embedding, and how does it work?

## [Modular embedding](./modular-embedding)

Embed individual dashboards, questions, or the query builder in your app with an interactive wizard and simple drop-in script, with minimal or no coding required. Control component UI and theming. Integrate your app's auth with Metabase SSO.

If you're on Metabase OSS or Starter, you can only embed components without SSO. See [Guest embeds](./guest-embedding).

### [Modular embedding SDK](./sdk/introduction)

With the Modular embedding SDK, you can embed individual Metabase components with React (like standalone charts, dashboards, the query builder, and more). You can manage access and interactivity per component, and you have advanced customization for seamless styling.

### [Modular embedding SDK quickstart](./sdk/quickstart)

Jump to a SDK quickstart with a sample React application.

### [Guest embedding](./guest-embedding)

Guest embedding is a secure way to embed charts and dashboards. Guest embeds are view-only; people won't be able to drill-through charts and tables.

### [Translating embeds](./translations)

Upload a translation dictionary to translate questions and dashboards in modular embeds.

## [Full app embedding](./full-app-embedding)

Full app embedding allows you to embed full Metabase app in an iframe. Full app embedding integrates with your data permissions to let people slice and dice data on their own using Metabase's query builder.

### [Full app embedding quickstart](./full-app-embedding-quick-start-guide)

You'll embed the full Metabase application in your app. Once logged in, people can view a Metabase dashboard in your web app, and be able to use the full Metabase application to explore their data, and only their data.

### [Full app UI components](./full-app-ui-components)

Customize the UI components in your full app embed by adding parameters to the embedding URL.

## [Public embeds](./public-links)

Admins can also create unsecured public links or embeds of questions and dashboards.

## [Securing embeds](./securing-embeds)

How to make sure the right people can see the right data in your embedded Metabase.

## [AI agent resources](./ai-agent-resources)

Machine-readable docs and agent skills to help AI coding agents with embedding setup, upgrades, and migrations.

---

# Parameters for static embeds

Also known as: parameters for signed embeds, or standalone embeds.

Parameters are pieces of information that are passed between Metabase and your website via the [embedding URL](./static-embedding#adding-the-embedding-url-to-your-website). You can use parameters to specify how Metabase items should look and behave inside the iframe on your website.

## Types of parameters

Parameters can be signed or unsigned.

### Signed parameters

Signed parameters, such as filter names and values, must be added to your server code.

- [Editable parameters](#adding-a-filter-widget-to-a-static-embed)
- [Locked parameters](#restricting-data-in-a-static-embed-with-locked-parameters)

### Unsigned parameters

Unsigned parameters, such as appearance settings, should be added directly to your iframe's `src` attribute.

- [Default values for editable parameters](#populating-an-embedded-filter-widget-with-a-default-value)
- [Visibility settings for editable parameters](#hiding-filter-widgets-from-a-static-embed)
- [Appearance settings](#customizing-the-appearance-of-a-static-embed)

## Adding a filter widget to a static embed

You can use **editable parameters** to add [filter widgets](/glossary/filter-widget) to embedded dashboards or SQL questions.

1. Go to your dashboard or SQL question. Make sure you've set up a [dashboard filter](../dashboards/filters) or [SQL variable](../questions/native-editor/sql-parameters).
2. Click on the **sharing icon** > **Embed this item in an application**.
3. Under **Parameters**, you'll find the names of your dashboard filters or SQL variables.
4. Select **Editable** for each parameter that should get a filter widget on your embed.
5. Click **Publish** to save your changes.
6. Add or update the code on your server to [match the code generated by Metabase](./static-embedding#previewing-the-code-for-an-embed).

Editable parameters are responsible for passing filter values from the embedded filter widget (displayed on the iframe) through to the filters on your original dashboard or SQL question (in your Metabase).

### You can't disable parameters when the original question or dashboard requires a value

If the filter on a dashboard or question is set to [Always require a value](../dashboards/filters), you won't be able to disable the parameter when embedding.

## Populating an embedded filter widget with a default value

If you want to set a default value for your [editable filter widget](#adding-a-filter-widget-to-a-static-embed), you can pass that default value to the corresponding parameter name in your iframe's `src` attribute:

```
your_embedding_url?parameter_name=value
```

For example, if your embedded dashboard has a filter connected to an editable parameter called "Breakfast", and you want to set the default value to "Hash browns":

```
your_embedding_url?breakfast=Hash_browns
```

To specify default values for multiple filters, separate them with ampersands (&):

```
your_embedding_url?breakfast=Hash_browns&lunch=Sandwich
```

You can set multiple default values for a single filter by separating the values with ampersands (&):

```
your_embedding_url?breakfast=Hash_browns&breakfast=Pancakes
```

## Hiding filter widgets from a static embed

If you have multiple editable parameters (resulting in multiple filter widgets), you can hide specific ones from your static embed by adding `#hide_parameters` to the end of the URL in your iframe's `src` attribute:

```
your_embedding_url#hide_parameters=parameter_name
```

For example, to hide a filter called "Breakfast" from your embedded dashboard:

```
your_embedding_url#hide_parameters=breakfast
```

You can hide multiple filter widgets by separating the parameter names with commas:

```
your_embedding_url#hide_parameters=breakfast,lunch
```

You can also simultaneously assign a parameter a default value _and_ hide its filter widget:

```
your_embedding_url?breakfast=Hash_browns#hide_parameters=breakfast
```

## Unsigned parameter syntax

Whenever you're adding a parameter to the embedding URL in your iframe's `src` attribute, note that:

- Parameter _names_ are lowercase.
- Parameter _values_ are case-sensitive (the values must match your data).
- Spaces should be replaced by underscores.

## Restricting data in a static embed with locked parameters

If you want to restrict the data that's displayed in an embedded dashboard or SQL question, you can set up a **locked parameter**. A locked parameter filters the data in a dashboard or SQL question _before_ the results are displayed to people in a static embed.

1. Go to your dashboard or SQL question. Make sure you've set up a [dashboard filter](../dashboards/filters) or [SQL variable](../questions/native-editor/sql-parameters).
2. Click on the **sharing icon** > **Embed this item in an application**.
3. Under **Parameters**, you'll find the names of your dashboard filters or SQL variables.
4. Select **Locked** for each parameter that you want to restrict your data with.
5. Optional: select a value under **Preview locked parameters** to see what the restricted data looks like.
6. Click **Publish** to save your changes.
7. Add or update the code on your server to [match the code generated by Metabase](./static-embedding#previewing-the-code-for-an-embed).

You can use locked parameters to display filtered data based on attributes captured by your web server, such as a username or a tenant ID. For more examples, see the [reference apps repo](https://github.com/metabase/embedding-reference-apps).

Locked parameters will apply the selected filter values to your original dashboard or SQL question, but they won't be displayed as filter widgets on your embed. Locked parameters may also limit the values that are shown in your [editable filter widgets](#adding-a-filter-widget-to-a-static-embed).

If you just want to require a value for the parameter, you could set the filter as editable and configure the underlying question or dashboard to [always require a value](../dashboards/filters).

For more on how locked parameters work, check out [guest embedding](./guest-embedding#locked-parameters). The rules for locked parameters are the same for static embeds.

## Customizing the appearance of a static embed

You can change the appearance of an embedded item by adding hash parameters (e.g., `#theme=night`) to the end of the URL in your iframe's `src` attribute.

For example, the following embedding URL will display an embedded item in dark mode, without a border, and with its original title:

```
your_embedding_url#theme=night&bordered=false&titled=true
```

You can preview appearance settings from your question or dashboard's embedded appearance settings.

| Parameter name             | Possible values                                                                                                                                    |
| -------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------- |
| `background`               | `true` (default), `false`. Dashboards only.                                                                                                        |
| `bordered`                 | `true` (default), `false`.                                                                                                                         |
| `locale`\*                 | E.g., `ko`. See [list of locales](../configuring-metabase/localization#supported-languages)                                                     |
| `titled`                   | `true` (default), `false`.                                                                                                                         |
| `theme`                    | `null` (default), `night`. `theme=transparent` should work, but is deprecated (see [Transparent backgrounds](#transparent-backgrounds-for-embeds)) |
| `refresh` (dashboard only) | integer (seconds, e.g., `refresh=60`).                                                                                                             |
| `font`\*                   | [font name](../configuring-metabase/fonts)                                                                                                      |
| `downloads`\*\*            | `true` (default), `false`, `results`, `pdf`                                                                                                        |

\* Available on [Pro](/product/pro) and [Enterprise](/product/enterprise) plans

\*\* Disabling downloads is available on [Pro](/product/pro) and [Enterprise](/product/enterprise) plans.

For global appearance settings, such as the colors and fonts used across your entire Metabase instance, see [Customizing Metabase's appearance](../configuring-metabase/appearance).

## Setting the language for a static embed



To change the UI language for a static embed, you can set its [locale](../configuring-metabase/localization#supported-languages). For example, to set a public link's language to Korean, append `#locale=ko`:

```
https://metabase.example.com/public/dashboard/7b6e347b-6928-4aff-a56f-6cfa5b718c6b?category=&city=&state=#locale=ko
```

If you have multiple parameters, separate them with an ampersand (`&`):

```
category=Gadget&state=Vermont#theme=night&locale=ko
```

The `locale` parameter changes the language for Metabase UI elements, like the label of "Export to PDF" button. To change the _content_'s language in a static embed (like names of questions and dashboards), you'll need to [upload a translation dictionary](./translations).

## Transparent backgrounds for embeds

Making an embed transparent depends on the type of embed:

- Dashboards: set `background=false`. The `background` parameter can be combined with the `theme` parameter (e.g., `background=false&theme=night`).
- Questions: set `theme=transparent` (deprecated, but still supported).

## Disable downloads for an embedded question or dashboard



By default, Metabase will include a **Download** button on embedded questions, and an **Export to PDF** option on embedded dashboards.

You can configure these options with the `downloads` parameter in the embedding URL in the iframe's `src` attribute, see [customizing the appearance of static embeds](./static-embedding#customizing-the-appearance-of-static-embeds).

`downloads` accepts the following values:

- `true` (default): include both the Download and Export to PDF options.
- `false`: hide both the Download and Export to PDF options.
- `results`: show the Download option.
- `pdf`: show the Export to PDF option (dashboards only).

You can combine the explicit options: `downloads=results,pdf` is the same as `downloads=true`.

The `downloads` parameter replaces the legacy `hide_download_button` parameter.

## Maximum request size

The maximum length of a static embedding URL (including all parameters) is determined by your [`MB_JETTY_REQUEST_HEADER_SIZE`](../configuring-metabase/environment-variables#mb_jetty_request_header_size) environment variable. The default is 8192 bytes.

If your static embedding URL exceeds the maximum header size, you'll see a log message like `URI too long`. You can update the environment variable to accept larger headers. If you're using a proxy server, you may need to set a corresponding property on the server as well.

## Further reading

- [Static embedding documentation](./static-embedding).
- [Strategies for delivering customer-facing analytics](/learn/metabase-basics/embedding/overview).
- [Publishing data visualizations to the web](/learn/metabase-basics/embedding/charts-and-dashboards).

---

# Static embedding

> We recommend you use the new [Guest embedding](./guest-embedding) approach instead of static embedding.

In general, embedding works by displaying a Metabase URL inside an iframe in your website. A **static embed** (or signed embed) is an iframe that's loading a Metabase URL secured with a signed JSON Web Token (JWT). Metabase will only load the URL if the request supplies a JWT signed with the secret shared between your app and your Metabase. The JWT also includes a reference to the resource to load, e.g., the dashboard ID, and any values for locked parameters.

You can't use static embeds with [row and column security](../permissions/row-and-column-security), [drill-through](../questions/visualizations/drill-through), and user-specific data isn't captured in [usage analytics](../usage-and-performance-tools/usage-analytics) because signed JWTs don't create user sessions (server-side sessions). For those features, check out [Modular embedding](./modular-embedding).

You can, however, restrict data in static embeds for specific people or groups by [locking parameters](./static-embedding-parameters#restricting-data-in-a-static-embed-with-locked-parameters).

## How static embedding works

If you want to set up interactive Metabase filters in your iframe, your web server will need to make requests to Metabase for updated data each time a website visitor updates the filter widget.

To ask for updated data from Metabase, your web server will generate a new Metabase [embedding URL](#adding-the-embedding-url-to-your-website). For example, if a website visitor enters the value "true" in an [embedded filter widget](./static-embedding-parameters#adding-a-filter-widget-to-a-static-embed), your web server will generate a new embedding URL with an extra parameter:

```
your_metabase_embedding_url?filter=true
```

To prevent people from editing the embedding URL to get access to other parts of your Metabase (e.g., by changing the parameter to `filter=company_secrets`), your web server will add a signed JWT to the new embedding URL:

```
your_metabase_embedding_url/your_signed_jwt?filter=true
```

The signed JWT is generated using your [Metabase secret key](#regenerating-the-static-embedding-secret-key). The secret key tells Metabase that the request for filtered data can be trusted, so it's safe to display the results at the new embedding URL. Note that this secret key is shared for all static embeds, so whoever has access to that key will have access to all embedded artifacts.

If you want to embed charts with additional interactive features, like [drill-down](../questions/visualizations/drill-through) and [self-service querying](../questions/query-builder/editor), see [Modular embedding](./modular-embedding).

## Turning on the embedding feature in Metabase

1. Go to **Settings** > **Admin settings** > **Embedding > Static**.
2. Toggle **Enable static embedding**.

## Making a question or dashboard embeddable

![Sharing button to embed dashboard](./images/sharing-embed.png)

To create a static embed:

1. Go to the question or dashboard that you want to embed in your website.
2. Click on the **sharing icon**.
3. Select **Embed**.
4. Select **Static embedding**.
5. Optional: [customize the appearance of the embed](./static-embedding-parameters#customizing-the-appearance-of-a-static-embed)
6. Optional: [Add parameters to the embed](./static-embedding-parameters).
7. Click **Publish**.

## Adding the embedding URL to your website

The embedding URL for a question or dashboard is the Metabase URL that'll be displayed in your website's iframe. It's generated by your web server using your [Metabase site URL](../configuring-metabase/settings#site-url), [signed JWT](#how-static-embedding-works), and [parameters](./static-embedding-parameters):

```
metabase_site_url/embed/question/your_jwt_token?parameter_name=value
```

Once you've made a question or dashboard [embeddable](#making-a-question-or-dashboard-embeddable), you'll need to put the embedding URL for that question or dashboard on your website:

1. Go to the question or dashboard > **sharing icon** > **Embed**.
2. Make any changes and copy the code.
3. [Preview the code](#previewing-the-code-for-an-embed)
4. Add the code to the server code that builds your website.
5. Add the frontend code to the code that generates the page where you want the embedded item to appear.

For more examples, see our [reference apps repo](https://github.com/metabase/embedding-reference-apps).

## Previewing the code for an embed

1. Go to the question or dashboard > **sharing icon** > **Embed this item in an application**.
2. Click **Code**.
3. In the top code block, you'll find the sample code for your web server. You'll also find the iframe snippet to plug into your HTML template or single page app.

When you make changes to the look and feel or parameter preview settings, Metabase will update the code and highlight the changes. Make sure to copy these changes to your actual server code.

Metabase generates server code for:

- Clojure
- Node.js
- Python
- Ruby

For iframe snippets:

- ERB
- JSX
- Mustache
- Pug/Jade

## If you serialize your Metabase, use Entity IDs in your static embeds

Using [Entity IDs](../installation-and-operation/serialization#metabase-uses-entity-ids-to-identify-metabase-items) in your static embeds will make sure that the IDs are stable when exporting from one Metabase and importing to another Metabase.

To use an Entity ID in a static embed, all you need to do is edit the `resource` map in the `payload` used to sign your token. Replace the item's (autopopulated) ID with its Entity ID and you're done.

So, in the code below you'd change the `{ question:  }` to:

```js
const payload = {
  resource: { question:  },
  params: {},
  exp: Math.round(Date.now() / 1000) + (10 * 60) // 10 minute expiration
};
```

If you don't serialize your Metabase, don't worry about which ID you use; both will work just fine.

## Editing an embedded question or dashboard

If you change the [parameters](./static-embedding-parameters) of your embedded item:

1. After making your changes, copy the code Metabase generates.
1. Click **Publish** again.
1. [Update the code](#adding-the-embedding-url-to-your-website) on your server so that it matches the code generated by Metabase.

## Disabling embedding for a question or dashboard

You can find a list of all static embeds of questions and dashboards from **Admin settings** > **Embedding** > **Static**.

1. Visit the embeddable question or dashboard.
2. Click on the **sharing icon** (square with an arrow pointing to the top right).
3. Select **Embed**.
4. Select **Static embedding**
5. Click **Unpublish**.

## Customizing the appearance of static embeds

See [Customizing appearance of static embeds](./static-embedding-parameters#customizing-the-appearance-of-a-static-embed)

## Auto-refreshing the results of an embedded dashboard

> Auto-refreshing is only available for dashboards, not questions.

To refresh the results of a dashboard at a specific cadence, you can parameterize the embedded URL with `refresh`. For example, to set an embedded dashboard to refresh every 60 seconds, you would append `refresh=60` to the URL.

For example, the following code for generating an iframe URL for a dashboard would display the dashboard's title and refresh its results every 60 seconds.

```js
var iframeUrl =
  METABASE_SITE_URL + "/embed/dashboard/" + token + "#titled=true&refresh=60";
```

For the full list options you can parameterize, see [customizing the appearance of a static embed](./static-embedding-parameters#customizing-the-appearance-of-a-static-embed).

## Removing the "Powered by Metabase" banner

![Powered by Metabase](./images/powered-by-metabase.png)

The banner appears on static embeds created with Metabase's open-source version. To remove the banner, you'll need to upgrade to a [Pro](/product/pro) or [Enterprise](/product/enterprise) plan.

## Regenerating the static embedding secret key

Your embedding secret key is used to sign JWTs for all of your [embedding URLs](#adding-the-embedding-url-to-your-website).

1. Go to **Admin** > **Embedding** > **Static embedding**.
2. Under **Regenerate secret key**, click **Regenerate key**.

This key is shared across all static embeds. Whoever has access to this key could get access to all embedded artifacts, so keep this key secure. If you regenerate this key, you'll need to update your server code with the new key.

## Resizing dashboards to fit their content

Dashboards are a fixed aspect ratio, so if you'd like to ensure they're automatically sized vertically to fit their contents you can use the [iFrame Resizer](https://github.com/davidjbradshaw/iframe-resizer) script. Metabase serves a copy for convenience:

```html



```

Due to iframe-resizer's licensing changes, we recommend that you use iframe-resizer version 4.3.2 or lower.

## Custom destinations on dashboards in static embeds

You can only use the **URL** option for [custom destinations](../dashboards/interactive#custom-destinations) on dashboards with static embedding. External URLs will open in a new tab or window.

You can propagate filter values into the external URL, unless the filter is locked.

## Translating static embeds

See [Translating embedded questions and dashboards](./translations).

## Further reading

- [Parameters for static embeds](./static-embedding-parameters).
- [Reference apps repo](https://github.com/metabase/embedding-reference-apps).
- [Strategies for delivering customer-facing analytics](/learn/metabase-basics/embedding/overview).
- [Publishing data visualizations to the web](/learn/metabase-basics/embedding/charts-and-dashboards).
- [Customizing Metabase's appearance](../configuring-metabase/appearance).

---

# Tenants



Tenants give you a way to group users and keep them isolated from users in other tenants. If you're building a SaaS app with embedded Metabase dashboards, you can assign the customers of your SaaS to tenants.

The big advantage of grouping users into tenants is that you can use the same content and group permissions for all tenants, so you don't have to create all new stuff for each tenant, and each tenant only ever sees their own data.

![Tenants](./images/tenants.png)

You can use tenants to:

- Simplify bulk permissions through tenant groups and attributes.
- Create collections of shared assets to avoid duplicating dashboards.
- Provision tenants with SSO.

To get started with tenants:

1. [Get familiar with tenant concepts](#concepts).
2. [Enable tenants](#enable-multi-tenant-strategy).
3. Create a [tenant](#create-new-tenants-in-metabase) and [tenant users](#create-tenant-users-in-metabase), or [provision them with SSO](#provisioning-and-assigning-tenants-with-jwt).
4. [Create shared collections](#create-shared-collections-for-tenants).
5. Set up [data](#data-permission-overview) and [collection](#collection-permissions-for-tenants) permissions.

## Concepts

**Tenant** is an abstraction representing groups of users that share some properties but should be isolated from other users. For example, if you're building a SaaS app with embedded Metabase dashboards, the customers of your SaaS are tenants.

While working with tenants in Metabase, you'll encounter different user and group types, and some special collection types. Let's establish some terminology.

### User types

- **Tenant users** are the end users. In a B2B SaaS context, these are your customer's users. Usually tenant users interact with Metabase through an intermediate app (for example, through an embedded dashboard).

- **Internal users** are "normal" Metabase users who don't belong to any tenant. Think Metabase admins, or devs building the dashboards that will be shared with tenants.

### Group types

Tenant users and internal users can be organized into Metabase [groups](../people-and-groups/managing#groups).

![Tenant groups types](./images/tenant-groups.png)

- **All tenant users** is a special group representing all individual end users across all tenants.

  This group can be used to [configure permissions](#data-permissions-for-tenants) for all tenant users. If you need more granular permission controls within each tenant, you can also create tenant groups.

- **Tenant groups** can be used to create additional permission levels for tenant users.

  For example, if you're making recruiting software, every end user can be a recruiter (with access to all data about all open roles) or a hiring manager (with access to analytics for a specific role). So in Metabase, you can create two tenant groups - "Recruiters" and "Hiring managers" and configure appropriate permissions.

  Every tenant will be able to use tenant groups (so for example, every customer can have "Recruiters" and "Hiring managers").

- **All internal users** is a special group for all people who _aren't_ members of a tenant.

  These are the people working directly in Metabase. This group is equivalent to the "All users" group when multi-tenancy is not enabled.

- **Internal groups** are additional groups for internal users.

  For example, you might have a special internal group "Analytics developers" who only have access to create dashboards that will be later shared with tenants, but don't have full admin access to your whole Metabase.

Tenant users are completely isolated from internal users: tenant users can't be added to internal user groups, and internal users can't be added to tenant groups.

### Collection types

Collections are like folders that can contain charts, dashboards, and models. They also serve as an organizational unit for permission management: if a certain group of people should have access to a certain set of assets, you should put those assets into a collection.

![Tenant collections](./images/tenant-collections.png)

- **Shared collections** contain dashboards and charts that are shared between all tenants.

  For example, every tenant of a recruiting app should be able to see the number of job applications by date. You can create a [Metabase question](../questions/start) for "Count of applications by date" and save it into a shared collection.

  You'll need to [configure data permissions](#data-permissions-for-tenants) so that each tenant only sees _their_ job applications, and not _everyone's_ job applications (that would be bad).

  You can create many shared collections (or no shared collections at all, for that matter). For example, you can have a shared collection for analytics around recruitment and a separate shared collection for analytics around interviews.

  Shared collections can be optionally [synced to GitHub](#sync-shared-collections-to-github).

- **Tenant collections** are collections specific to each tenant. They are created automatically for each tenant.

  If you have special customers with bespoke analytics that only this customer gets, you can put those bespoke dashboards and charts into the tenant's collection. Tenant collections can also serve as a place for the tenant users to create and save new questions that can be shared with their fellow tenant users (but not with users from other tenants).

- **Internal collections**. Internal collections are exclusively an internal user concern. These are "normal" collections where you and other users internal to your Metabase can put the stuff that you don't want your end users to see (dashboards in development, internal metrics, maybe even analytics _about_ your tenants). Tenant users can't access any internal collections.

- **Personal collection**. Every Metabase user, including tenant users, gets a personal collection - their own space to save new questions and dashboards (of course, if they otherwise have the permissions to build and save new questions).

See [Collection permissions](#collection-permissions-for-tenants) for configuring access to different collection types.

## End user experience

End users who are members of tenants will not know that they are members of tenants.
In the experiences that expose users to Metabase collections (e.g. when using full-app embedding, modular embedding components with save enabled, or if tenant users are logging in directly into Metabase), tenant users see all the tenant and shared collections they have access to as just collections.

## Enable multi-tenant strategy

_Admin settings > People_

You can create and manage your tenants exclusively through Metabase UI, or, if that's not your jam, [through SSO](#provisioning-and-assigning-tenants-with-jwt). Regardless of how you manage your tenants, you'll need to enable multi-tenant strategy in Metabase first.

![Edit tenant strategy](./images/edit-tenant-strategy.png)

1. Go to **Admin settings > People**.
2. Click on the **gear** icon above the list of people.
3. Choose **Multi-tenant strategy**.

Changing Metabase to multi-tenant strategy enables special [user](#user-types) and [collection](#collection-types) types. You can create new tenants, tenant groups, and collections, and you get some additional admin settings in the People and Permissions tabs.

If you have an existing permissions and collection setup that you'd like to translate to use tenants, see [Changing tenant strategy](#changing-tenant-strategy).

Once you enable multi-tenant strategy, keep in mind that switching _from_ multi-tenant to single-tenant is a destructive action: all your tenant users and your tenant and shared collections will be disabled. See [Changing tenant strategy](#changing-tenant-strategy).

## Create new tenants in Metabase

_Admin settings > People > Tenant_

![New tenant user](./images/new-tenant.png)

To create new tenants in Metabase:

1. [Enable multi-tenant strategy](#enable-multi-tenant-strategy), if you haven't yet.
2. Go to **Admin settings > People** .
3. Select **Tenants** on the left sidebar and click **New tenant**.
4. Fill out the information for the tenant:
   - **Tenant name**: Display name for the tenant that will be displayed to _internal_ users. Not exposed to external users. This name can be changed later.
   - **Tenant slug**: Unique identifier of the tenant. It can be used to [match JWT claims](#use-tenant-claim-to-sign-in-users) and for setting up [data permissions](#use-tenant-attributes-for-data-permissions). See [Tenant slug](#special-tenant-slug-attribute) for more information.
   - **Tenant attributes**: you can define tenant attributes that will be inherited by every tenant user, see [Tenant attributes](#tenant-attributes).

You can avoid manually setting up tenants in Metabase by [provisioning tenants with JWT](#provisioning-and-assigning-tenants-with-jwt).

## Create tenant groups

_Admin settings > People > Tenant groups_

Tenant groups are applicable across tenants. For example, you can have tenant groups "Basic users" and "Premium users", and every tenant will be able to use those groups. Groups can be used to configure permissions so that, among the tenant's users, some get Basic permissions while others get Premium. See [Tenant concepts](#concepts) for more information.

To create a tenant group:

1. [Enable multi-tenant strategy](#enable-multi-tenant-strategy), if you haven't yet.
2. Go to **Admin settings > People**.
3. Select **Tenant groups** on the left sidebar and click **Create a group**.
4. Name your group.

To add people to tenant groups, see [Add people to groups](../people-and-groups/managing#adding-people-to-groups).

## Create tenant users in Metabase

_Admin settings > People > Tenant users_

Tenant users are the end users in tenants. In a B2B SaaS context, these are your customer's users. Usually tenant users interact with Metabase through an intermediate app (for example, through an embedded dashboard).
To add tenant users in Metabase:

1. [Create a tenant](#create-new-tenants-in-metabase).
2. Go to **Admin settings > People** .
3. Select **Tenant users** on the left sidebar and click **New tenant user**.
4. Fill out the user information, including the tenant and tenant groups.

   If your tenant has [tenant attributes](#tenant-attributes), they'll be inherited by the user, but you can override the value in "Attributes".

You can also [provision tenant users with JWT](#provisioning-and-assigning-tenants-with-jwt).

## Create shared collections for tenants

![Create shared collection](./images/create-shared-collection.png)

Shared collections contain dashboards and charts that are shared between all tenants. If you're using shared collections, make sure that you configure [data permissions](#data-permissions-for-tenants) so that tenants can only see _their data_ in the shared collections. See [Tenant concepts](#concepts) for more information.

To create a shared collection:

1. [Enable multi-tenant strategy](#enable-multi-tenant-strategy), if you haven't yet.
2. Open Metabase navigation sidebar by clicking on the **three lines** in the top left (that's regular Metabase, not Admin settings).
3. You should see "External collections" in the sidebar. If you don't, make sure you have enabled multi-tenant strategy.
4. Click on the **+** next to "External collections" to create a shared collection.

You can have multiple shared collections and nested shared collections. You can also [sync shared collections to GitHub](#sync-shared-collections-to-github).

## Sync shared collections to GitHub

_Admin settings > Remote Sync_

You can set up [Remote sync](../installation-and-operation/remote-sync) for shared collections. This means you'll be able to develop shared content in one Metabase, push it to a GitHub repo, and then have the shared content in your production Metabase always synced with that repo. See [Remote sync docs](../installation-and-operation/remote-sync) for more details.

## Tenant attributes

_Admin settings > People > Tenants_

You can create tenant-level [user attributes](#tenant-attributes) which all users of the tenant inherit. This is useful for configuring attribute-based data permissions like [row-level security](../permissions/row-and-column-security), [impersonation](../permissions/impersonation), or [database routing](../permissions/database-routing).

![Edit tenant](./images/edit-tenant.png)

To create a tenant attribute manually using the Metabase UI:

1. Go to **Admin settings > People**
2. Select **Tenants** on the left sidebar.
3. Click on **three dots** next to the tenant.
4. Input the attribute key and value.

You can also assign tenant attributes automatically through JWT claims, see [Setting tenant attributes using tenant claims](#setting-tenant-attributes-using-tenant-claims) below.

Once you add a tenant attribute, all users of that tenant will inherit the attribute, but the value can be overridden for any particular user, see [Edit user attributes](../people-and-groups/managing#adding-a-user-attribute).

### Special tenant slug attribute

Each tenant user will get a system-defined attribute `@tenant.slug` that corresponds to the slug of the tenant. For example, if you created a tenant "Meowdern Solutions" with the slug `meowdern_solutions`, then every user from this tenant will get a special attribute `@tenant.slug : "meowdern_solutions"`.

![Slug attribute](./images/slug-attribute.png)

If you create Metabase tenants through Metabase UI, you can choose the slug when creating the tenant. If you're [using JWT to provision tenants](#provisioning-and-assigning-tenants-with-jwt), tenant slug is the value of the `@tenant` claim for JWT (or another tenant assignment attribute you selected). Slug cannot be changed later.

The special `@tenant.slug` attribute can be used just like a normal attribute to configure attribute-based permissions like [row-level security](../permissions/row-and-column-security), [impersonation](../permissions/impersonation), or [database routing](../permissions/database-routing). Your chosen tenant slug should correspond to how the tenant is actually identified in your setup.

For example, if you want to use row-level security, and tenants are identified in your tables by their IDs (instead of names), then your tenant slug should be an ID as well.

For example, if your data looks like this:

```
| Customer ID | Order number | Order date | Order total |
| ----------- | ------------ | ---------- | ----------- |
| 175924      | 3            | 2025-10-13 | 175.34      |
| 680452      | 7            | 2025-10-13 | 34.56       |
```

and you want to enforce row-level security by `Customer ID`, then the tenant slug should have the form `175924` so that it could be matched to the Customer ID in your table.

Similarly, if you want to use tenant slug for impersonation, you'll have to map the tenant slug to a database role, and for database routing - to a database.

## Provisioning and assigning tenants with JWT

### Use tenant claim to sign in users

You can [set up JWT SSO](../people-and-groups/authenticating-with-jwt) and use the JWT to sign in tenant users.

Once you [enable multi-tenant user strategy](#enable-multi-tenant-strategy), Metabase will look for a `@tenant` claim in your JWT to determine if the user is a tenant user, and which tenant the user belongs to. The value of `@tenant` key should be the tenant's slug. Here's an example of a JWT claim to sign in a tenant user:

```json
{
  "email": "mittens@example.com",
  "first_name": "Mister",
  "last_name": "Mittens",
  "@tenant": "meowdern_solutions"
}
```

If the user has already been assigned to a tenant (for example, through Metabase UI), then the JWT _must_ contain the tenant claim to sign the user in.

### Customize tenant claim

By default, Metabase looks for a `@tenant` key in your JWT. To set up a different key:

1. Go to **Admin** > **Settings** > **Authentication** > **JWT** > **User attribute configuration**
2. Change the **Tenant assignment attribute** key to your preferred identifier.

### Provisioning tenants and users

You can [turn on JWT user provisioning](../people-and-groups/authenticating-with-jwt) so that Metabase will automatically create users and tenants mentioned in the JWT.

When user provisioning with JWT is enabled:

1. Metabase reads the tenant identifier from the JWT claim. By default, this is the `@tenant` key (you can configure this).
2. If the tenant doesn't exist, Metabase automatically creates it. Metabase will use the value of the `@tenant` key (or your chosen assignment attribute) as the tenant slug.
3. New users are automatically assigned to the tenant from their JWT.

### Setting tenant attributes using tenant claims

To create tenant attributes from JWT SSO, include a claim `@tenant.attributes`:

```json
{
  "@tenant": "meowdern_solutions",
  "@tenant.attributes": {
    "id": "13371337",
    "name": "Mammoth Solutions"
  },
  "email": "mittens@example.com",
  "first_name": "Mister",
  "last_name": "Mittens"
}
```

If a tenant attribute with this name doesn't exist, Metabase will create the attribute and assign the value from the JWT claim. However, if the tenant attribute already exists, Metabase will **not** update the value.

### Troubleshooting JWT authentication with tenants

Some common auth error messages and what they mean:

- **Cannot add tenant claim to internal user**: JWT includes a tenant, but the user is an internal user. Only tenant users can have a tenant.
- **Tenant claim required for external user**: JWT lacks a tenant claim, but the user is an external user.
- **Tenant ID mismatch with existing user**: JWT has a different tenant than the user's assigned tenant.
- **Tenant is not active**: The tenant exists but has been deactivated.

## Data permissions for tenants

_Admin settings > Permissions_

Data permissions control what data people can see on charts and dashboards, and what they can do with that data. To control _which_ charts people see, you can use [collection permissions](#collection-permissions-for-tenants) instead.

### Data permission overview

For an overview of how data permissions work in Metabase, see [Data permissions](../permissions/data). Here are the highlights (but please do read the full [Data permissions](../permissions/data) documentation):

- **"View data"** controls which data each user group can see on dashboards and charts.

  For example, if your tenant data is commingled in one database, then you can use [**Row and column security**](../permissions/row-and-column-security) or [**Impersonation**](../permissions/impersonation) "View data" permissions to provide tenant users with access to only certain rows and columns.

  If every tenant has their data in a separate database, then instead of using permissions for data access control, you can use [**database routing**](../permissions/database-routing) to route queries to appropriate databases directly.

  For a comparison of data isolation methods, see [Choosing a data isolation method](../permissions/data-isolation-methods).

- **Create queries** controls whether tenant users can create queries on the data they see. If you want to give your tenant users the ability to drill through (e.g., through `drills` parameter in [modular embedding](../embedding/modular-embedding)), you need to give them "Create queries" permissions, because a drill through is a new query.

- **Download results** controls, unsurprisingly, whether people can download results of queries. You need to set download permissions if you want to give your users the option to download their data as a spreadsheet (for example, through `with-downloads` parameter in [modular embedding](../embedding/modular-embedding)).

Data permissions in Metabase can be specified on database or table level and are granted to groups. You'll need to use the special **All tenant users**, and your tenant groups (if any) to assign data permissions. Keep in mind that Metabase permissions are additive, so if someone is a member of two different groups, they will be granted the _most_ permissive access. In particular, if "All tenant users" has "Can view" access to an entire table, but another tenant group has restricted access (e.g. "Row and column security"), then the users of the tenant group will still see all the data in the table because they get it via the "All tenant users" group. If you're using tenant groups, we recommend revoking access for "All tenant users" and configuring access on group-by-group basis.

Please review [Data permissions documentation](../permissions/data) for more details on permissions setup.

### Use tenant attributes for data permissions

[Row and column security](../permissions/row-and-column-security), [Impersonation](../permissions/impersonation), and Database routing require user attributes. You can [specify custom tenant attributes](#tenant-attributes) to configure data permissions based on attribute values. See [Tenant attributes](#tenant-attributes). 

## Collection permissions for tenants

Collection permissions control which entities (dashboards, questions, models etc) people can see.

To configure what _data_ can people see in those entities, and what they can do with that data, see [Data permissions](#data-permissions-for-tenants) instead.

In Metabase, there are different levels of collection permissions: **No** access, **View**-only access, and **Curate** access (allows for creating and saving new entities like dashboards). For more general information about collection permissions in Metabase, see [Collection permissions](../permissions/collections).

Permissions are granted to groups. Which permissions are available to each group depend on the type of the group (external/tenant or internal) and the type of the collection.

### Tenant user collection permissions

- For **internal collections**, tenant users will have **No** access.
- For **shared collections**, tenant users can only have **View** or **No** access. This means that at _most_, tenant users can see existing entities but not create new ones.

  Different tenant groups can have different levels of access to different shared collections. For example, you can have a "Basic analytics" shared collection viewable by all users, and "Advanced analytics" collection only viewable by tenant group "Premium users".

  See [Configuring shared collection permissions](#configuring-shared-collections-permissions).

- For **tenant collections**, tenant users will always have **Curate** permissions, which means that tenant users will always be able to save new questions in their tenant collection.

  If you don't want your tenant users to create and save their own charts, you'll need to disable "Create queries" [data permissions](#data-permissions-for-tenants) for tenant users, and, if you're embedding Metabase, configure the embedded UI components to disable saving.

- For their own **personal collections**, tenant users will always have **Curate** permissions.

### Internal user collection permissions

- Metabase Admins will have **Curate** access to all shared collections and all tenant collections.
- Other internal and non-admin groups have **No** access by default, but can be granted **View** or **Curate** access to shared or tenant collections. See [Configuring shared collection permissions](#configuring-shared-collections-permissions) for details.

For configuring permissions to _internal_ collections for internal users, see [general docs on collection permissions](../permissions/collections).

### Configuring shared collections permissions

_Admin settings > Permissions_

To configure access to shared collections for tenant and internal groups, go to **Admin settings > Permissions > Shared collections**.

You can configure access for each shared collection and their subcollections for both internal and external users. See general docs on [collection permissions](../permissions/collections).

Special **Root shared collection** controls who has access to _all_ shared collections. For example, if you want to make sure your internal users don't have access to any tenant shared collections, you can revoke permissions from the Root shared collection.

When configuring permissions, remember that in Metabase, all permissions are additive, so if someone is a member of two different groups, they will be granted the _most_ permissive access. In particular, if "All tenant users" has "View" access to a shared collection, but another tenant group has "No" access to that collection specified in the permission settings, the users of the tenant group will still get "View" access because they have it via the "All tenant groups". If you're using tenant groups, we recommend revoking access for "All tenant users" and configuring access on group-by-group basis.

## Subscription permissions for tenants

_Admin settings > Permissions_

By default, all tenant users will be created with **No** [subscription permissions](../permissions/application#subscriptions-and-alerts). If you want your users to be able to create subscriptions (either in full-app embedding, modular embedding, or by logging in directly to Metabase), you'll need to change the Subscription and alerts permissions to "Yes".

## Deactivate a tenant

_Admin settings > People > Tenants_

**Deactivating a tenant will also deactivate all users who belong to this tenant**.

To deactivate a tenant:

1. Go to **Admin settings > People**.
2. Select **Tenants** on the left sidebar.
3. Click on **three dots** next to the tenant.
4. Choose **Deactivate tenant**.

All tenant users will be deactivated and won't be able to sign in anymore. Tenant users will not be permanently deleted (Metabase does not delete users, only deactivates), so even though the tenant users will be deactivated, you won't be able to create new users with the same email.

## Changing tenant strategy

### From single-tenant to multi-tenant

When you enable multi-tenant strategy, all users that currently exist in Metabase will be considered "internal" users. If you don't want any of those users to become [tenant users](#concepts), you can just proceed with tenant setup as if you had a fresh new instance (create tenants, create collections, set up permissions, etc).

However, if you want to assign some existing users to tenants, you'll need to:

1. Mark them as tenant users using the API call:

   ```sh
   PUT /api/user/:id
   {"tenant_id": 1}
   ```

2. If you're using JWT for SSO, [add a `@tenant` claim to your JWT](#provisioning-and-assigning-tenants-with-jwt).
3. Set up [tenant groups](#concepts), [data permissions](#data-permissions-for-tenants), and [collection permissions](#collection-permissions-for-tenants) because you won't be able to use existing internal groups for tenant permissions.

### From multi-tenant to single-tenant

If you disable multi-tenant strategy, _all your tenant users will be deactivated_ and _all collections will be deleted_ (although you'll get both users and collections back if you reactivate multi-tenant strategy later). So if you want to keep the active users but just disable tenancy features, you'll need some extra prep.

1. Replicate the tenant setup you have with regular groups and collections (instead of tenant groups and shared collections). Review documentation for Data permissions, Collection permissions, and User groups. Make sure to thoroughly test your setup with test users. A [development instance](../installation-and-operation/development-instance) might come in handy.
2. If you're using any tenant groups, remove the tenant group memberships of all tenant users.
3. Change the tenant users to internal users using API:

   ```sh
   PUT /api/user/:id
   {"tenant_id": null}
   ```

   **If you don't do this step, all your users will be deactivated.**

4. Finally, disable the feature once everything is verified to work.

## Limitations

- **Tenant collections and personal collections can't be disabled**.

  If you don't want your tenant users to create and save their own charts, you can disable "Create queries" [data permissions](#data-permissions-for-tenants), and, if you're embedding Metabase, configure the embedded UI components to disable saving.

- **Tenant users can't change tenants.** Once an external user is assigned to a tenant, they cannot switch to another tenant.

- **If you disable multi-tenant strategy, deactivated tenant users will not show up in "Deactivated users"**, but Metabase will still keep track of them and won't allow creating new users with the same email.

- **There are no tenant-specific groups**. Tenant groups are shared between all tenants. If you need a special group for just some of your tenants, create a tenant group but don't add any members from the tenants that the group isn't applicable to.

## Further reading

- [Embedding overview](./start)
- [JWT authentication](../people-and-groups/authenticating-with-jwt)
- [Permissions overview](../permissions/start)
- [Data isolation methods](../permissions/data-isolation-methods)

---

# Translate embedded components



You can set a locale on modular embeds (guest, SSO, and SDK) to translate Metabase's UI. If you've uploaded a translation dictionary, Metabase will also translate content strings (like dashboard names and filter labels) for all [modular embeds](./modular-embedding).

## Set a locale to translate UI, and upload a dictionary to translate content

To translate an embed's user interface, set the locale in the config. The `locale` setting works for all modular embeds (guest, SSO, and SDK). Metabase UI elements (like menus) will be translated automatically - you don't need to add translations for them to your dictionary.

For guest and SSO embeds (not the SDK), set the `locale` in `window.metabaseConfig`:

```html



```

If you also want to translate content (like item titles, headings, filter labels, or data), you'll need to add a translation dictionary.

### SDK translations

For the SDK, set the `locale` prop on the `MetabaseProvider` component:

```tsx

```

If you've uploaded a translation dictionary, the SDK will also translate content strings (like dashboard names and filter labels) to this locale from that dictionary.

## Add a translation dictionary

The dictionary must be a CSV with these columns:

- **Language** with the locale code
- **String** with the string to be translated
- **Translation**

> Don't put any sensitive data in the dictionary, since anyone can see the dictionary—including viewers of public links.

To add a translation dictionary:

1. Go to **Admin > Embedding**.
2. Under **Translate embedded dashboards and question**, click **Upload translation dictionary**.

Uploading a new dictionary will replace the existing dictionary.

To remove a translation dictionary, upload a blank dictionary.

## Example translation dictionary

Metabase uses these dictionaries to translate user-generated content, like dashboard names in [modular embeds](./modular-embedding).

| Language | String      | Translation  |
| -------- | ----------- | ------------ |
| pt-BR    | Examples    | Exemplos     |
| pt-BR    | First tab   | Primeira aba |
| pt-BR    | Another tab | Outra aba    |
| pt-BR    | Title       | Título       |
| pt-BR    | Vendor      | Vendedor     |
| IT       | Examples    | Esempi       |

Prefer hyphens in your `pt-BR` translation dictionary. Underscores are also acceptable (if you download the dictionary _after_ uploading it, Metabase will transform `pt-BR` to `pt_BR`), but since the `locale` parameter in modular embeds only accepts locales with hyphens (like `pt-BR`), we recommend using hyphens for consistency.

[See a list of supported locales](../configuring-metabase/localization#supported-languages).

## Use full phrases in translation dictionaries

Currently, Metabase doesn't tokenize strings for translations, so you should include exact phrases in your translation dictionary.

For example, if you have a dashboard called "Monthly Sales", it's not sufficient to have translations of "Monthly" and "Sales" - you also need to include "Monthly Sales" as a full string.

Exact translations also apply to strings that use punctuation and special characters. For example, if you have a question title "How many Widgets did we sell this week?", you must include that exact string (with "?") into the translation dictionary. Metabase would treat "How many Widgets did we sell this week" as a different string. Essentially, the strings are keys in a table Metabase looks up, so they must match exactly.

## Translations are case-insensitive

Translations are case-insensitive. For example, if your translation dictionary has a translation:

| Language | String   | Translation |
| -------- | -------- | ----------- |
| pt-BR    | Examples | Exemplos    |

`Examples`, `examples`, `EXAMPLES` would all be translated as `Exemplos`. That is, Metabase won't preserve the original case; it will output the exact translation string in the dictionary.

## Include markdown formatting in translation dictionaries

If the strings you want to translate include markdown formatting, you'll need to include the formatting in the dictionary. For example:

| Language | String         | Translation    |
| -------- | -------------- | -------------- |
| pt-BR    | `**Examples**` | `**Exemplos**` |
| pt-BR    | `_Examples_`   | `_Exemplos_`   |
| pt-BR    | `## Examples`  | `## Exemplos`  |

## The AI chat component isn't translated

One exception is that Metabase won't translate the text in the [AI chat component](./sdk/ai-chat). While Metabot can understand other languages, it works best in English.

## Further reading

- [Modular embedding](./modular-embedding)
- [Guest embedding](./guest-embedding)

---