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.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.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
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 astudio.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.
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 likePost.author). - A page of related rows (for
List-arity fields likeCustomer.posts).
<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/DecimalIDs render as"value".to_owned(),IntIDs as42_i64.
CORS in dev
The UI runs on127.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:
[[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 withmode = "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 viawindow.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: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.
@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.
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/deleteand 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.
- An op selector + Show SQL button that fetches the rendered
SQL Studio would run for
- Drift dots on each model in the sidebar.
⚠ driftin amber when columns don’t match;✕ tablein red when the table is missing entirely. Driven byGET /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.
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.
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.
Schema search
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.
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+CHECKon 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.
cratestack studio eject if you need to customize the visual
style or wire in proprietary actions ahead of the upstream’s schedule.