Studio UI

CrateStack Studio ships a browser-based UI built with Leptos and served by Trunk. It’s a thin CSR app that consumes the read API — every action you can do in the browser maps one-to-one to a JSON endpoint.
The UI ships as a sibling crate at crates/cratestack-studio-ui/ in the framework repo. It is deliberately excluded from the Cargo workspace (it targets wasm32-unknown-unknown), so it has its own Cargo.toml and Cargo.lock. The two development paths below run it through a Trunk dev server alongside cratestack studio run.cratestack studio eject gives you a standalone Studio binary project with the UI already bundled in — no Trunk needed. Pass --with-ui to also unpack the Leptos+Trunk sources into a writable directory you can fork and iterate on with trunk serve.

Running the UI locally

You’ll need two terminals — one for the Studio backend, one for the Trunk dev server.
Open http://127.0.0.1:8080. Trunk’s [[proxy]] block in Trunk.toml forwards /api/* to the backend, so the browser sees a single origin.

Prerequisites

Trunk itself fetches wasm-bindgen and any other tooling on first build. Tailwind is pulled from a CDN in index.html, so there’s no Node toolchain in the picture.

Try it with demo data

You don’t need a database — or even a studio.toml — to see the UI working. The framework repo ships a dev harness that boots the Studio API against an in-memory SQLite workspace with three targets and some seeded rows:
--no-default-features skips the embed-ui bundle, which you don’t want in dev — Trunk is already serving the UI, and rebuilding the embedded copy on every change just slows the loop down. Every screenshot on this page was taken against that harness, so you can reproduce all of them locally in about a minute. The demo workspace exposes a catalog target in rw mode (Customer, Post, Product), a read-only analytics target, and an API-backed upstream-api target for comparing the three source types.

What the UI shows you

The UI is one page, four panes:

The catalog target's Post model. Sidebar lists the schema's models; the tools row and RW affordances sit above the records table.

Selecting a row opens the drawer on the right with that row’s fields, the relation picker, and the write affordances (on rw targets):

Row p1 selected. The drawer shows every field, a relation picker, and Edit / Delete / Copy Rust query.

Pagination

The Previous / Next buttons stack cursors locally so navigation is stateless on the server side. Next is disabled when the API’s next_cursor is null (you’ve hit the end). Previous is disabled on the first page.

Following relations

Type a relation field name (author, posts, etc.) into the input in the drawer and click Follow. The result panel below shows:
  • A single related row (for Required-arity fields like Post.author).
  • A page of related rows (for List-arity fields like Customer.posts).
If the field doesn’t exist or isn’t a relation, the panel surfaces the API’s error message. The relation picker is a typed dropdown built from the model’s relation fields. Labels show <field> → <target> (<arity>) so you can pick the right traversal without leaving the drawer.

Copy Rust query

The drawer’s Copy Rust query button calls /api/targets/:key/models/:m/snippet?pk=… and writes the returned find_unique snippet to the system clipboard using the browser’s Clipboard API. Same shape as the rest of the snippet endpoint:
  • String/Cuid/Uuid/Decimal IDs render as "value".to_owned(),
  • Int IDs as 42_i64.
The snippet appears in a code block below the button so you can also hand-copy.

CORS in dev

The UI runs on 127.0.0.1:8080; the backend on 127.0.0.1:7878. To let the browser cross those origins, Studio enables a permissive CORS layer by default. Disable it in studio.toml when binding to a wider interface:
The Trunk dev server’s [[proxy]] block forwards /api/* to the backend, which also avoids the cross-origin hop when you keep the proxy in place.

Writing data (RW targets)

On targets with mode = "rw" the UI exposes three additional flows.

+ New button

Above the records table on RW targets: a + New button opens an inline form with one input per writable field. Submitting calls POST /api/targets/:key/models/:m/records. Per-field validation errors surface inline (see the validators reference for the full list).

Edit in the drawer

Selecting a row and clicking Edit turns the drawer’s field list into editable inputs. Save PATCHes the row; the API’s response replaces the drawer’s view. Validation errors from the server appear inline next to each field that failed.

Delete in the drawer

A Delete button next to Edit confirms via window.confirm(), fires DELETE …/records/:pk, and clears the drawer on success.

Mode badge

Each model header shows a small badge reflecting the target’s mode: RO in slate, RW in green. Studio also hides the write buttons on RO targets — the badge is there to communicate intent before users click anything.

Typed editors

The create form and the drawer’s edit mode dispatch on each field’s declared scalar instead of painting a single text box everywhere: The model-list endpoint (GET /api/targets/:key/models) carries is_enum and enum_variants per field so the UI can render the dropdown without a second fetch.

Edit mode on row p1. Each field gets the control its declared scalar calls for — views is a number input, not a text box.

The @id field and relation/list fields are not editable and are omitted from the form — the primary key identifies the row you’re PATCHing, so it isn’t part of the patch.
Leaving an optional field’s input blank sends null for that field, which is how you clear a column from the UI. A field that was already NULL stays NULL when you save without touching it.

Power tools

Three additions to the records pane and two to the header — together they round out the read-API surface for poking around the system:
  • Tools row above the records table:
    • An op selector + Show SQL button that fetches the rendered SQL Studio would run for list / get / create / update / delete and displays it with bound parameters.
    • An Explain checkbox that additionally asks the driver to plan that SQL (see below).
    • Export JSON / Export CSV links pointing at the export endpoint so the browser downloads the file.
  • Drift dots on each model in the sidebar. ⚠ drift in amber when columns don’t match; ✕ table in red when the table is missing entirely. Driven by GET /api/targets/:key/drift.
  • Schema search in the header. Type a term and matching models / fields / enums show up in a dropdown right below the input.
  • Audit button in the header opens a 28rem overlay listing the most recent CREATE / UPDATE / DELETE operations.
See the tools reference for the underlying endpoints.

SQL preview and query plans

Show SQL renders the statement without touching the database. Tick Explain first and Studio also returns the driver’s query plan underneath it:

list on Post with Explain ticked: the rendered SQL, its bound parameters, and SQLite's EXPLAIN QUERY PLAN output.

Explain is off by default on purpose: rendering SQL is pure and instant, while planning it is a round trip to the database, so you opt into that cost per click. Planning is limited to the read operations (list, get) — Studio has no EXPLAIN ANALYZE path at all, so a mutation is never planned or executed here. Asking to explain a create / update / delete returns the preview with a note instead of a plan.

Searching author matches both the FK scalar and the relation field, each labelled with its model and type.

Audit overlay

The audit overlay after two edits to catalog/Post, newest first.

The overlay lists the most recent 100 entries. By default the log lives in process memory and is lost on restart; set audit_file to persist it across restarts.

What’s not in the UI yet

  • Inline diff between schema and live DB. Drift reports missing/extra columns; a column-by-column diff (comparing declared types and nullability, not just column presence) is still open. The honest blocker is that “changed” is dialect-dependent — enums are stored as TEXT + CHECK on Postgres, so a naive type comparison would report permanent false drift.
  • Procedure execution. Procedures show up in schema search, but Studio is a data browser and does not call them. The callable path is the generated RPC server/client.
The UI itself is plain Leptos CSR — fork it via cratestack studio eject if you need to customize the visual style or wire in proprietary actions ahead of the upstream’s schedule.