- Python 3.12+
- Node.js 18+ (provides
npm) — install viabrew install node, nvm, or nodejs.org — and yarn classic v1 (npm install -g yarn, used for the committedapp/yarn.lock;bun.lock/package-lock.jsonare gitignored) - bun — used by
make app-checkto runtsc -b(TypeScript incremental compile). Install viacurl -fsSL https://bun.sh/install | bashorbrew install oven-sh/bun/bun. - uv — Python package manager
- Databricks CLI v1.4.0+ — install per the official guide (the legacy
databricks-cliPyPI package is unrelated and not supported). Verify withdatabricks --version. v1.4.0 is the minimum formake app-deploybecause the bundle'spostgres_projects/postgres_rolesresources are only accepted by CLI ≥ 1.4.0. - Access to a Databricks workspace
Every workflow runs through make targets at the project root. The
underlying invocations are pure-Python orchestrators (scripts/build_app.py,
scripts/dev.py) plus bun / uv / node_modules/.bin/* — no
project-specific CLI required. Prefer make from the project root.
make (from root) |
What it does |
|---|---|
make app-install |
Install JS dependencies (yarn) |
make app-build |
Compile UI, generate OpenAPI schema, package wheels (runs app/scripts/build_app.py) |
make app-start-dev |
Build then start uvicorn + vite via app/scripts/dev.py (foreground; Ctrl+C to stop) |
make app-stop-dev |
Stop dev servers started in another shell (pkill-based) |
make app-check |
TypeScript (tsc -b) + Python (basedpyright) type-check |
make app-regen-api |
Regenerate ui/lib/api.ts after backend model changes (no full wheel rebuild) |
make app-test |
Backend pytest suite |
make fmt |
Format Python (run from root before committing) |
make test |
Library unit tests |
make integration |
Integration tests (requires live workspace) |
Lock files (
yarn.lockanduv.lock) must be committed to ensure reproducible builds.
Option A — Databricks CLI (recommended):
databricks auth login --host https://your-workspace.cloud.databricks.comOption B — .env file (useful when working with multiple profiles):
Create a file at app/.env (git-ignored) with the variables below, filling in your own values:
DATABRICKS_CONFIG_PROFILE=<your-profile> # matches a profile in ~/.databrickscfg
DATABRICKS_WAREHOUSE_ID=<your-warehouse-id>
DQX_CATALOG=dqx # Unity Catalog catalog name
DQX_SCHEMA=dqx_studio # schema inside the catalog
DQX_JOB_ID=<task-runner-job-id> # required for profiler/dry-run
DQX_WHEELS_VOLUME=/Volumes/dqx/dqx_studio/wheels # UC volume path; auto-set by DABs in production
DQX_ADMIN_GROUP=admins # workspace group granted bootstrap Admin access; AppConfig default is unset (no bootstrap admin locally) — set this to your test admin group
# Lakebase (optional — leave DQX_LAKEBASE_ENDPOINT empty to run OLTP tables on Delta locally)
DQX_LAKEBASE_ENDPOINT= # e.g. projects/dqx-studio-db/branches/dqx/endpoints/primary; empty = Delta-only mode
DQX_LAKEBASE_DATABASE_NAME=databricks_postgres # logical Postgres DB; defaults to the always-present admin DB
DQX_LAKEBASE_SCHEMA=dqx_studio # Postgres schema (default: dqx_studio)
DQX_LAKEBASE_POOL_MIN_SIZE=1 # psycopg connection pool floor
DQX_LAKEBASE_POOL_MAX_SIZE=10 # psycopg connection pool ceiling
DQX_LAKEBASE_TOKEN_REFRESH_MINUTES=50 # OAuth token refresh cadence (token expires at 60)DQX_JOB_ID, DQX_WHEELS_VOLUME, DQX_LAKEBASE_ENDPOINT, and DQX_LAKEBASE_DATABASE_NAME are injected automatically when deployed via DABs. For local dev, set them manually only if you want to exercise the corresponding feature locally:
| Want to test... | Set... |
|---|---|
| Profiler / dry-run | DQX_JOB_ID (and the wheel volume must exist) |
| Lakebase OLTP path | DQX_LAKEBASE_ENDPOINT (empty = falls back to Delta — fine for most local dev) |
| Wheel sync | DQX_WHEELS_VOLUME |
Lakebase locally: The same OAuth token-refresh logic that runs in production also runs locally. The app authenticates as your CLI user (via
databricks-sdkdefault auth chain), so your CLI principal must have a Postgres role on the project branch. Easiest path: run the bundle once against your dev workspace so thepostgres_rolesblock provisions the role, then pointDQX_LAKEBASE_ENDPOINTat the project's endpoint path (projects/<project>/branches/<branch>/endpoints/primary).
Local dev never provisions anything — it always points at resources that already exist (created by a bundle deploy or by hand). So the production "bundle-managed vs. bring-your-own" choice collapses locally to "which value do I put in app/.env":
| Resource | Local knob | Notes |
|---|---|---|
| SQL warehouse | DATABRICKS_WAREHOUSE_ID=<existing-id> |
Any warehouse you have CAN_USE on. Required for queries, profiling, and dry-runs. |
| Catalog | DQX_CATALOG=<existing-catalog> (+ DQX_SCHEMA, DQX_TMP_SCHEMA) |
The catalog and schemas must already exist; local dev does not create them. You need USE CATALOG + USE SCHEMA (+ SELECT to profile tables). |
| Lakebase | DQX_LAKEBASE_ENDPOINT=<existing-endpoint-path> |
Point at an existing project endpoint you have a Postgres role on (projects/<project>/branches/<branch>/endpoints/primary). Leave it empty to skip Lakebase and run OLTP tables on Delta — the simplest local setup. |
In production the bundle always provisions its own SQL warehouse and Lakebase project (the catalog is always pre-existing). The fastest way to get a matching warehouse + catalog + Lakebase for local dev is to run make app-deploy once against a dev workspace, then copy the resulting IDs/endpoint path into app/.env.
From the project root:
make app-install # JS dependencies (yarn)
cd app && uv sync # Python dependenciesOr from the app/ directory:
uv sync
yarn install --frozen-lockfileThe React frontend must be compiled before the backend can serve it.
From the project root:
make app-buildOr directly from the app/ directory:
uv run python scripts/build_app.pyThis generates the OpenAPI schema, compiles the React/TypeScript UI into __dist__/, and packages everything into a wheel. The wheel filename and METADATA both carry a build-tag local-version segment (e.g. .b20260530t012345) so successive deploys at the same git commit force a fresh pip install in Databricks Apps' persistent venv.
From the project root:
make app-start-dev # builds first, then starts uvicorn + vite in the foregroundOr directly from the app/ directory:
uv run python scripts/dev.pyThis spawns:
- uvicorn on
http://localhost:9002(FastAPI,--reloadfor backend hot reload) - vite on
http://localhost:9001(UI + HMR, with built-in proxy forwarding/api,/docs,/redoc,/openapi.jsonto uvicorn)
Access the app at:
- UI: http://localhost:9001
- API: http://localhost:9001/api
- OpenAPI docs: http://localhost:9001/docs
Logs stream to the foreground terminal. Ctrl+C sends SIGINT to both children (via process group) for a clean shutdown — typically under one second.
make app-start-dev # all logs stream to stdout (foreground)
make app-stop-dev # stop dev servers started in another shellTo background the dev loop and tail logs separately, redirect to a file:
nohup make app-start-dev > /tmp/dqx-dev.log 2>&1 &
tail -f /tmp/dqx-dev.logAdding a new API endpoint:
- Define the endpoint in
backend/routes/v1/with a Pydantic response model andoperation_id - Add request/response models to
backend/models.pyif needed - Run
make app-regen-apito regenerate the OpenAPI schema andui/lib/api.ts(fast — no full wheel rebuild) - Use the generated React Query hooks in your components
Making UI changes:
- Edit components in
ui/components/or routes inui/routes/ - Vite HMR reloads the browser automatically — no manual refresh needed
- Run
make app-regen-apiafter backend changes to updateui/lib/api.ts
make app-check # TypeScript (`bun run tsc -b`) + Python (`basedpyright --level error`) type-check
make fmt # format Python (run before every commit)make app-check is type-check only — it does not run ESLint or ruff. Run linters separately if you need them: make lint from the project root covers ruff + mypy at the library level.
make test # unit tests
make integration # integration tests (requires live workspace)The profiler creates a temporary view using your OBO token and submits a Databricks Job as the service principal. You need USE CATALOG + USE SCHEMA + SELECT on the tables you want to profile, plus DQX_JOB_ID set to a deployed task-runner job. See README.md for the full authentication model.
If the wheel upload fails locally with a 403, grant your user write access:
databricks volumes grant <catalog>.dqx_studio.wheels WRITE_VOLUME --user <your-email> -p <your-profile>Port already in use:
lsof -i :9001
make app-stop-devMissing static assets (__dist__ does not exist):
make app-buildTypeScript type errors in the UI:
make app-regen-api # regenerates ui/lib/api.ts from the current OpenAPI specBuild artifacts are stale:
rm -rf .build src/databricks_labs_dqx_app/__dist__
make app-builduv hangs:
uv sync -v # verbose output to diagnose
rm -rf .venv/.lock # remove stale lock if neededOBO token missing locally:
The X-Forwarded-Access-Token header is only injected by the Databricks Apps platform; it is absent when running locally. In that case the backend falls back to the SDK default auth for OBO operations — i.e. your configured CLI profile / .env (DATABRICKS_CONFIG_PROFILE or DATABRICKS_TOKEN). No extra setup is needed beyond Configure Authentication: just databricks auth login or an app/.env, and OBO endpoints run as your own identity.
This fallback only activates locally — production deployments authenticate via DATABRICKS_CLIENT_ID/DATABRICKS_CLIENT_SECRET (no profile/PAT present), so the header is always required there. Note OBO calls then run as your identity, not an arbitrary end user, so it can't simulate per-user permission differences.