Studio Write API
Phase 3 adds three endpoints on top of the read API. They share its URL shape, error envelope, and authentication conventions — the only new ingredients are HTTP methods, request bodies, and a structured validation envelope.| Method | Path | Returns |
|---|---|---|
| POST | /api/targets/:key/models/:model/records | 201 + { "row": {…} } |
| PATCH | /api/targets/:key/models/:model/records/:pk | 200 + { "row": {…} } |
| DELETE | /api/targets/:key/models/:model/records/:pk | 200 + { "row": {…} } |
The mode gate
Studio’sstudio.toml declares each target’s mode:
ro target returns 403 FORBIDDEN with
the new FORBIDDEN error code. The gate runs before the data source
is touched — there’s no way for a request to leak past it into the
database.
The default mode is ro, so writes are opt-in per target.
Validators
Studio mirrors the framework’s macro-side validators server-side. Before any SQL runs, the request payload is checked against every validator attribute on the model’s fields:| Attribute | Code | Checked against |
|---|---|---|
@email | EMAIL | String fields |
@length(min:, max:) | LENGTH | String / Bytes fields |
@range(min:, max:) | RANGE | Int / Decimal fields |
@regex("…") | REGEX | String fields |
@uri | URI | String fields |
@iso4217 | ISO4217 | String fields |
REQUIRED— a required, non-defaulted, non-@idfield is missing ornullin a CREATE payload (ornullon a non-Optional field in an UPDATE).TYPE_MISMATCH— the JSON value’s type doesn’t line up with the field’s declared scalar (e.g. anIntfield got a string).
UNIQUE— Postgres SQLSTATE23505or SQLiteSQLITE_CONSTRAINT_UNIQUE/…_PRIMARYKEYreturned for the write.FOREIGN_KEY— Postgres23503or SQLite…_FOREIGNKEY.
422 VALIDATION_ERROR envelope as
the other codes, so the UI can drop the message next to the input
that broke. Postgres 22001 (string truncation) maps to LENGTH,
22P02 (invalid text representation) to TYPE_MISMATCH, and 23514
(check violation) / SQLite …_CHECK to REGEX. Unrecognized driver
errors still surface as DATABASE_ERROR (500).
Failures roll up into a single 422 VALIDATION_ERROR response with
the error.fields array populated:
error.fields is omitted entirely on non-validation errors, so
the existing error contract is unchanged.
CREATE — POST …/records
@id fields are required unless the schema declares
@default(dbgenerated()). Generated defaults like createdAt
populated by the database are visible in the response row.
UPDATE — PATCH …/records/:pk
null on a
non-Optional field returns a REQUIRED validation error.
If no row matches :pk, the response is 400 INVALID_PRIMARY_KEY.
DELETE — DELETE …/records/:pk
{ "row": {} } if the upstream service uses 204 No Content semantics). A
second DELETE against the same PK returns 400 INVALID_PRIMARY_KEY.
Error codes (Phase 3 additions)
| HTTP | Code | When |
|---|---|---|
| 403 | FORBIDDEN | Target is read-only (mode = "ro") and the request was a write. |
| 422 | VALIDATION_ERROR | One or more field-level validators (or REQUIRED / TYPE_MISMATCH) rejected the payload. Per-field detail in error.fields. |
UNKNOWN_TARGET, UNKNOWN_MODEL,
UNKNOWN_FIELD, NOT_A_RELATION, NO_PRIMARY_KEY,
INVALID_PRIMARY_KEY, UNSUPPORTED, DATABASE_ERROR, UPSTREAM_ERROR,
INTERNAL_ERROR.
Driver behavior
| Driver | CREATE | UPDATE | DELETE | Notes |
|---|---|---|---|---|
| Postgres | ✅ | ✅ | ✅ | INSERT/UPDATE/DELETE … RETURNING * wrapped in row_to_json. Binds are typed per the field’s declared scalar. |
| SQLite | ✅ | ✅ | ✅ | RETURNING json_object(…). Values land via rusqlite’s typed Value enum. |
| Deployed API | ✅ | ✅ | ✅ | Forwards POST / PATCH / DELETE to the upstream’s generated /api/<plural-snake-model> routes. Upstream auth and policy still apply. |
What the UI does with this
The Studio UI wires these endpoints into three flows on RW targets:+ Newabove the records table opens an inline form with one input per writable field. Per-field validation errors surface inline.- Edit in the record drawer turns the field list into editable inputs. Save calls PATCH; per-field errors render in place.
- Delete in the drawer confirms via
window.confirm()and fires DELETE on accept.
RO / RW) next to the model header lets users see
at a glance whether edits are allowed before they click anything.