Studio Read API
Oncecratestack 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:
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.
@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:fieldsegment isn’t a field on:model.400 NOT_A_RELATION— the field exists but isn’t typed as another model.501 UNSUPPORTEDagainst 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:
String/Cuid/Uuid/Decimal→"value".to_owned(), with quotes and backslashes escaped.Int→42_i64.
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.next_cursor is null, you’ve hit the end.
Secrets in studio.toml
Phase 1a resolves two reference forms on boot:
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.