skills/metabase-full-app-to-modular-embedding-upgrade/SKILL.md
Migrates a project from Metabase Full App / Interactive (iframe-based) embedding to Modular (web-component-based) embedding. Use when the user wants to replace Metabase iframes with Modular embedding web components.
npx skillsauth add metabase/agent-skills metabase-full-app-to-modular-embedding-upgradeInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Follow the workflow steps in order — do not skip any step. Create the checklist first, then execute each step and explicitly mark it done with evidence. Each step's output feeds into the next, so skipping steps produces wrong migrations.
If you cannot complete a step due to missing info or tool failure, you must:
Your response should contain these sections in this order:
Each step section should end with a status line:
Status: ✅ complete or Status: ❌ blockedSteps are sequential — do not start a step until the previous one is ✅ complete.
Follow the app's existing architecture, template engine, layout/partial system, code style, and route patterns. Do not switch paradigms (e.g., templates to inline HTML or vice versa). If the app has middleware for shared template variables, prefer that over duplicating across route handlers.
This skill converts Full App / Interactive embedding (iframe-based) to Modular embedding (web-component-based via embed.js).
The consumer's app may be written in any backend language (Node.js, Python, Ruby, PHP, Java, Go, .NET, etc.) with any template engine. Keep instructions language-agnostic unless a specific language is detected in Step 1.
<iframe> elements pointing to Metabase with appropriate web components (e.g. <metabase-question>, <metabase-dashboard>)embed.js script tag (exactly once at app layout level)window.metabaseConfig setup code (exactly once at app layout level)Fetch the version-specific llms-embedding-full.txt using this URL:
https://www.metabase.com/docs/v0.{VERSION}/llms-embedding-full.txt
The version in the URL uses the format v0.58 (normalize: strip leading v or 0., drop patch — e.g., 0.58.1 → 58 → URL uses v0.58). This single file contains all embedding documentation for that version, optimized for LLM consumption.
Other constraints:
Use AskUserQuestion and halt until answered if:
locale values (ask user which locale to set in window.metabaseConfig)Create a checklist to track progress. In Claude Code, use TaskCreate/TaskUpdate tools:
Always AskUserQuestion for the Metabase instance version — even if a version appears in Docker tags or env vars, confirm it with the user. Abort if v52 or older (modular embedding was introduced in v53).
Then fetch llms-embedding-full.txt for the confirmed version (see "Allowed documentation sources" for URL format).
Before anything else, determine the Metabase version. Grep the project for Docker image tags (metabase/metabase:v, metabase/metabase-enterprise:v), METABASE_VERSION, or version references. If undetected, AskUserQuestion (options: v52 or older, v53, v54–v58, v59+). Abort if v52 or older (modular embedding not available — it was introduced in v53). Record the version — it controls jwtProviderUri placement in later steps.
Perform the project scan and doc fetch concurrently — they are independent. Use parallel tool calls within a single message wherever there are no dependencies.
Fetch llms-embedding-full.txt for the target version (see "Allowed documentation sources" for URL format). These docs are the authoritative source for web component attributes, window.metabaseConfig options, and SSO endpoint behavior for the target version. Use them in Step 2 for mapping instead of relying on hardcoded tables alone.
Launch this concurrently with the project scan steps below.
package.json, requirements.txt, Gemfile, pom.xml, go.mod, composer.json, etc.).Use Grep to search for all of these patterns (in parallel):
<iframe in all template/HTML/JSX/view filesiframe in all server-side code files (JS/TS/Python/Ruby/Go/Java/PHP) — catches iframes built via string concatenation or template literalsauth/sso adjacent to iframe or src attributes. Note: the SSO URL may be constructed in a separate variable or function and passed to the iframe src — if the iframe src is a variable, trace its definition to check for auth/sso.For each file with a match, read the entire file.
Use Grep to search for all of these patterns (in parallel):
/auth/sso/sso/metabase or similar SSO route patternsjwt.sign or jwt.encode or JWT or jsonwebtoken or PyJWT or joseJWT_SHARED_SECRET or METABASE_JWT_SHARED_SECRETreturn_to (Metabase SSO redirect parameter)redirect near auth/sso (catches the SSO redirect logic)For each matching file, read the entire file.
Find the single file (or common code path) where the HTML <head> section is defined — this is where embed.js and window.metabaseConfig will be injected.
Search for:
<head> or <!DOCTYPE or <html in template/view filesinclude('head'), <%- include, {% extends, {% block, layout, base.html, _layout, application.htmlres.send(...)), identify where the <head> content is generatedGrep for METABASE_ and MB_ prefixed variables. Record every Metabase-related variable name and where it is read.
Compile all findings into:
Backend: {language}, {framework}, {template engine}
Metabase config:
- Site URL variable: {name} (read at {file}:{line})
- Dashboard path variable: {name} (read at {file}:{line})
- JWT secret variable: {name} (read at {file}:{line})
- Other variables: ...
Layout/head file: {path}:{line range} (or "inline HTML in {file}:{line range}")
Iframes found: {count}
- {file}:{line} — {brief description}
- ...
SSO endpoint: {file}:{line} — {route} ({method})
Use the documentation fetched in Step 1a as the authoritative reference for web component attributes, window.metabaseConfig options, and SSO endpoint behavior. The hardcoded tables below are fallbacks — if the docs describe additional attributes or different behavior for the target version, prefer the docs.
For each iframe found in Step 1:
Extract from the iframe src attribute (which may be a template expression, variable, or literal):
/dashboard/1, /question/entity/abc123, /collection/5dashboard, question, collection, or home (if path is /)/{resource_type}/entity/{entity_id} use entity IDs#logo=false&top_nav=false)/sso/metabase?return_to=...)| Full App iframe path pattern | Modular Web Component | Required Attribute |
|---|---|---|
| /dashboard/{id} or /dashboard/entity/{entity_id} | <metabase-dashboard> | dashboard-id="{id or entity_id}" |
| /question/{id} or /question/entity/{entity_id} | <metabase-question> | question-id="{id or entity_id}" |
| /model/{id} or /model/entity/{entity_id} | <metabase-question> | question-id="{id or entity_id}" |
| /collection/{id} or /collection/entity/{entity_id} | <metabase-browser> | initial-collection="{id or entity_id}" |
| / (Metabase home / root) | <metabase-browser> | initial-collection="root" |
If the iframe path is built dynamically from a variable, the web component attribute should use the same variable/expression.
If an iframe path does not match any known pattern → AskUserQuestion.
Parameters to drop (not applicable — modular web components do not include Metabase application chrome):
| Full App Parameter | Why it is dropped |
|---|---|
| top_nav | Web components have no Metabase top navigation bar |
| side_nav | Web components have no Metabase sidebar |
| logo | Web components have no Metabase or whitelabel logo |
| search | Web components have no Metabase search bar |
| new_button | No + New button (use with-new-question / with-new-dashboard on <metabase-browser> if applicable) |
| breadcrumbs | Web components have no Metabase breadcrumbs |
Parameters that map to web component attributes:
| Full App Parameter | Modular Equivalent |
|---|---|
| header=false | with-title="false" on the component |
| action_buttons=false | drills="false" on the component |
Parameters that map to window.metabaseConfig:
| Full App Parameter | metabaseConfig Property |
|---|---|
| locale={code} | locale: "{code}" |
Locale migration rules:
locale: "{code}" to window.metabaseConfig automaticallywindow.metabaseConfig (modular embedding supports only one global locale)For each iframe, output:
iframe #{n}: {file}:{line}
Old: {full iframe HTML or code}
Content type: {dashboard|question|collection|home}
ID: {static value or variable expression}
Dropped params: {list}
Mapped attributes: {list}
New: {exact replacement web component HTML}
Create a complete file-by-file change plan covering all areas below. Every change should be specified with the target file, the old code, and the new code.
<head> (or as close as possible to other <script> tags)<script defer src="{METABASE_SITE_URL}/app/embed.js"></script>
{METABASE_SITE_URL} should be rendered dynamically using the project's existing template expression syntax.Modular embedding reads its configuration from window.metabaseConfig. There is no defineMetabaseConfig() function — assign the config object directly.
<script>
window.metabaseConfig = {
instanceUrl: "{METABASE_SITE_URL}",
// Add auth fields here only if supported by the confirmed version's docs
};
</script>
locale parameter was found on any iframe in Step 2c, add locale: "{code}" to the config object. If multiple iframes had different locale values, the user will have already been asked which one to use (per AskUserQuestion trigger).instanceUrl (and jwtProviderUri if supported) should be rendered dynamically using the project's template expression syntax.window.metabaseConfig supports for the confirmed version. For example, jwtProviderUri may or may not be available. If the docs list it, include it as a full absolute URL (e.g., http://localhost:9090/sso/metabase) — relative paths don't work. If the docs don't list it, the JWT Identity Provider URI must be configured in Metabase admin settings instead (see Step 3f).window.metabaseConfig should be set exactly once — if it appears in per-iframe code instead of the layout, each component will re-initialize the SDK.The existing SSO endpoint currently redirects the browser to Metabase's /auth/sso?jwt={token}&return_to={path}.
For modular embedding, the embed.js SDK sends a fetch request to the JWT Identity Provider URI and expects a JSON response. The endpoint should be converted to return JSON only — do not keep a fallback to the old redirect-based auth flow.
This is a full migration, not a gradual one. The old iframe-based embedding is being completely replaced, so the redirect behavior is no longer needed.
Consult the auth docs fetched in Step 0 for the expected SSO endpoint response format for the confirmed version.
Constraints:
{ "jwt": "<token>" } — no other fields, because the SDK parses this exact shapenew URL("/auth/sso", ...), searchParams.set("return_to", ...)) as it is now dead codeFor each iframe from Step 2d's Migration Mapping Table:
width/height attributes or inline style, apply them directly to the web component element (e.g., <metabase-dashboard dashboard-id="1" style="width:800px;height:600px">) — do not wrap in a <div>/sso/metabase?return_to=...). But do not remove the SSO endpoint itself — it is still needed for modular embedding auth.res.send('<iframe ...')), replace the iframe HTML within that handler's response stringAfter replacing iframes and converting the SSO endpoint, identify and remove:
src URL (e.g., iframeUrl, mbUrl) if they are no longer used anywheremods = "logo=false") if they are no longer referenced anywhere (check the SSO endpoint — if the redirect logic was removed, these strings may now be dead code too)/auth/sso, return_to parameter handling) — this is already handled as part of Step 3cList these as part of the plan — they will be included in the final summary:
http://localhost:9090)http://localhost:9090/sso/metabase). Check the fetched docs to determine whether this is required or optional for the confirmed version (it depends on whether window.metabaseConfig supports a JWT provider field).Apply all changes from Step 3 in this order (backend changes first to minimize the window where things are broken):
window.metabaseConfig assignment and embed.js script tag to the layout/head file (Step 3b + 3a, config before embed.js)Constraints:
old_string / new_string for every changePerform all of these checks. Checks 5a–5c can run in parallel (all are independent grep searches). Check 5d and 5e require reading specific files. Each check should have an explicit pass/fail result.
Use Grep to search for <iframe and iframe across all project files (excluding node_modules, .git, lockfiles).
Verify that no Full App / Interactive Embedding iframes pointing to Metabase remain.
Non-Metabase iframes should be untouched. Also leave any guest embedding (formerly "static embedding") or public embedding iframes untouched — those use different URL patterns (e.g., /embed/ or /public/) and are not part of this migration.
Pass criteria: zero Full App / Interactive Embedding iframes found (guest/public embed iframes are excluded).
Use Grep to search for /app/embed.js across all project files (excluding node_modules, .git). This pattern is specific to Metabase's embed script URL and avoids false positives from other tools that may use a generic embed.js filename.
Pass criteria: exactly one occurrence in the layout/head file.
Use Grep to search for window.metabaseConfig across all project files (excluding node_modules, .git).
Pass criteria: exactly one occurrence (the assignment in the layout/head file).
Read the SSO endpoint file. Verify:
{ jwt: token }res.redirect, new URL("/auth/sso", ...), return_to) has been fully removedresponse=json exists (since JSON is the only response format now)Pass criteria: endpoint returns JSON only, no redirect fallback remains.
Read each modified file and verify:
dashboard-id, question-id, or initial-collection)Pass criteria: all checks pass.
If any check fails:
Organize the final output into these sections:
window.metabaseConfigDoc fetching:
llms-embedding-full.txt returns 404, verify the Metabase version number and retry. If still failing, mark Step 1 ❌ blocked.Validation:
tools
Drive a Metabase instance from the terminal via the `mb` CLI. Authenticate with named profiles; inspect databases (list, get, full metadata rollup, schemas, tables in a schema) and trigger manual schema sync / field-values rescan; inspect tables, fields; list/get/create/update/archive cards (questions, models, metrics) and run them as JSON/CSV/XLSX; list/get/create/update dashboards and patch dashcards; list/get/create collections and traverse the hierarchy by id, entity_id, or "root"/"trash" (with items and recursive tree); list/get/create/update/archive native query snippets, segments, and measures; author/update/run transforms and schedule transform-jobs; read/update settings; search content (cards, dashboards, collections, transforms, metrics); manage Enterprise workspaces; git-sync to/from a git remote (status, dirty, import, export, branches, stash, add/remove a collection from sync). Use whenever the user wants to interact with a Metabase from the terminal — "log into metabase", "what profiles do I have", "list cards", "run card 42 as CSV", "create a transform", "list dashboards", "move a dashcard", "list collections", "what's in collection 4", "show the collection tree", "list snippets", "create a segment", "archive a measure", "search metabase for X", "spin up a workspace", "import the latest changes", "add a directory to git sync", "set a setting", "what schemas are in this database", "trigger a sync", "rescan field values", or anything hitting `mb <verb>`.
development
Runs the Metabase semantic checker against a tree of Representation Format YAML files to verify that all references resolve — cross-entity references (collection_id, dashboard_id, parent_id, parameter source cards, snippet references, transform tags, etc.) and references to columns inside MBQL and native queries. Slow (≥1 min per run). Only use when the user explicitly asks to verify entity references or column references in MBQL/SQL queries; in most cases this runs as a CI step, not locally. Requires database metadata on disk (by default `.metadata/table_metadata.json`).
development
Understands the Metabase Database Metadata Format — a YAML-based on-disk representation of databases, tables, and fields synced from a Metabase instance. Use when the user needs to read, edit, or understand metadata files produced by `@metabase/database-metadata`, or when reasoning about a project's schema (columns, types, FK relationships) through the `.metadata/databases` folder.
development
Understands the Metabase Representation Format — a YAML-based serialization format for Metabase content (collections, cards, dashboards, documents, segments, measures, snippets, transforms). Use when the user needs to create, edit, understand, or validate Metabase representation YAML files, or when working with Metabase serialization/deserialization (serdes). Covers entity schemas, MBQL and native queries, visualization settings, parameters, and folder structure.