Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
45 commits
Select commit Hold shift + click to select a range
23c72ba
feat(app): prefix-derived Studio storage model
laurencewells Oct 6, 2026
f238e24
feat(app): audience principal mapping and prefix config
laurencewells Oct 6, 2026
c126f48
feat(app): persist and resolve Studio setup configuration
laurencewells Oct 6, 2026
149b345
fix(app): resolve config schema derivation and test isolation in Task 3
laurencewells Oct 6, 2026
e897a00
fix: add missing DQX_GENIE_SCHEMA and DQX_DEMO_SCHEMA to conftest tes…
laurencewells Oct 6, 2026
cda25be
fix: apply schema fallback logic in conftest fixture
laurencewells Oct 6, 2026
0180fac
refactor(app): carry storage and audience on ActiveResources
laurencewells Oct 6, 2026
682b300
fix(app): require demo schema in DemoSeedService
laurencewells Oct 6, 2026
86102fb
refactor(app): shared UC grant inspection and ACL helpers
laurencewells Oct 6, 2026
4a5ea9a
feat(app): bootstrap Lakebase before storage and add configuration/ac…
laurencewells Oct 6, 2026
5c05a41
fix(app): let unbound setup reconcile reach the orchestrator and shar…
laurencewells Oct 6, 2026
07cb24f
test: update root app backend fixture for ActiveResources
laurencewells Oct 6, 2026
4037351
feat(app): provision prefix storage and verify catalog access
laurencewells Oct 6, 2026
8fc2a53
fix(app): verify volume and schema access after provisioning storage
laurencewells Oct 6, 2026
1b6a626
feat(app): require warehouse CAN_MANAGE and reconcile audience CAN_USE
laurencewells Oct 6, 2026
d8c0575
feat(app): verify audience, admin and runner least-privilege access
laurencewells Oct 6, 2026
fe107ab
fix(app): require metadata dimensions before audience access verifica…
laurencewells Oct 6, 2026
f45f4c9
feat(app): verify app sharing with an explicit warning when unreadable
laurencewells Oct 6, 2026
46b85b2
feat(app): start setup without a bound volume
laurencewells Oct 6, 2026
c1b3332
fix(app): never keep a stale setup binding after a failed re-bind
laurencewells Oct 6, 2026
9310df2
feat(app): setup configuration endpoint
laurencewells Oct 6, 2026
6db855d
fix(app): serialize setup configuration save and classify catalog/gro…
laurencewells Oct 6, 2026
9e18401
feat(app): setup configuration form and warning state
laurencewells Oct 6, 2026
a9812a7
feat(app): Marketplace binds warehouse CAN_MANAGE and Lakebase only
laurencewells Oct 6, 2026
a03f4c9
feat(app): prefix-derived bundle storage and least-privilege grants
laurencewells Oct 6, 2026
44288da
fix(app): broad UC principal must not override an explicit studio_use…
laurencewells Oct 6, 2026
a541c51
docs(app): Studio installation and permission model
laurencewells Oct 6, 2026
346d2e2
fix(app): let the app SP read bundle dashboard and manage its bound w…
laurencewells Oct 6, 2026
400f3a6
fix(app): retry app identity and keep saved setup choices editable un…
laurencewells Oct 6, 2026
4e847b1
feat(app): admin setup-warnings banner and prefilled configuration ed…
laurencewells Oct 6, 2026
fe25034
test(app): tighten setup tests, drop dead setup code, reject mixed-ca…
laurencewells Oct 6, 2026
0c44e4e
test(app): live Marketplace prefix-storage provisioning integration test
laurencewells Oct 6, 2026
3295bd8
docs(app): align permission matrix, QA and runbook docs with the fina…
laurencewells Oct 6, 2026
7228934
fix(app): inspect group grants via SHOW GRANTS and report deployment …
laurencewells Oct 7, 2026
a995cbd
docs(app): note first-deploy Lakebase 404 retry
laurencewells Oct 7, 2026
e2c9143
fix(app): declare app SP schema grants explicitly so the bundle keeps…
laurencewells Oct 7, 2026
113a88a
fix(app): reuse admin-verified catalog grants on unattended restarts
laurencewells Oct 7, 2026
615e063
fix(app): limit verification reuse to catalog-level runner grants
laurencewells Oct 7, 2026
582bfc4
fix(app): runner reads tmp views via schema-level SELECT, drop per-vi…
laurencewells Oct 7, 2026
93e7d87
docs(app): note runner tmp SELECT in cold-startup checks; comment pri…
laurencewells Oct 7, 2026
680473c
style(app): apply black formatting
laurencewells Oct 7, 2026
f28125e
fix(app): reconcile swapped SQL warehouse and review setup warnings i…
laurencewells Oct 7, 2026
5e2999c
docs(app): drop runner Lakebase prerequisite after #1564 and fill Stu…
laurencewells Oct 8, 2026
49ed510
fix(app): restore metadata-dim failure log and correct DQX_USER_GROUP…
laurencewells Oct 8, 2026
5d2f02b
Merge branch 'main' into feat/studio-marketplace-permissions
OGordon100 Oct 9, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 28 additions & 5 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -387,15 +387,21 @@ app-check-cli: ## Verify the Databricks CLI meets the minimum version for deploy
#
# ONE-TIME prerequisite per catalog (the bundle does not manage the pre-existing
# catalog, so it cannot grant catalog-level access): grant USE CATALOG on the
# chosen catalog to the app SP, the task-runner SP, and ``account users``.
# See app/DEPLOYMENT.md.
# chosen catalog to the app SP (plus CREATE SCHEMA), the task-runner SP, and the
# configured UC audience principal (the studio_user_group, or ``account users`` in
# broad mode). See app/DEPLOYMENT.md.
#
# Usage: make app-deploy PROFILE=my-profile TARGET=dev
# make app-deploy PROFILE=my-profile TARGET=dev \
# BUNDLE_VARS='--var=catalog_name=foo'
# make app-deploy PROFILE=my-profile TARGET=dev \
# STUDIO_PREFIX=dqx_studio_acme STUDIO_USER_GROUP=data-team
# make app-deploy PROFILE=my-profile TARGET=dev STUDIO_USER_GROUP=users # broad mode
#
# BUNDLE_VARS forwards arbitrary ``--var key=value`` arguments to ``bundle
# deploy`` and ``bundle run``.
# deploy`` and ``bundle run``. STUDIO_PREFIX sets the ``prefix`` variable (storage
# names) and STUDIO_USER_GROUP sets ``studio_user_group``; an explicit --var in
# BUNDLE_VARS always wins.
#
# FORCE=1 appends ``--force`` to ``bundle deploy``. Use it when the deploy
# aborts because a resource was modified in the workspace UI since the last
Expand All @@ -406,11 +412,28 @@ app-check-cli: ## Verify the Databricks CLI meets the minimum version for deploy
app-deploy: app-check-cli $(if $(filter release,$(TARGET)),,app-build) ## Deploy and start app; release target uses prebuilt tag (FORCE=1 to overwrite remote edits)
@test -n "$(PROFILE)" || (echo "Usage: make app-deploy PROFILE=<databricks-profile> TARGET=<bundle-target>"; exit 1)
@test -n "$(TARGET)" || (echo "Usage: make app-deploy PROFILE=<databricks-profile> TARGET=<bundle-target>"; exit 1)
cd app && databricks bundle deploy -p $(PROFILE) -t $(TARGET) $(if $(FORCE),--force) $(BUNDLE_VARS)
cd app && databricks bundle run $(APP_NAME) -p $(PROFILE) -t $(TARGET) $(BUNDLE_VARS)
cd app && databricks bundle deploy -p $(PROFILE) -t $(TARGET) $(if $(FORCE),--force) $(STUDIO_BUNDLE_VARS)
cd app && databricks bundle run $(APP_NAME) -p $(PROFILE) -t $(TARGET) $(STUDIO_BUNDLE_VARS)

