# Update Viz

```
PATCH 
/v1/viz/:viz_id
```

Update viz metadata.

ROLE GATE + ACL, not ACL alone. Until 2026-08-25 this route was guarded ONLY by the folder ACL, so `role` and the folder ACL were two authorization systems that disagreed about the product's most valuable object. Measured on prod: a user the product calls read-only, holding one folder `edit` grant, rewrote another user's dashboard description and slug and flipped `viewMode` to `public` — while the adjacent `PUT /viz/{id}/branches/main/files/...` refused the same user with "Your role is read-only." Two adjacent operations on the same resource cannot disagree about whether the caller may write.

`require_creator_or_admin` is the same dependency that already guards folders, dbt, connections and viz FILES; the folder ACL still applies underneath (it is what decides WHICH viz), so this narrows and never widens.

Slug handling mirrors the create path: a structurally-invalid slug (forbidden word / bad charset) is a hard 409 up front, and a *collision* with an existing viz on the same account surfaces as a clean 409 via the `VizSlugConflictError` raised by the SQL layer's IntegrityError guard. Previously a combined `{name, slug}` PATCH onto a taken slug let the bare `IntegrityError` propagate → opaque 500 (ticket viz-create-name-slug-combined-patch-500). Unlike create we do NOT auto-suffix on update — the caller asked for a specific slug, so a collision is a 409, never a silent rename.

`datasetIds` RE-POINTS THE DASHBOARD, and it is validated here for the first time. The binding is what both query lanes turn into the set of SQL aliases a tile may read, so changing it changes what the dashboard can see — the id, the address, the grants, the alerts and the subscriptions all stay where they are. Create has always refused a binding whose datasets it cannot read or that are still being prepared; this route accepted any integer and answered 200, so a typo pointed a live dashboard at nothing and said it had worked. It now runs the same check, and refuses an EMPTY list outright: unbinding a dashboard that already renders is never what a re-point meant, and a dashboard meant to carry hand-written SQL against no dataset is something to create, not to arrive at by emptying one that works.

Re-sending the binding the dashboard already has is NOT a re-point and is not checked. The app PATCHes whole objects, so the common body restates the ids; validating an echo would make a rename fail because a dataset nobody touched is mid-refresh.

Re-pointing is complete on its own only when the new dataset carries the SAME SQL alias as the old. The generated source names its datasets by alias, never by id, so a new dataset with a different alias needs the source edited and published as well — the binding stops refusing the new alias, it does not rewrite the queries that still name the old one.

## Request[​](#request "Direkter Link zur request")

## Responses[​](#responses "Direkter Link zur Responses")

* 200
* 404
* 422

Successful Response

Not found

Validation Error
