Studio Read API

Once cratestack studio run is up, Studio serves a stable JSON API over the bind address (default 127.0.0.1:7878). The Leptos UI in Phase 1b is one consumer; you can hit the same endpoints from a script, a notebook, or your own tool. All responses are JSON. All error responses use the same envelope:
Stable codes: UNKNOWN_TARGET, UNKNOWN_MODEL, UNKNOWN_FIELD, NOT_A_RELATION, NO_PRIMARY_KEY, INVALID_PRIMARY_KEY, UNSUPPORTED, FORBIDDEN, VALIDATION_ERROR, DATABASE_ERROR, UPSTREAM_ERROR, INTERNAL_ERROR. Writes use the Phase 3 codes (FORBIDDEN, VALIDATION_ERROR); see the write API page for the mutation surface.
Phase 1b adds SQLite drivers (via rusqlite), relation follow, and API-backed list/get. Many-to-many through a junction table still returns UNSUPPORTED.

Endpoints

GET /api/targets

GET /api/targets/:key/schema

Returns an OwnedSchemaSummary — parsed mixins, models, types, enums, and procedures by name.

GET /api/targets/:key/models

Same models, but with per-field detail and primary-key resolution.
is_relation is true when the field’s declared type names another model in the same schema (the relation-follow endpoint lands in Phase 1b).

GET /api/targets/:key/models/:model/records

Cursor-paginated list of rows. Query params:
next_cursor is null when the page didn’t fill — i.e. you’ve hit the end. Rows are ordered by primary key ascending; the cursor is the last seen @id value, serialized as a text-shaped reference that Studio binds and casts in SQL.

GET /api/targets/:key/models/:model/records/:pk

Single row by primary-key value. 404 with INVALID_PRIMARY_KEY if no row matches.

GET /api/targets/:key/models/:model/records/:pk/rel/:field

Follow a @relation field from a specific row. The response shape depends on the field’s arity:
  • List arity (posts Post[] @relation(...)) — returns a paginated page (same shape as /records), with cursor-based pagination on the target model’s primary key.
  • Required / Optional arity (author User @relation(...)) — returns a single optional row.
The resolver reads @relation(fields: [SRC], references: [TGT]) on the source field. CrateStack’s parser requires @relation on both sides of a relation, which lets Studio treat both directions uniformly: the target table is filtered on references[0], and the bound value comes from the source row’s fields[0]. Errors specific to this endpoint:
  • 404 UNKNOWN_FIELD — the :field segment isn’t a field on :model.
  • 400 NOT_A_RELATION — the field exists but isn’t typed as another model.
  • 501 UNSUPPORTED against an [target.api] target — the generated REST surface doesn’t expose arbitrary column filters, so relation traversal needs a [target.db] block.

GET /api/targets/:key/models/:model/snippet?pk=…

The “copy Rust query” generator. Returns a ready-to-paste find_unique call against the macro delegate:
Primary-key literals are typed:
  • String / Cuid / Uuid / Decimal"value".to_owned(), with quotes and backslashes escaped.
  • Int42_i64.
Other primary-key scalars (DateTime, Bytes, etc.) return 501 UNSUPPORTED in Phase 1a.

Errors

Cursor pagination in practice

The cursor is opaque, but the model is straightforward: it’s the PK value of the last row in the page, encoded as a string.
When next_cursor is null, you’ve hit the end.

Secrets in studio.toml

Phase 1a resolves two reference forms on boot:
Unset env vars and missing files raise a MissingEnv / SecretFile error at load time, and the error message names the bad config field so you don’t have to grep your studio.toml for which env: line broke.

What’s not here yet

  • Many-to-many through a junction table returns UNSUPPORTED. Phase 2 widens relation resolution.
  • Relation follow against API targets returns UNSUPPORTED — the generated REST surface doesn’t expose arbitrary column filters, so the target needs a [target.db] block for traversal.
  • Mutations (create / update / delete with policy-validation pass-through) land in Phase 3.

Driver coverage