APP_NAME ?= dqx-studio

# Studio storage prefix / audience. STUDIO_USER_GROUP=users selects broad mode:
# workspace ACLs use `users`, UC grants use `account users`. An explicit
# studio_user_group in BUNDLE_VARS also suppresses the broad UC principal.
studio_prefix_var = $(if $(STUDIO_PREFIX),$(if $(findstring prefix=,$(BUNDLE_VARS)),,--var prefix=$(STUDIO_PREFIX)))
studio_group_var = $(if $(STUDIO_USER_GROUP),$(if $(findstring studio_user_group,$(BUNDLE_VARS)),,--var studio_user_group=$(STUDIO_USER_GROUP)))
studio_uc_var = $(if $(filter users,$(STUDIO_USER_GROUP)),$(if $(findstring studio_uc_principal,$(BUNDLE_VARS))$(findstring studio_user_group,$(BUNDLE_VARS)),,--var "studio_uc_principal=account users"))
STUDIO_BUNDLE_VARS = $(studio_prefix_var) $(studio_group_var) $(studio_uc_var) $(BUNDLE_VARS)

# The broad-mode keyword is exactly lowercase `users`; any other casing would be
# deployed as a (non-existent) dedicated group, so reject it before deploying.
studio_group_lower = $(subst U,u,$(subst S,s,$(subst E,e,$(subst R,r,$(STUDIO_USER_GROUP)))))
ifneq ($(filter app-deploy,$(MAKECMDGOALS)),)
ifneq ($(and $(filter users,$(studio_group_lower)),$(filter-out users,$(STUDIO_USER_GROUP))),)
$(error Broad mode requires lowercase STUDIO_USER_GROUP=users)
endif
endif

