~/gqlith — docs/rest.md
Early access gqlith is in private early access — the code is not publicly available yet. These docs preview the surface so you know what to expect. Join the list →

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" } } }
StatusMeaning
200OK — soft-delete tables return the tombstone record on DELETE
201Created
204No Content — hard-delete on DELETE
400Bad request — invalid filter / unknown field / page-size cap
401Missing or invalid bearer token
403RBAC denied
404Not found, or cross-tenant isolation (intentionally identical)
409Unique-constraint conflict
412Stale If-Match ETag — body code STALE_OBJECT
413Body too large
415Wrong Content-Type
422Validation error — body includes source.pointer
428Lock-protected table written without If-Match
429Rate 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.