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
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, UNSAFE_DB_WRITE, DATABASE_ERROR,
UPSTREAM_ERROR, INTERNAL_ERROR. FORBIDDEN, VALIDATION_ERROR,
and UNSAFE_DB_WRITE are only ever returned for writes; see the
write API page for the mutation surface.
Studio supports Postgres and SQLite (via
rusqlite) natively, plus
API-backed targets that proxy through a deployed CrateStack service.
Relation follow works against the two DB-backed drivers; API-backed
targets return UNSUPPORTED for follow, and many-to-many through a
junction table returns UNSUPPORTED on every driver — see
What’s not here yet.[target.db] is a direct database connection
Studio is an admin tool. A [target.db] target opens a direct SQL
connection with database-level access by design — it is not a proxy
in front of your deployed service’s generated API, and it does not
replay that service’s request-time behavior. Two things follow from
that, and both are intended, not bugs:
@@allowpolicies are never evaluated, on reads or writes, against a[target.db]target. A@sensitiveor role-gated field is readable through Studio’s HTTP API by anyone who can reach it — unauthenticated, regardless of what the schema’s policies declare.@@internal(...)route suppression is not enforced either. A model whosecreateis suppressed with@@internal("create")— or denied outright by an@@allowrule — is still creatable throughPOST /api/targets/{key}/models/{model}/recordson anrw[target.db]target. Suppression is a generation-time routing decision about the generated API; a direct SQL connection sits beneath it, the waypsqldoes.- Writes additionally bypass
@versionbumping and@@emit(...)event-outbox rows; see the unsafe-write guard on the write API page.
[target.db] write are TargetMode::Rw,
the @version/@@emit write-routability guard, and its
allow_unsafe_writes opt-in. Making Studio enforce @@internal while
@@allow stayed unenforced was considered and rejected as the arbitrary
half of the change.
An [target.api]-only target is the opposite and gets both for free:
it issues ordinary HTTP requests against the deployed service’s generated
routes — the same surface the TypeScript and Dart clients consume — so a
suppressed verb has no route to call (405, or 404 when every verb on
the path is suppressed) and policies are evaluated server-side against the
identity in [target.api].auth.
Prefer an [target.api]-only target for a schema whose policies you need
enforced, and scope which network can reach a [target.db]-backed Studio
instance accordingly — Studio binds 127.0.0.1:7878 by default, which
keeps it off the network unless you deliberately rebind it or put it
behind a proxy.
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. 400 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.
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
Studio 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
UNSUPPORTEDon every driver. Not yet implemented. - 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.