##@ Build & lockfiles

build: ## Build sdist + wheel (with --require-hashes against build-constraints)
Expand Down
49 changes: 31 additions & 18 deletions app/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ but is protected transitively by the instance-level guard.

```
{user_catalog}
├── dqx_studio ← main schema (SP-managed)
├── <prefix> ← main schema (default prefix `dqx_studio`; SP-managed)
│ ├── dq_profiling_results (Delta) profiler run results
│ ├── dq_validation_runs (Delta) dryrun + scheduled run history
│ ├── dq_quarantine_records (Delta) invalid rows captured by runs
Expand All @@ -84,8 +84,10 @@ but is protected transitively by the instance-level guard.
│ ├── dq_schedule_configs_history (OLTP*) schedule config change audit log
│ ├── dq_schedule_runs (OLTP*) scheduler last/next run state (survives restarts)
│ └── dq_migrations (Delta) Delta migration version tracker
├── dqx_studio_tmp ← temp views created via OBO for profiler/dryrun jobs
└── dqx_studio.wheels (volume) ← DQX + task-runner wheels uploaded at app startup
│ └── wheels (volume) ← DQX + task-runner wheels uploaded at app startup
├── <prefix>_tmp ← temp views created via OBO for profiler/dryrun jobs
├── <prefix>_genie ← approved Genie views + metadata dimensions
└── <prefix>_demo ← demo source tables

Lakebase project (when enabled, default `lakebase_project_id` = `dqx-studio-db`):
└── databricks_postgres (database — always-present admin DB; no per-app DB provisioned)
Expand All @@ -96,12 +98,16 @@ Lakebase project (when enabled, default `lakebase_project_id` = `dqx-studio-db`)
└── dq_migrations (Postgres migration version tracker)
```

`(OLTP*)` = lives in **Lakebase Postgres**.
The Lakebase schema (`DQX_LAKEBASE_SCHEMA`) never follows the UC prefix. UC storage names derive from an existing catalog and a validated prefix (`setup/storage.py`): DAB passes `DQX_CATALOG` / `DQX_PREFIX` (plus per-schema overrides) and treats them as authoritative; Marketplace has no volume binding and collects catalog, prefix, and audience through the setup form (`routes/v1/setup.py`, persisted via `setup/configuration.py`, locked once storage exists).

`(OLTP*)` = lives in **Lakebase Postgres** when
`lakebase_endpoint` is set, otherwise **Delta** (the
`v2: Delta OLTP fallback` migration).

## Key Decisions

- **No config.yaml** — all settings stored in Delta or Lakebase tables.
- **Dedicated catalog** — user selects at install; `dqx_studio` and `dqx_studio_tmp` schemas are declared as bundle resources and created by `databricks bundle deploy`.
- **Dedicated catalog** — user selects at install; the prefix-derived schemas (`<prefix>`, `<prefix>_tmp`, `<prefix>_genie`, `<prefix>_demo`) are declared as bundle resources and created by `databricks bundle deploy`; Marketplace setup creates them instead.
- **Hybrid storage** — high-volume append tables in Delta; transactional/low-latency tables in Lakebase Postgres.
- **Rule promotion** — export rules then deploy separately to prod; or save directly to prod checks table.
- **Target environments** — Dev, UAT/QA (prod-like data); app is not intended for production rule execution.
Expand All @@ -110,16 +116,18 @@ Lakebase project (when enabled, default `lakebase_project_id` = `dqx-studio-db`)

