Developing Lightdash Data Apps Locally
You are editing a Lightdash data app that was created or downloaded with the Lightdash CLI.
The only way to reach data is the SDK
- All app data access goes through
@lightdash/query-sdk. Deployed apps use Lightdash's postMessage bridge;lightdash apps previewuses a loopback proxy restricted to the same SDK routes and project. Do not add directfetchor REST calls. - For the SDK surface (query builder,
useLightdash, filters, downloads), read thelightdash-data-appskill in this folder.
External HTTP APIs go through linked connections
- The one sanctioned path to a third-party API is
lightdash.externalFetch('<alias>', ...)against an external connection a project admin configured and linked to this app (see thelightdash-data-appskill). - The app's links are declared in this folder's
lightdash-app.ymlunderexternalConnections(- alias: stripe/connectionSlug: stripe-api). When the key is present, upload reconciles the app's links to match it exactly — so to link a connection that already exists in the project, add its{alias, connectionSlug}entry and upload. Do not remove entries casually: an entry removed from the list (or an empty list) unlinks on upload, and a manifest without the key leaves links unchanged. - Watch the upload output: a
connectionSlugthat doesn't exist in the target project is skipped with a warning (the app uploads, butexternalFetchon that alias fails at runtime). Linking requires either the admin-levelmanage:ExternalConnectionpermission or an admin-enabled connection plus permission to manage the target data app — if a link is skipped or forbidden, report it to the user instead of retrying. - If the app needs an API with no existing connection, stop and say so — do not vendor an HTTP client or try to reach it another way. An admin must create the connection first (in project settings, or as code:
lightdash download --include-external-connections, editlightdash/external-connections/<slug>.yml,lightdash uploadwith the secret inLIGHTDASH_EXTERNAL_CONNECTION_SECRET_<SLUG>); then it can be linked via the manifest as above.
Library boundaries — build with what's preinstalled
- The app builds against a fixed template dependency set — see
package.json(React, Recharts, d3 + d3-cloud/d3-sankey, Radix primitives, Tailwind, lucide-react, date-fns, html-to-image, jspdf, and more). Design within this set; it covers almost all data-app needs. - Adding new npm packages only works when the Lightdash organization has custom dependencies enabled — assume it does not. When disabled, upload rejects new dependency declarations. Do not attempt
pnpm addto find out; ask the user whether their organization has custom dependencies enabled before considering a new library. - Do not work around a missing library by vendoring its source into
src/, inlining minified code, or fetching code at runtime. If the template set genuinely cannot express what's asked, say so and let the user decide.
The edit → build → upload loop
Run these commands from this folder under apps/<slug>/. The upload path ../.. selects the Lightdash content root.
- Edit files under
src/only. - Run
lightdash apps validateto check the source, manifest, dependencies, external-connection aliases, and semantic references against the downloaded context. Uselightdash apps validate --buildto add the Cloud-parity Vite production build; use--liveto check against fresh project explores or--format jsonin CI. A green run reports any call sites it could not fully analyze instead of silently claiming complete coverage. lightdash upload --apps <slug> --path ../..(theslugfrom this folder'slightdash-app.yml) — the server rebuilds and serves the app. The server rebuild remains the final result that ships.
Cloud-parity local builds
lightdash apps validate --buildassemblessrc/in an isolated copy of the CLI's trusted template and invokes barevite build— it deliberately does not typecheck because the Cloud build does not typecheck.- Apps made with
lightdash apps createalready have the standard template dependencies installed. Afterlightdash download, runnpm installbefore requesting--buildifnode_modulesis absent. Validation does not install the standard dependency set implicitly. - Apps with custom dependencies are restored into the isolated build directory with
pnpm install --frozen-lockfile --ignore-scripts, matching Cloud. An install or Vite failure is a validation error, includes the command output, and exits non-zero. - Do not modify machine configuration,
.npmrcfiles, registry settings, or the project's dependency files to force the check to pass. The server rebuild on upload remains authoritative. - Exception — adding a dependency (only for organizations with custom dependencies enabled — see "Library boundaries" above). This is the one workflow that still requires pnpm: upload rejects new dependencies unless
pnpm-lock.yamlwas regenerated to matchpackage.json, so dependency resolution MUST succeed locally. Usepnpm add <pkg>— prefixed with Socket Firewall when available (sfw pnpm add <pkg>; check withcommand -v sfw) to block known-malicious packages — or after editingpackage.jsonrunpnpm install --lockfile-only(updates the lockfile without installing). If resolution fails, stop and report the exact pnpm error to the user — never hand-editpackage.jsonand proceed without the lockfile; the upload will fail. - Never run dependency lifecycle scripts. The app's
.npmrcsetsignore-scripts=true— leave it. A downloaded app can be authored by someone else, and their dependencies' install scripts must not execute on this machine. Explicitnpm run build/npm run devandpnpm build/pnpm devcommands still work.
Preview locally against real data
lightdash apps preview (run in this folder) starts a local dev server that renders the app against the Lightdash instance you are logged into, using your CLI credential. Requires npm install to have succeeded; if it hasn't, skip preview and rely on the server rebuild.
- Preview does not pass your API key to vite or browser code: the CLI holds it behind a loopback proxy that only forwards the SDK route allowlist (query execution, result polling, downloads, current user), pinned to this app's project. No credential is written to the app folder. Never put a real key in
.env.localor anyVITE_-prefixed var — anythingVITE_*is inlined into the page and readable by any code running there. - There is no manual
npm run devequivalent with data access — barenpm run devstarts the page but API calls fail with 401. Always uselightdash apps preview. - An endpoint that works in preview but not when deployed means it is outside the data-app SDK surface — use the SDK, don't work around the proxy.
- Declared custom dependencies work in preview too — the dev server bundles whatever the dependency install put in
node_modules, the same set the server installs on upload. - Preview shows your data under your permissions and user attributes — viewers of the deployed app may see different data. Do not treat preview as verification of viewer-specific behavior.
- Local preview keeps the credential out of the app environment/browser but is not a sandbox: vite and the downloaded tooling execute as your OS user, can read that user's files (including the existing CLI config), and the app can read the query results it requests. Only preview source and dependencies you trust.
- The dev server applies a CSP that forces API traffic through its same-origin proxy, but
script-srcstays permissive (vite needs it), so preview is not a full stand-in for the deployed Content-Security-Policy. A library that works in preview may still be blocked when deployed; the app page after upload is the final check. - Host-mediated features are not emulated locally. External connections (
externalFetch), data-app-viz row/field context, Google Sheets export, the product inspector, and product URL-state integration must be tested after upload.
Project context (read-only reference)
.lightdash/context/ holds a point-in-time snapshot of the source project:
models/— the semantic layer, one YAML file per model: the real tables/dimensions/metrics you can query. Use these exact names. Start withmodels/_index.md, then read only the model files you need. (Apps downloaded with an older CLI carry the whole semantic layer insemantic-layer.ymlinstead and have nomodels/.)parameters.yml— project-wide parameters (if any).prompt-history.md— the prompts used to generate each version.theme/— styling instructions and assets.
Read-only files
Most root config is reference only — editing it has no effect because the server rebuilds against its trusted template. This applies to vite.config.js, tailwind.config.js, tsconfig.json, and other build/tooling files.
package.json is partially editable only when custom dependencies are enabled for the Lightdash organization — see "Library boundaries" above; treat it as read-only otherwise. When enabled, you may add npm dependencies with pnpm add <pkg> — registry packages with plain semver versions only (no git/file/url specs), up to 60 direct dependencies, and pnpm-lock.yaml must be updated alongside (see the exception above — this is the one step that still requires pnpm and must succeed locally). On upload the CLI warns which packages will be installed in the build sandbox; install scripts never run. Other root config (vite/tailwind/tsconfig) remains read-only.

