Generated REST API
The shape of the REST + WebSocket API ForgeDB generates from your schema — CRUD routes, query parameters, operational routes, and realtime streams.
forgedb generate api emits an axum router tailored to your schema. Every model gets a
full CRUD route set; the whole database gets operational and realtime routes. Each handler
is generated for one schema — nothing is interpreted at runtime.
CRUD routes per model#
For a model Post, the route base is the kebab-cased model name, under /api:
| Method + Route | Purpose | Success | Errors |
|---|---|---|---|
GET /api/post | List with filter/sort/paginate. | 200 {data, total, limit, offset} | — |
POST /api/post | Create a record. | 201 {id} | 409 unique/FK, 422 field constraint |
GET /api/post/{id} | Fetch one by id. | 200 <record> | 404 |
PUT /api/post/{id} | Replace the whole record. | 200 | 404, 409, 422 |
DELETE /api/post/{id} | Delete by id. | 204 | 404, 409 (referenced by children) |
The {id} path segment is a string in the URL; integer-PK models parse it back to their
key type.
List response shape#
{ "data": [ /* records */ ], "total": 137, "limit": 20, "offset": 0 }total counts matches before pagination.
Records serialize their fields in schema declaration order — on the list page, on the point read, and in the WebSocket event payloads alike. JSON object key order is not significant and no client should depend on it, but it is stable rather than incidental.
Query parameters (list + read)#
The list handler filters, sorts, and paginates. Filtering and sorting are generated
per-model over a closed set of fields; the query string itself is parsed by the
schema-agnostic forgedb-query-params substrate.
| Param | Applies to | Meaning |
|---|---|---|
?<field>=<value> | list | Exact-match filter on a filterable field (typed compare — ?n=3 matches a stored 3.0). |
?sort=<field> / sort direction | list | Sort by a sortable field. |
?limit= / ?offset= | list | Pagination (limit is clamped to a maximum). |
?projection=<name> | get + list | Return only a declared @projection's columns. Unknown name → 400; absent → full record. |
?as_of=<watermark> | get + list | Point-in-time read at a row-count watermark (snapshot). Non-numeric → 400. |
Filters are a closed generated set
Only fields the generator marks filterable/sortable are accepted — there is no ad-hoc
?fields= or runtime column list. json columns are not filterable/sortable (no total
order). ?projection= accepts only declared projection names.
The ?as_of= token is a row-count watermark, not a wall-clock instant, and is valid
only within a compaction epoch — discard pinned tokens after a detected reopen. See
snapshots.
Operational routes (no auth)#
These sit outside the JWT tenant guard so load balancers and probes reach them without a token:
| Route | Response | Use |
|---|---|---|
GET /health | {"status":"ok"} | Liveness — never touches the DB. |
GET /ready | {"status":"ready"} | Readiness — acquires a read lock. |
GET /metrics | {"model_count", "rows_per_model", "total_rows"} | Minimal per-model row counts (JSON, not Prometheus text). |
GET /snapshot | {"watermarks": {"<Model>": <n>}} | Current per-model watermarks (for ?as_of= reads). |
Point liveness probes at /health, readiness at /ready. See
deployment.
WebSocket routes#
Realtime streams. /subscribe, /live-query, and /replicate all sit behind the
tenant guard.
| Route | Kind | Payload |
|---|---|---|
GET /subscribe/<model-kebab> | Change feed (insert/update/delete/link events). | Typed JSON events, filtered by a generated per-model matcher. |
GET /live-query/<model>?<field>=<value> | Stateful result-set subscription. | Typed <Model>LiveDelta (Init/Added/Updated/Removed). |
GET /replicate?after=<offset> | Durable, resumable replication stream. | Opaque binary PersistedEvent frames from a monotonic global offset. |
/replicate requires the broker
The /replicate endpoint (and the browser read-replica that consumes it) only functions
when the durable broker is attached. Set [runtime].replication = true — see
[runtime]. /subscribe and /live-query are in-process and
always available; their broadcast buffer is [runtime].changefeed_capacity.
WebSocket clients send the JWT in the Authorization header. Behind a reverse proxy,
forward the Upgrade/Connection headers. See
deployment.
Related#
- TypeScript SDK — the generated typed client over this API.
[tenant]&[auth]— which routes the guard covers.