Stateful resources declared in `databricks.yml`:

- `resources.schemas.main_schema` — `dqx_studio` schema
- `resources.schemas.tmp_schema` — `dqx_studio_tmp` schema
- `resources.volumes.wheels` — wheels volume
- `resources.schemas.main_schema` — `${var.prefix}` schema
- `resources.schemas.tmp_schema` — `${var.prefix}_tmp` schema
- `resources.schemas.genie_schema` — `${var.prefix}_genie` schema
- `resources.schemas.demo_schema` — `${var.prefix}_demo` schema
- `resources.volumes.wheels` — `wheels` volume inside the main schema
- `resources.postgres_projects.dqx_studio` — Lakebase Postgres project (autoscaling, scale-to-zero)

Each carries `lifecycle.prevent_destroy: true` (Databricks CLI 0.268+), which blocks `databricks bundle destroy` and any deploy that would force-replace the resource. To intentionally tear something down: drop the flag, `databricks bundle deployment unbind <key> -t <target>`, then destroy.

The app connects to the always-present `databricks_postgres` admin database on the Lakebase project (set as the default `lakebase_database_name`) via the `DQX_LAKEBASE_ENDPOINT` endpoint path and creates its own `dqx_studio` Postgres schema there on first start. The app SP's Postgres role (`resources.postgres_roles.app_sp`, a `DATABRICKS_SUPERUSER` member) grants the CREATE-schema privilege. We deliberately do not use `database_catalogs` because it also creates a Unity Catalog catalog and therefore requires `CREATE CATALOG` on the metastore — a permission most app deployers don't hold.

