Docs / Protocols
REST surface
Generate a REST API from Postgres — JSON:API 1.1 with sparse fieldsets, compound include, FiltrQL on the query string, aggregates, actions, ETag / If-Match.
gqlith generates a JSON:API 1.1 REST API from your Postgres schema, served at /api/v1 with the same RBAC, row filters and tenancy enforced on every request.
Enabled by setting targets.restServer: jsonapi. Defaults: /api/v1, page[size]: 25, ETag enabled.
Media type
Requests with a body (POST / PATCH) must send Content-Type: application/vnd.api+json — wrong content-type returns 415, and parameterized media types (; charset=utf-8) are rejected on Accept. Every response advertises its capabilities via JSON:API profiles (meta.profile), so clients can feature-detect filtering and aggregates.
List
GET /api/v1/recipes?filter=status = 'published' AND servings >= 4&sort=-createdAt
Authorization: Bearer <jwt>
Accept: application/vnd.api+json
{
"data": [{
"type": "recipes",
"id": "1",
"attributes": { "title": "Pasta Carbonara", "status": "published", "servings": 4 },
"relationships": {
"cuisine": {
"links": { "related": "/api/v1/recipes/1/cuisine" },
"data": { "type": "cuisines", "id": "2" }
}
}
}],
"meta": { "totalCount": 42 },
"links": {
"first": "/api/v1/recipes?page[number]=1",
"next": "/api/v1/recipes?page[number]=2"
}
}
Filter — FiltrQL
The full FiltrQL grammar is at Filtering. The same expressions work in the MCP surface’s filterql argument.
GET /api/v1/recipes?filter=cuisine.name icontains 'italian' AND reviews ANY (rating >= 4)
GET /api/v1/recipes?filter=createdAt >= now-7d&sort=-createdAt
Aggregates over REST
Every entity gets an aggregate endpoint under the same permission model — analytics without a resolver:
GET /api/v1/recipes/aggregate?aggregations[count]=true&aggregations[avg]=servings
&filter=status = 'published'
Returns a single recipes_aggregate resource carrying count and the requested sum / avg / min / max figures.
Pagination
Page-based (default) or cursor-based:
GET /api/v1/recipes?page[size]=10&page[number]=2
GET /api/v1/recipes?page[after]=<base64-cursor>&page[size]=10
page[size] is capped (default 100); exceeding the cap returns 400 with code PAGE_SIZE_EXCEEDED.
Sparse fieldsets
Per-type, including the types pulled in by include:
GET /api/v1/recipes/1?fields[recipes]=title,slug,status&fields[cuisines]=name
Unknown fields return 400 with source.parameter: "fields[recipes]".
Compound documents and relationship routes
GET /api/v1/recipes/1?include=cuisine,author # full related resources, deduplicated
GET /api/v1/recipes/1/cuisine # related resource
GET /api/v1/recipes/1/relationships/cuisine # linkage only — { type, id }
Include depth and total resources are capped (restServer.include.maxDepth, restServer.include.maxResources).
Create, update, delete
POST /api/v1/recipes
Content-Type: application/vnd.api+json
{ "data": { "type": "recipes", "attributes": { "title": "Pasta", "slug": "pasta", "status": "published" } } }
Response: 201 Created with a Location header.
PATCH /api/v1/recipes/42
If-Match: "abc123etag"
{ "data": { "type": "recipes", "id": "42", "attributes": { "status": "archived" } } }
| Status | Meaning |
|---|---|
200 | OK — soft-delete tables return the tombstone record on DELETE |
201 | Created |
204 | No Content — hard-delete on DELETE |
400 | Bad request — invalid filter / unknown field / page-size cap |
401 | Missing or invalid bearer token |
403 | RBAC denied |
404 | Not found, or cross-tenant isolation (intentionally identical) |
409 | Unique-constraint conflict |
412 | Stale If-Match ETag — body code STALE_OBJECT |
413 | Body too large |
415 | Wrong Content-Type |
422 | Validation error — body includes source.pointer |
428 | Lock-protected table written without If-Match |
429 | Rate limited — shared bucket with /graphql and /mcp |
Actions
Non-CRUD operations live under a generated /actions/<verb> convention, row- or collection-scoped:
POST /api/v1/recipes/42/actions/restore # soft-delete restore
POST /api/v1/gqlith_files/actions/request-upload # mint upload ticket(s)
POST /api/v1/gqlith_files/42/actions/confirm # confirm an upload
GET /api/v1/gqlith_files/42/actions/download # 302 → short-lived signed URL
Runtime schema discovery
With custom fields enabled, GET /api/v1/recipes/schema returns a permission-projected JSON Schema (2020-12) of the entity — including the caller’s tenant-defined fields — so a client can render forms it has never seen. Gated by schemaDiscovery.projected.