Skip to main content

metabase-data-app-setup

Scaffold a new Metabase data-app into the connected remote-sync repository's `data_apps/<app>/` directory from the `data-app-template`. Use when the user asks to start, create, scaffold, or set up a data-app from scratch.

Jump to install

Source facts

Repository
metabase/metabase
Last source activity
September 16, 2026 at 07:40
Detected SKILL.md language
English
Stars
49,286
Forks
6,823

Install options

The review-first prompt is selected by default. You can switch to a direct command or download a local copy.

Review the source files

Read SKILL.md and any companion files shown by SkillsMP before deciding whether to install.

File Explorer
12 files

Showing SKILL.md

SKILL.md
Source instructions · Read-only preview
name
metabase-data-app-setup
description
Scaffold a new Metabase data-app into the connected remote-sync repository's `data_apps/<app>/` directory from the `data-app-template`. Use when the user asks to start, create, scaffold, or set up a data-app from scratch.
# Create a Metabase Data App A Metabase **data-app** is a single JS bundle that the host loads inside a Near Membrane sandbox and renders inside its own React tree. The scaffold is a Vite + React + TypeScript project: source under `src/`, a dev server that previews the app against a real Metabase **through the same Near Membrane sandbox + distortion rules Metabase uses in production** — so `npm run dev` behaves like production, including for third-party libraries the app bundles — and `npm run build` producing a single `dist/index.js`. (Because the sandbox runs a built bundle, a change rebuilds it and does a *soft reload* — re-evaluates the bundle in the sandbox and remounts the app, keeping auth/SDK loaded — rather than hot-swapping modules; component state resets, but there's no full browser refresh.) The dev preview also shows a corner **⚠ Diagnostics** toolbar that captures runtime errors — including the sandbox's otherwise-opaque blocked-API messages — so failures surface instead of being swallowed. The same data is served as JSON at `http://localhost:5174/__data-app/diagnostics`, which is how *you* read it (see "Reading the diagnostics feed" below) — you have a shell, not a browser, and these failures are invisible from the terminal otherwise. **Data apps are served from Git, not uploaded.** A single repository is connected to Metabase via remote-sync (Admin → Settings → Remote sync). Each app lives in its own directory `data_apps/<app>/` inside that repo — its source, a `data_app.yaml` (name/path), and the committed built bundle at the `path` its `data_app.yaml` declares (`dist/index.js` by default). On each remote-sync import Metabase materializes one app per directory and serves it at `/apps/<slug>` url, where the slug **is** the directory's name. So this skill always scaffolds **into the connected repo's `data_apps/<app>/` directory**, never as a standalone project. **The scaffold ships inside this skill at `./template/`** — a Vite + React + TypeScript project that was installed alongside the skill. Step 3 just copies it into the app directory; the skill then guides you through the customization + first-app-content steps — it never generates project files from scratch. If you find yourself writing `package.json`, `vite.config.ts`, `tsconfig.json`, or `src/index.tsx` by hand, stop — copy the template instead. ## When to invoke this skill - "scaffold a new data app" / "create a Metabase data app" / "set up a data-app project" - "I want to build a data app" / any vague intent to author a data app - Starting a fresh agent task that will produce a data-app bundle. - Do **not** use this skill for an existing data-app project when the task is to build screens, use Metabase data, generate or refresh schema files, wire saved questions / tables / metrics / actions, add filters, or author data hooks. Treat those as existing-data-app editing tasks and use the agent's normal skill-discovery flow from the user's wording. ## Step 1 — Locate the remote-sync repository Data apps live inside the Git repository connected to Metabase via remote-sync. Find it before scaffolding: - Ask: **"Do you already have a Git repository connected to this Metabase via remote-sync?"** - **Yes** → ask for its local path. - **No** → ask the user to **create one** (a plain Git repo they will connect under Admin → Settings → Remote sync) and share its path. The skill does **not** create or connect the repo — the user owns that. - Verify the path exists and is a Git working tree (it has a `.git`). This repo is the working directory for every step below. ## Step 2 — Name the app and create its directory 1. Settle on the app's **directory name** before scaffolding — it is used verbatim as the slug (the `/apps/<slug>` URL), so it **must be dash-cased**: lowercase letters, numbers, and single dashes (`[a-z0-9]+(?:-[a-z0-9]+)*`), e.g. `sales-overview`. Anything else (uppercase, spaces, underscores) is rejected on sync. If the purpose isn't clear yet, ask a one-line "what's this app for?" and propose a name; confirm it. 2. Ensure `<repo>/data_apps/` exists; create it if missing. 3. Create `<repo>/data_apps/<slug>/`. **If it already exists**, treat it as an existing project (see below) — never overwrite without confirmation. ### Detecting an existing app If `<repo>/data_apps/<slug>/` already holds a project, verify it matches the current `data-app-template`. Check **all** of: 1. `vite.config.ts` is a one-liner: `export default dataAppConfig()` (from `@metabase/embedding-sdk-react/data-app-dev/config`). There is **no** local `config/` directory: the whole bundle contract (externals/globals, the dev sandbox entry, CSS/SVG handling) lives inside that SDK config, not the scaffold. `dataAppConfig` takes only a curated set of overrides (currently just `port`); the contract plugin is always applied and can't be overridden. 2. `src/index.tsx` default-exports a `DataAppFactory` (type from `@metabase/embedding-sdk-react/data-app`) returning `{ component, providerProps? }` (no args). **All checks pass** → template-shaped. Ask: "Extend this app, or scaffold a new one under a different slug?" If extend → skip the copy step, edit `src/`. If new → pick a different slug and restart at Step 2. **Any check fails** → not template-shaped (older scaffold or drift). **Stop.** Tell the user the structure differs from the current template, extending it risks breaking the bundle contract, and ask whether to (1) migrate it, (2) scaffold fresh under a new slug and port the code over, or (3) proceed anyway at their risk. Wait for the answer. Never overwrite existing files without explicit confirmation. ## Step 3 — Copy the template into the app directory The template ships **inside this skill** at `./template/` (installed alongside the skill via `skills add metabase/metabase/skills#release-x.<major>.x`). Copy it into the app directory: ```bash APP_DIR="<repo>/data_apps/<slug>" # `<skill-dir>` = the directory this SKILL.md was loaded from # (e.g. `.claude/skills/metabase-data-app-setup`); the template is its `template/` subfolder. cp -R "<skill-dir>/template/." "$APP_DIR/" ``` A data app is a *subdirectory* of the remote-sync repo, not its own repository — so this is a plain copy, never a nested `git clone` / `git init`. Everything below runs **inside `$APP_DIR`**. ## Step 4 — Customize Once the template is in `<repo>/data_apps/<slug>/` (run everything below from that directory): 1. Edit `package.json` `name` to match the slug. 2. Pin `@metabase/embedding-sdk-react` to the published data-apps tag (the template ships with `*`): ```bash npm install @metabase/embedding-sdk-react@64-alpha ``` This resolves to the current internal-testing SDK build with the `@metabase/embedding-sdk-react/data-app` entrypoint (the app's APIs) and the `@metabase/embedding-sdk-react/data-app-dev/config` entrypoint `vite.config.ts` uses (the dev/build preset, which serves the sandbox entry). Do not use `latest` or `64-stable` as data apps are not yet released. 3. **Ensure the repo-root `.gitignore` ignores `.env.local`** — do this *before* creating any credentials file so the secret can never be committed. Create the `.gitignore` if the repo doesn't have one, then add the entry if it's missing: ```bash ROOT="$(git rev-parse --show-toplevel 2>/dev/null)" if [ -z "$ROOT" ]; then echo "MISSING (run this from inside the connected git repo)" else GITIGNORE="$ROOT/.gitignore" # Create the repo-root .gitignore if absent, then ensure `.env.local` is # ignored so the credentials file (next step) can never be committed. [ -f "$GITIGNORE" ] || : > "$GITIGNORE" grep -qxF ".env.local" "$GITIGNORE" || echo ".env.local" >> "$GITIGNORE" fi ``` 4. Set up the Metabase credentials at the **repository root** — `<repo>/.env.local` (usually two levels up from the app dir), **not** the app dir. One `.env.local` there serves every data app in the repo. Create it from the example if absent, then verify the two vars are set **without printing the file** (it may hold other secrets) — `source` it and echo only a pass/fail signal, never the values: ```bash # Resolve the repo root first; an unguarded $(git ...) would expand to # "/.env.local" outside a repo and touch a system-level file. ROOT="$(git rev-parse --show-toplevel 2>/dev/null)" if [ -z "$ROOT" ]; then echo "MISSING (run this from inside the connected git repo)" else ENV_FILE="$ROOT/.env.local" [ -f "$ENV_FILE" ] || cp .env.local.example "$ENV_FILE" # Source inside a subshell so the vars never leak into your environment. ( source "$ENV_FILE" 2>/dev/null [ -n "$DATA_APP_MB_URL" ] && [ "$DATA_APP_MB_URL" != "mb_replace_me" ] && [ -n "$DATA_APP_MB_API_KEY" ] && [ "$DATA_APP_MB_API_KEY" != "mb_replace_me" ] ) && echo "creds present" || echo "MISSING" fi ``` If it prints `MISSING`, **ask the user to fill `DATA_APP_MB_URL` (the running Metabase instance) and `DATA_APP_MB_API_KEY` (Admin → Authentication → API keys) in `<repo>/.env.local` themselves** — up front, before anything needs the key. > **Never ask the user to paste the API key into the chat, and never `cat` / `echo` / print `.env.local` or its variables.** It's git-ignored and may hold *other* secrets — the file's contents and the key must never enter the conversation or your context. Every command that needs the key `source`s the file (as above) so the shell uses the value directly; you only ever see the `creds present` / `MISSING` signal, never the secret itself. (`creds present` only means both vars are filled and not the default `mb_replace_me` placeholder — not that the URL or key are valid; a bad key surfaces later when a request fails.) 5. `npm install` (or whichever package manager the user prefers — the template ships with no lockfile, so `npm` / `yarn` / `pnpm` / `bun` all work; use the project's existing lockfile if one appears post-clone). 6. **Fix the app's `.gitignore` so the lockfile *and* the built bundle get committed.** Two things must end up tracked in the remote-sync repo: - **Lockfile** — strip the lockfile-ignoring block (the chunk between `# Lockfiles —` and `bun.lockb`, covering `package-lock.json` / `yarn.lock` / `pnpm-lock.yaml` / `bun.lock` / `bun.lockb`) so the project commits its lockfile for reproducible installs. - **The built bundle** — Metabase serves the file at the `path` declared in `data_app.yaml` (the template builds to `dist/index.js`, the default `path`) straight from the committed Git tree, so **that file must be committed**. If the template's `.gitignore` ignores `dist/` (or wherever your build outputs), remove that line. **Verify with `git status`** — after `npm install` + a build, both the generated lockfile and the built bundle (the file `path` points at) must appear as untracked/committable files. If either doesn't, the relevant `.gitignore` line is still there; remove it and re-check. Do **not** skip this — agents have repeatedly shipped projects with no committed lockfile or an un-synced bundle. 7. `npm run dev` and confirm the preview at http://localhost:5174 renders the starter "Hello, data app" message. 8. If the preview hits CORS, add `http://localhost:5174` under Admin → Embedding → Embedded analytics SDK → CORS. 9. **Edit `data_app.yaml`** (it ships with the template, in the app directory). This is the per-app config Metabase reads on sync — one file per app. Fill in its fields for this app: ```yaml name: Sales App # display name shown in the admin UI description: Pipeline health and quota attainment by region # optional — see below path: ./dist/index.js # bundle path, relative to this app's directory — leave as-is unless you change the build output # allowed_hosts: # optional — external origins the app may fetch/XHR (see below) # - https://api.example.com # - https://*.internal.acme.com ``` Commit it alongside the built bundle (the file `path` points at). **`description`** — optional: a single short sentence saying what the app does, shown under its name in the admin UI so admins can tell apps apart at a glance. Sync folds any whitespace into single spaces and rejects anything over 255 characters; the admin list wraps what is left rather than cutting it off, so a sentence reads well there and a paragraph crowds out the rows around it. Replace the template's placeholder with a real sentence about *this* app, or delete the line entirely if it adds nothing beyond the name. **`allowed_hosts`** — only needed if the app calls an **external** API directly with `fetch`/`XHR`. The sandbox blocks all network egress by default; listing an origin here (exact or a `*.` subdomain wildcard) opens it in both `npm run dev` (dev-server CSP) and Metabase (iframe CSP + sandbox). Do **not** list the Metabase instance — Metabase data is read via the `useMetabaseQuery` data hooks and written via `useAction` (the SDK handles auth), never raw `fetch`. Leave `allowed_hosts` out entirely when the app only talks to Metabase. Native `<form action="…">` submissions and `<iframe src="…">`/navigations obey the **same** allowlist. Prefer a client-side `<form onSubmit>` that `preventDefault`s and writes via `useAction`/`fetch`: a native submit *navigates the sandboxed iframe away* from the app. If you do use one, the target host must be in `allowed_hosts` or it's blocked (`form-action` for submits, `frame-src` for embeds/navigations). A host you navigate to or embed must also permit framing (`X-Frame-Options`/`frame-ancestors`) — many public sites don't. ## Step 5 — Verify the starter app At this point the data app exists. Keep this workflow focused on creating the project scaffold and proving the starter bundle works. 1. Run `npm run typecheck`. 2. Run `npm run build`. 3. Confirm `git status` shows the app source, lockfile, `data_app.yaml`, and the built bundle (`dist/index.js` by default) as committable files. 4. If the user asked for a live preview, run `npm run dev` and confirm the starter "Hello, data app" screen renders through the sandbox preview. 5. With the preview open, check the diagnostics feed once — it is the only place runtime failures appear to you (see *Reading the diagnostics feed*): ```bash curl -s "http://localhost:5174/__data-app/diagnostics?startEventId=0" ``` Expect `clients: 1` and no entries with `"alert": true`. `clients: 0` means no preview tab is open, so an empty feed proves nothing. Stop here if the user only asked to create, scaffold, or set up a data app. If the next task is to build or iterate on the actual app UI — especially if it mentions an existing data app, Metabase data, generated schema files, saved questions, tables, metrics, actions, filters, semantic-layer entities, or data hooks — treat it as a separate existing-data-app editing task. Use the agent's normal skill-discovery flow from those terms. Do not expand this scaffold skill with data-layer authoring rules. **Do not modify `src/index.tsx` or `tsconfig.json` unless the change is genuinely required.** The whole build/dev setup lives in the SDK behind `dataAppConfig()` (which also serves the dev HTML shell — there's no `index.html` to edit), so `vite.config.ts` is just: ```ts import { dataAppConfig } from "@metabase/embedding-sdk-react/data-app-dev/config"; export default dataAppConfig(); ``` `dataAppConfig` exposes only a curated set of overrides (currently just `port`). The whole contract — factory shape, externals/globals, the dev sandbox entry, CSS inlining, and SVG-as-component support — is baked in and **can't** be overridden; that's deliberate, so a data app can't drift from what Metabase loads. There's no local build config to touch (and no `index.html` — the SDK's dev server serves the HTML shell), and tweaks to `src/index.tsx` still risk breaking the factory shape and silently break drill popups and routing. There is intentionally **no escape hatch** for extra Vite plugins, aliases, or `define`s — `port` is the only knob. If you think you need more, you almost certainly don't; solve it in `src/` instead. **After every meaningful round of edits, run `npm run typecheck`.** It runs `tsc --noEmit` over `src/` and `vite.config.ts` — catches wrong prop shapes against the SDK types, broken refactors, missing imports, etc. The Vite dev server does NOT typecheck (it only transpiles), so errors that would fail a production CI run can sit invisibly in a passing `npm run dev` session. Run it before declaring a task complete. **Before handoff, re-check package hygiene.** `@metabase/embedding-sdk-react` should use the expected data-app SDK source/tag for the target environment. No date picker dependency should be installed when the app only needs date ranges — not `react-datepicker`, `react-day-picker`, `flatpickr`, or a UI suite's picker (`@mui/x-date-pickers`, `antd`, `rsuite`, …); that is `DateRangePopover` from `@metabase/embedding-sdk-react/data-app`. `@types/react-datepicker` should not be installed unless the chosen `react-datepicker` version actually needs it. ## Reading the diagnostics feed `npm run dev` serves everything the toolbar shows as JSON — the only way *you* see runtime failures, since sandbox blocks, CSP refusals, failed queries and uncaught errors reach neither the terminal nor `npm run typecheck`. Check it after any change you can't verify by reading the code. **Loop:** note `nextEventId` before editing → make the change (rebuilds automatically) → re-read. `startEventId` is inclusive and survives page reloads. ```bash curl -s "http://localhost:5174/__data-app/diagnostics?startEventId=0" ``` ```jsonc { "entries": [{ "eventId": 31, "kind": "blocked-network", "alert": true,
View on GitHub
This SKILL.md is very large, so SkillsMP previews the first section here. View on GitHub