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 + RoutePurposeSuccessErrors
GET /api/postList with filter/sort/paginate.200 {data, total, limit, offset}
POST /api/postCreate 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.200404, 409, 422
DELETE /api/post/{id}Delete by id.204404, 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.

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.

ParamApplies toMeaning
?<field>=<value>listExact-match filter on a filterable field (typed compare — ?n=3 matches a stored 3.0).
?sort=<field> / sort directionlistSort by a sortable field.
?limit= / ?offset=listPagination (limit is clamped to a maximum).
?projection=<name>get + listReturn only a declared @projection's columns. Unknown name → 400; absent → full record.
?as_of=<watermark>get + listPoint-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:

RouteResponseUse
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.

RouteKindPayload
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.

Search documentation

Find pages across the ForgeDB docs