UC privileges for the app SP and task-runner SP are declared **natively** as `grants:` on the schema/volume resources (using `${resources.apps.dqx-studio.service_principal_client_id}` and `${var.dqx_service_principal_application_id}`), so `databricks bundle deploy` applies them — there is no post-deploy grant script. The one exception is `USE CATALOG` on the pre-existing (user-selected) catalog, which the bundle can't grant because it doesn't manage the catalog; grant it once per catalog as a documented prerequisite (see `DEPLOYMENT.md`).
UC privileges for the app SP and task-runner SP are declared **natively** as `grants:` on the schema/volume resources (using `${resources.apps.dqx-studio.service_principal_client_id}` and `${var.dqx_service_principal_application_id}`), so `databricks bundle deploy` applies them — there is no post-deploy grant script. DAB schemas are owned by the deployer, so the app SP gets an explicit privilege list **including `MANAGE`** on each Studio schema (`USE_SCHEMA`, `CREATE_TABLE`, `CREATE_FUNCTION`, `CREATE_VOLUME`, `SELECT`, `MODIFY`, `EXECUTE`, `READ_VOLUME`, `WRITE_VOLUME`, `APPLY_TAG`, `MANAGE`; never `ALL_PRIVILEGES`, because the bundle engine drops `MANAGE` when combined with it). The runner SP is least privilege: `USE_SCHEMA`/`SELECT`/`MODIFY` on the main schema, `USE_SCHEMA`/`SELECT` on `_tmp` (the runner reads OBO temp views through schema-level `SELECT`, so it can read any view in `_tmp`; there are no per-view runner grants, only the app SP's per-view `MANAGE` for cleanup), `READ_VOLUME` on wheels, nothing on `_genie`/`_demo`. The audience is granted via `studio_uc_principal` (the `studio_user_group`, or `account users` in broad mode); admin-group UC grants are deliberately not declared because `admin_group` may be the built-in `admins` (never a valid UC grantee), so the app applies and verifies them at runtime after each restart. The one manual step is catalog access on the pre-existing (user-selected) catalog, which the bundle can't grant because it doesn't manage the catalog: `USE CATALOG` + `CREATE SCHEMA` for the app SP, `USE CATALOG` for the runner and audience (see `DEPLOYMENT.md`). The warehouse ACL gives the app SP `CAN_MANAGE`; audience and admin group get `CAN_USE`.

## Architecture

Expand Down Expand Up @@ -496,7 +504,7 @@ See `DEVELOPMENT.md` for local `.env` and Lakebase notes.
## Important Notes

- **SQL safety:** all interpolated identifiers must pass `validate_fqn` and be wrapped with `quote_fqn` from `sql_utils.py`. All string literals must be escaped with `escape_sql_string` (ANSI doubled quotes — never backslash). User-supplied SQL bodies must pass `is_sql_query_safe()` from the DQX library and raise `UnsafeSqlQueryError` on rejection.
- **Setup and activation:** Lakebase and Delta migrations, app-SP sibling-schema capabilities, score views, and the entitlement view are required before the app reports ready. End-user grants are best effort and never gate readiness; catalog access can be scoped to the intended user groups. Genie SELECT grants are limited to five approved views and two metadata tables (`dim_dq_rules` and `dim_dq_monitored_tables`), never the whole schema or entitlement table. Each successful metadata refresh retries the metadata-table grants. Metadata refresh and Genie space provisioning remain best effort.
- **Setup and activation:** Lakebase and Delta migrations, app-SP catalog/schema capabilities, score views, and the entitlement view are required before the app reports ready. Audience, administrator, and runner access is **applied and verified by the setup access step** (`setup/access.py`, `setup/checks.py`): setup attempts each grant or additive ACL update, then re-reads the state, and missing or uninspectable grants block readiness (fail closed). The one exception is app sharing (`app_sharing` step): if neither the app SP nor the setup administrator can read the app ACL it reports a warning, not a block. Configured audiences come from `setup/audience.py`: a dedicated group, or `users` in DAB broad mode only (workspace ACL `users`, UC `account users`); the workspace `admins` group never receives UC grants, only a custom `DQX_ADMIN_GROUP` does. Setup mutations are allowed for members of `DQX_ADMIN_GROUP` or `admins`. Genie SELECT grants are limited to five approved views and two metadata tables (`dim_dq_rules` and `dim_dq_monitored_tables`), never the whole schema or entitlement table. Each successful metadata refresh retries the metadata-table grants. The metadata-dimension refresh is required for activation (the access step needs the metadata tables); only Genie space provisioning remains best effort. ACL changes are additive (`update_permissions`), never `set_permissions`.
- **Scheduler:** runs in-process as an asyncio task, gated by an exclusive file lock (`/tmp/.dqx_scheduler.lock`) so only one uvicorn worker drives it. Disable with `DQX_SCHEDULER_DISABLED=1`.
- **Caches:** `app_cache` (`cache.py`) is per-process in-memory with TTL. SP `WorkspaceClient`, OBO `WorkspaceClient`, and per-user catalog list are all cached. Use the `MISS` sentinel — never `is None` — to detect cache absence.
- **SPA static files:** `spa_static.py` falls through to `index.html` only for non-asset paths (positive allowlist of asset extensions), so SPA routes containing dots still work.
Expand Down Expand Up @@ -576,9 +584,11 @@ to their native syntax.
Stateful resources declared in `databricks.yml` with
`lifecycle.prevent_destroy: true` (Databricks CLI 0.268+):

* `resources.schemas.main_schema` — `dqx_studio` schema
* `resources.schemas.tmp_schema` — `dqx_studio_tmp` schema
* `resources.volumes.wheels` — wheels volume
* `resources.schemas.main_schema` — `${var.prefix}` schema
* `resources.schemas.tmp_schema` — `${var.prefix}_tmp` schema
* `resources.schemas.genie_schema` — `${var.prefix}_genie` schema
* `resources.schemas.demo_schema` — `${var.prefix}_demo` schema
* `resources.volumes.wheels` — `wheels` volume inside the main schema
* `resources.postgres_projects.dqx_studio` — Lakebase Postgres project
(autoscaling + scale-to-zero per [Lakebase Autoscaling](https://docs.databricks.com/aws/en/oltp/upgrade-to-autoscaling)),
paired with `resources.postgres_roles.app_sp` (the app SP's Postgres role)
Expand Down Expand Up @@ -615,8 +625,11 @@ UC privileges for the app SP and task-runner SP are declared
**natively** as `grants:` on the schema/volume resources (via
`${resources.apps.dqx-studio.service_principal_client_id}` and
`${var.dqx_service_principal_application_id}`), so `bundle deploy`
applies them — there is no post-deploy grant script. The one manual
step is `USE CATALOG` on the pre-existing (user-selected) catalog,
which the bundle can't grant because it doesn't manage the catalog;
grant it once per catalog as a documented prerequisite (see
`DEPLOYMENT.md`).
applies them — there is no post-deploy grant script. The app SP gets
an explicit privilege list including `MANAGE` (no `ALL_PRIVILEGES`) on the Studio schemas; the runner SP is
least privilege (see "Bundle conventions" above). The one manual step
is catalog access on the pre-existing (user-selected) catalog, which
the bundle can't grant because it doesn't manage the catalog; grant it
once per catalog as a documented prerequisite (see `DEPLOYMENT.md`).
The wheels `uc_securable` app binding is DAB-only; the Marketplace
manifest binds just the warehouse (`CAN_MANAGE`) and Lakebase.
14 changes: 8 additions & 6 deletions app/CUSTOMER_QA.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,8 @@ One Databricks Asset Bundle (`databricks.yml`) provisions everything in one `mak
- **Databricks App** (FastAPI + React, single process, served by the Apps runtime)
- **Serverless Job** for Spark work — the *task runner* — invoked for profiler, dry-run, and scheduled runs
- **Lakebase Postgres project** (`postgres_projects.dqx_studio` + `postgres_roles.app_sp`) for OLTP state
- **Two UC schemas** (`dqx_studio`, `dqx_studio_tmp`) under a customer-supplied catalog
- **UC volume** (`wheels`) the app uses to ship DQX wheels into the job
- **Four UC schemas** derived from a storage prefix (default `dqx_studio`): `<prefix>`, `<prefix>_tmp`, `<prefix>_genie`, `<prefix>_demo`, under a customer-supplied catalog
- **UC volume** (`<prefix>.wheels`) the app uses to ship DQX wheels into the job
- **SQL warehouse** — managed by the bundle, or BYO (bring-your-own) if you already have one
- **Lakeview dashboard** (`dashboards.dqx_quality_overview`) pinned to the app's *Insights* page

Expand Down Expand Up @@ -154,15 +154,17 @@ Roles are resolved from **Databricks workspace-group membership** via the `dq_ro
### 4.3 What permissions does the app's service principal need?
Scoped tight:

- `USE CATALOG` + `USE SCHEMA` + `ALL PRIVILEGES` on the two DQX schemas (`dqx_studio`, `dqx_studio_tmp`) only
- `READ VOLUME` + `WRITE VOLUME` on the wheels volume only
- `CAN USE` on the SQL warehouse the app is bound to
- `USE CATALOG` + `CREATE SCHEMA` on the selected catalog (the one manual prerequisite)
- Ownership (Marketplace) or an explicit privilege list incl. `MANAGE` (DAB) on the four prefix-derived Studio schemas and the wheels volume only
- `CAN MANAGE` on the SQL warehouse the app is bound to, so setup can share it with the audience additively
- `Service Principal: User` role on the task-runner SP (so the app can submit jobs as it)

The task-runner SP is least privilege: `USE CATALOG`; `USE SCHEMA`, `SELECT`, `MODIFY` on the main schema; `USE SCHEMA` and `SELECT` on `<prefix>_tmp` (the runner reads OBO temporary views through this schema-level `SELECT`, so it can read any view in `_tmp`; no per-view grants); `READ VOLUME` on the wheels volume; nothing on `<prefix>_genie` or `<prefix>_demo`.

It is **not** a workspace admin and **not** a metastore admin. If you remove the app, those grants are the only blast radius.

### 4.4 What's the deployer's permission burden?
Documented as a table in `DEPLOYMENT.md` — about 10 line items, the bulk of which collapse if the deployer is added to a UC-admin group. The single most common failure on first deploy is missing `MANAGE`/`USE CATALOG` on the target catalog. `bundle deploy` applies schema/volume grants natively, but the one manual prerequisite is granting `USE CATALOG` on the (pre-existing, bundle-unmanaged) catalog to the app SP, task-runner SP, and `account users`. We surface that error explicitly with a fix.
Documented as a table in `DEPLOYMENT.md` — about 10 line items, the bulk of which collapse if the deployer is added to a UC-admin group. The single most common failure on first deploy is missing `MANAGE`/`USE CATALOG` on the target catalog. `bundle deploy` applies schema/volume grants natively, but the one manual prerequisite is granting catalog access on the (pre-existing, bundle-unmanaged) catalog: `USE CATALOG` + `CREATE SCHEMA` to the app SP, and `USE CATALOG` to the task-runner SP and the audience group (`account users` only in broad mode). We surface that error explicitly with a fix.

### 4.5 Is everything audited?
Yes, but the auditing is **distributed** across the platform rather than in one DQX log:
Expand Down
Loading
Loading