Building data apps locally requires data apps, including the build sandbox, to be enabled on your Lightdash instance. See Self-hosting data apps if you run your own instance.
- Download an app’s source, check it into git, and review changes in pull requests.
- Edit the app in your own editor, or point your own AI coding agent at it.
- Preview the app locally against real Lightdash data, with your permissions.
- Upload your changes — the server rebuilds the app and publishes a new version.
- Create brand-new apps from scratch, without going through the UI at all.
- Move apps between projects and instances.
lightdash/chart-types/ and move with the --chart-types flags instead of --apps. See Chart types as code.
Prerequisites
- The Lightdash CLI installed and logged in (
lightdash login <your-instance-url>), version 1.55.0 or later. - Data apps enabled on the instance (enterprise).
- An Editor role or above on the project — uploading requires the
create:DataAppscope. Non-Editors can be grantedcreate:DataApp@previewinstead to upload only to preview projects they created themselves; see Restrict data app uploads to previews. - Node.js 20+ (which includes npm) for local builds, previews, and
lightdash apps create.
Download, edit, and upload an app
Start with an app built in the Lightdash UI, or create an app locally.1. Download the app
Apps are identified by their slug — a project-scoped id derived from the app’s name. You can pass a slug, a UUID, or simply paste the app’s URL:--apps-only gives you a clean, app-only checkout — no charts, dashboards, or space files. The app lands in lightdash/apps/revenue-explorer/.
2. Look around
Every downloaded app is a locally buildable Vite + React project:
The built output (
dist/) is intentionally never downloaded or uploaded — the server always rebuilds from source.
The snapshot covers every model in the project, not only the ones the app queries, so --apps-only doesn’t make it smaller. It’s generated on each download, so there’s no value in reviewing its diffs. When you commit apps to git, add .lightdash/ to .gitignore, and add .claude/ and .agents/ too if your team doesn’t use AI coding agents on the app. The generated .gitignore already excludes node_modules, dist, and .env*.
3. Make a change
Edit anything undersrc/. If you’re using an AI coding agent, point it at the app folder — the bundled AGENTS.md and .claude/skills/ teach it the ground rules, and the model files in .lightdash/context/models/ tell it which fields exist.
To sanity-check that the source still compiles:
lightdash-app.yml, pnpm-lock.yaml, external connection aliases, and the fields your code references against the local .lightdash/context/ snapshot. Add --live to check against fresh explores from the project selected with lightdash config set-project (or set in LIGHTDASH_PROJECT) instead. See lightdash apps validate for the full flag list.
Both modes check against explores deployed to Lightdash, not your local dbt files. apps validate only checks the app folders you give it; the validator checks every app in the project, including apps never downloaded as code.
4. Preview it against real data (optional)
http://localhost:5173) and edit src/ with instant hot reload.
See Local preview for details and limitations.
5. Upload
src/ tree and manifest, then returns immediately:
Data apps are not enabled, data apps aren’t turned on for your organization. On a self-hosted instance, see Self-hosting data apps.
That’s the whole loop: edit src/, upload, repeat.
Creating a new app locally
You don’t need the UI to start an app.lightdash apps create scaffolds a complete, buildable app folder:
- Checks the slug is available in the target project (derived from the name; override with
--slug). - Asks for confirmation, then installs the template’s npm packages and generates the shadcn UI components locally (this is why npm is required). Pass
-yto skip the prompts in scripts. - Writes a ready-to-edit app folder with the same layout as a downloaded app, including the
.lightdash/context/snapshot of your project’s semantic layer.
--description <text>, --slug <slug>, --project <uuid> (defaults to your selected project), -p, --path <path> (content root, default ./lightdash).
Understanding identity: slugs
Theslug field in lightdash-app.yml is the app’s identity, and it’s scoped to a project — the same slug can exist in different projects. On upload, the server upserts by slug in the target project:
- Slug exists there → a new version is appended to that app.
- Slug doesn’t exist there → a new app is created with that slug.
name (or description) in the manifest and upload. The slug — and therefore the app’s identity and URLs — doesn’t change.
Downloaded manifests don’t contain the app’s or the project’s UUID. CLI versions that predate slugs can’t upload these manifests, so keep every machine and CI pipeline that uploads apps on the CLI version in Prerequisites. Bundles downloaded before slugs existed still carry
appUuid and projectUuid and upload to the same project by matching appUuid; re-download them to pick up a slug.Copying an app instead of updating it
Pass--create-new to force a fresh app (with a newly generated slug) instead of updating the one the manifest points at — useful for forking:
Moving apps between projects and instances
Between projects on one instance, target the other project:LIGHTDASH_URL and LIGHTDASH_API_KEY):
spaceSlug records the app’s space as a path. On upload, the app is placed in that space in the target project, and the space is created if it doesn’t exist there. A manifest without spaceSlug (a personal app) leaves an existing app where it is and creates a new one as a personal app.
Pass --app-space <spaceRef> (slug or space UUID) to land any new apps this upload creates in a specific space instead. It takes precedence over spaceSlug and only affects the first upload of an app to a project:
Versioning
Every accepted upload creates a new version of the app, alongside versions created by prompting in the UI. Versions move throughpending → building → ready (or error), and viewers always see the latest ready version. The full timeline — including who created each version and any build errors — is in the app’s version history in the UI, where you can also restore an earlier version.
Uploads don’t merge changes or check for conflicts. Each upload becomes the newest version, so it replaces any changes made in the UI or uploaded by someone else since you downloaded the app. If that happens, restore the overwritten version from version history.
Two behaviors keep the version history clean:
- Unchanged uploads are skipped. If the source, dependencies, and viz schema are byte-for-byte identical to the latest version, the server skips the rebuild and reports
matches v<n> — skipped, no rebuild. Name/description edits still apply. Pass--forceto rebuild anyway. (A latest version inerrorstate is never skipped — re-uploading retries the build.) - Build slots are limited. Each project runs at most 5 app builds at once. If you upload a large batch, the CLI automatically waits for a free slot (up to 10 minutes of consecutive waiting) instead of failing — you’ll see
Project build limit reached — waiting for builds to finish….
lightdash upload exits non-zero if any app upload fails, and prints a per-run tally: data apps created / updated / unchanged / failed / skipped.
We recommend treating git as the source of truth for locally-managed apps: commit the app folder, make changes via PRs, and upload from CI or after merge. Re-download after someone iterates on the app in the UI, so your local copy doesn’t silently drift behind.
Data apps on dashboards
Dashboards reference data app tiles portably, byappSlug in the dashboard YAML. The CLI keeps the two in sync:
lightdash download(with dashboards) also downloads any data apps the dashboards reference, intolightdash/apps/.lightdash upload(with dashboards) uploads referenced app folders before the dashboards — even without any--appsflag — so the tiles resolve. Unchanged apps that already exist in the target are skipped.
Local preview
lightdash apps preview runs a downloaded (or locally created) app on your machine against a real Lightdash instance:
http://localhost:5173). Edits to src/ hot-reload instantly, and every query the app runs goes to your real project.
How authentication works. Your API key never reaches the app or the browser. The CLI starts a loopback proxy that holds your credential; the dev server forwards the app’s API calls through it. The proxy only allows the same narrow set of SDK routes a deployed app can reach (running queries, polling results, downloads), pins requests to one project, and strips cookies in both directions. A bare npm run dev without the CLI has no data access at all.
Things to know:
- You see your own data. Preview runs with your permissions and user attributes. Viewers of the deployed app may see different results.
- Pick the project with
--project <uuid>(defaults to the manifest’s project) and override credentials with--url/--token(preferlightdash loginorLIGHTDASH_API_KEYover--token, which leaks into shell history). - Not everything is emulated. External connection fetches, Google Sheets export, the network inspector, data-app-viz field context, and URL state integration only work in the deployed app.
- Apps downloaded with an older CLI may predate the preview proxy — re-download the app if preview tells you the scaffolding is too old.
External connections
If an app fetches from third-party APIs via external connections, the manifest records the links:connectionSlug is resolved in the target project:
- If the field is present, the app’s links are reconciled to match it exactly (an empty list
[]unlinks everything). - If the field is absent, existing links are left untouched.
- A slug that doesn’t exist in the target is skipped with a warning — the app still uploads, but that fetch will fail at runtime until the connection is linked.
lightdash/external-connections/<slug>.yml and upload before apps in the same run, so the links resolve. Managing connections requires the admin-only manage:ExternalConnection scope.
Advanced: how the build works
You never upload compiled output — the server builds every version from source in an isolated sandbox:lightdash uploadposts the manifest and thesrc/tree. The server validates the bundle, stores the source, and creates a new version inpending.- A build job restores the source into a sandbox containing the trusted app template. Any
scriptsin an uploadedpackage.jsonare replaced with the template’s — the build command is never uploader-controlled. - The sandbox runs the standard Vite production build. There’s no AI involved and no auto-fixing: the build either succeeds or fails loudly.
- On success the compiled app is published and the version becomes
ready. On failure the version is markederrorwith the compiler output attached in the version history.
npm run build is only a convenience pre-check — a local failure (from a Node version mismatch, say) doesn’t predict a server failure, which is why the CLI treats it as a warning.
For package and lockfile limits, see Custom dependencies.
Custom dependencies
See Custom dependencies to add packages, check the upload requirements, and troubleshoot dependency errors.CLI reference
Seelightdash apps create, lightdash apps preview, lightdash apps validate, and the data app flags on lightdash download and lightdash upload for the full flag lists.