The @kozou/api REST layer
@kozou/api is Kozou’s own REST layer, and the default backend since
v1.0. Given the same Schema Context that drives every other
surface — built from your CREATE TABLE, CREATE VIEW, and
COMMENT ON statements — it serves the tables and views of your
database as a REST API, with no hand-written route code.
This page covers what @kozou/api is and why it exists, how it runs
under kozou dev (and how to opt out to an external PostgREST), the
endpoint shapes it generates, the OpenAPI document it derives from your
COMMENT text, and the security boundary it draws. For where REST sits
among Kozou’s outputs, see The three surfaces.
For the tag grammar behind the descriptions, see
COMMENT conventions.
What it is, and why
Section titled “What it is, and why”Earlier Kozou releases wired up an external PostgREST container to serve
REST, and the Admin UI talked to it through a pluggable data adapter.
@kozou/api replaces that external container with code Kozou owns: it
generates CRUD endpoints and an OpenAPI document directly from the
Schema Context, and queries PostgreSQL itself. Since v1.0 it is the
default — kozou dev runs it in-process, and PostgREST is an opt-out
(see How it runs below).
The Admin UI reaches @kozou/api through the same data-adapter seam it
already uses for PostgREST, so switching the data layer is not a
breaking change for UI code — the same browser flows run unchanged
against either backend.
Two motivations stand out:
- One less moving part. With
@kozou/api(the default), you do not run a separate PostgREST container; the REST layer starts in-process with the rest ofkozou dev. - A COMMENT-native OpenAPI document. Because
@kozou/apireads the Schema Context, the OpenAPI it emits carries the descriptions, enum values, AI notes, and widget hints you wrote inCOMMENT. See the OpenAPI section below.
How it runs
Section titled “How it runs”@kozou/api is the default backend for kozou dev.
With no extra configuration, kozou dev serves REST in-process through
@kozou/api — no PostgREST container required.
# Default: Admin UI + MCP, REST served in-process by @kozou/apikozou devkozou dev starts three surfaces with one command:
| Surface | Default port | Notes |
|---|---|---|
| Admin UI | 3333 | The generated SvelteKit app (@kozou/svelte-ui) |
| MCP HTTP | 3334 | The MCP server for AI agents (@kozou/mcp) |
@kozou/api | 3335 | The in-house REST layer, bound to 127.0.0.1 |
The Admin UI’s server-side fetches reach @kozou/api in-process, and
@kozou/api issues SQL to PostgreSQL directly.
Opting out to PostgREST
Section titled “Opting out to PostgREST”To use an external PostgREST instead, set adapter.type: postgrest in
kozou.config.yaml (and add a PostgREST
service — the scaffolded docker-compose.yml shows one), or override it
for a single run with --adapter postgrest:
# Opt out of the in-house backend for one runkozou dev --adapter postgrestWith PostgREST selected, the Admin UI’s server-side fetches reach a PostgREST container instead, through the same data-adapter seam.
| Flag | Default | Description |
|---|---|---|
--adapter <kind> | api (from adapter.type) | The REST backend: api (in-house, the default) or postgrest (external opt-out). Overrides the adapter.type config field for one run. |
--api-port <n> | 3335 | Port for the @kozou/api server (used when the backend is api). |
# Move the @kozou/api port off 3335kozou dev --api-port 4000For the full set of kozou dev options, see the dev command
page; for the adapter.type config field it reads, see
kozou.config.yaml.
REST endpoint shapes
Section titled “REST endpoint shapes”@kozou/api generates endpoints at runtime from each table and view in
the Schema Context. The examples below use a generic products table
(with a status column constrained to draft / published /
archived) and an orders table; substitute your own resource names.
The examples assume the default port:
B=http://127.0.0.1:3335Service info
Section titled “Service info”GET / returns service info and the list of available resources.
curl -s "$B/" | jqList — pagination, sort, search, filter
Section titled “List — pagination, sort, search, filter”GET /<resource> lists rows of a table or view. It supports:
page(1-based) andpageSizefor pagination,sort=field.asc,other.descfor ordering (multiple keys allowed),search=<text>for a free-textILIKEacross text columns,<column>=<value>for equality filters.
It returns { rows, total, page, pageSize }.
# Published products, newest first, 20 per pagecurl -s "$B/products?page=1&pageSize=20&sort=created_at.desc&status=published" | jq
# Free-text search across text columnscurl -s "$B/products?search=keyboard" | jqFree-text search covers text-typed columns; uuid columns are
excluded from it, since uuid ILIKE text has no operator in
PostgreSQL.
Get by id
Section titled “Get by id”GET /<resource>/<id> fetches a single row by its primary key. It
returns the row, or 404.
curl -s "$B/products/42" | jqThe item routes (get, update, delete) address a row by primary key, so they require a table that has one. A table with a composite primary key is addressed by joining the key components with commas, in the order the key declares them:
# Composite key (tenant_id, id) = (7, 42)curl -s "$B/order_lines/7,42" | jqA resource with no primary key — a view, or a table without one — has no item route; only the list endpoint is available. (A composite-key value cannot itself contain a comma: the path segment is split on commas after URL-decoding.)
Create
Section titled “Create”POST /<resource> creates a row from a JSON body and returns 201
plus the created row. Columns that PostgreSQL can supply on its own
(a DEFAULT, a server-generated value) may be omitted; an empty body
inserts a row of column defaults.
curl -s -X POST "$B/products" \ -H 'content-type: application/json' \ -d '{"name":"Mechanical keyboard","status":"draft"}' | jqUpdate
Section titled “Update”PATCH /<resource>/<id> updates the supplied columns and returns the
updated row, or 404.
curl -s -X PATCH "$B/products/42" \ -H 'content-type: application/json' \ -d '{"status":"published"}' | jqDelete
Section titled “Delete”DELETE /<resource>/<id> deletes by primary key and returns the
deleted row, or 404.
curl -s -X DELETE "$B/products/42" | jqRelation-select — the as=options form
Section titled “Relation-select — the as=options form”For populating a relation picker, @kozou/api offers a lightweight
lookup that returns just { id, label } pairs instead of full rows:
GET /<resource>?as=options&label=<col>&fields=<a,b>&q=<text>&limit=<n>label— the column to use as each option’s display label.fields— additional columns to search against.q— the free-text query (matched withILIKE).limit— the maximum number of options to return.
It returns { options: [{ id, label }] }.
# Options for an orders form's "product" foreign keycurl -s "$B/products?as=options&label=name&fields=name,sku&q=key&limit=20" | jqThis corresponds to the relation search the Admin UI uses when you type into a foreign-key combobox.
Write rules and errors
Section titled “Write rules and errors”- Views are read-only. A
CREATE VIEWis published as a list endpoint only — a view has no primary key, so it has no item route. A write to a view returns405. - Unknown columns are rejected. A create or update body that names a
column not in the table returns
400. - Unknown resources return
404.
RPC actions — POST /rpc/<schema>.<fn>
Section titled “RPC actions — POST /rpc/<schema>.<fn>”Beyond the table and view CRUD above, since v1.4 @kozou/api also
compiles the verbs of your schema. A Postgres function whose
COMMENT carries the @expose: rpc tag becomes a callable action at
POST /rpc/<schema>.<fn>, with its arguments taken from the JSON body.
The same action is surfaced everywhere at once — in REST here, in the
OpenAPI document, in the MCP
describe_functions tool, and on the Admin UI’s “Actions” page.
# Run the approve_order(order_id uuid) actioncurl -s -X POST "$B/rpc/public.approve_order" \ -H 'content-type: application/json' \ -d '{"order_id":"…"}' | jqExposure is opt-in and never silent, and whether a caller may actually
run an action is enforced by the PostgreSQL EXECUTE privilege under the
request’s role. The RPC wire shape is a stable contract as of Kozou v1.6,
alongside the table/view CRUD surface (stable since v1.0). For the full
rules, see RPC actions.
The OpenAPI document at /openapi.json
Section titled “The OpenAPI document at /openapi.json”GET /openapi.json returns an OpenAPI 3.1 document for the whole API.
What sets it apart is that its descriptions come from your database
COMMENT text — this is the COMMENT-native OpenAPI that distinguishes
the in-house layer.
# The document version and the schema component namescurl -s "$B/openapi.json" | jq '.openapi, (.components.schemas | keys)'
# One table's schema (description, x-kozou-ai, enum, x-kozou-widget)curl -s "$B/openapi.json" | jq '.components.schemas["public.products"]'The mapping from Schema Context to the document:
- Table, view, and column descriptions (the prose body of a
COMMENT) become schemadescriptions. @ainotes become anx-kozou-aiextension.CHECKlists andENUMmembers become anenum.- The resolved widget becomes an
x-kozou-widgetextension, alongside the JSON Schematype/format.
Nullable columns are emitted as a type union — for example
["string", "null"]. A table with a primary key gets list, create,
get, update, and delete paths; a table without a primary key gets list
and create only (it has no item route); views get the read-only list
path.
A products status column written like this:
COMMENT ON COLUMN products.status IS 'Publication state of the product. @ai: Only ''published'' rows should appear in public listings. @widget: enum-select';surfaces in the document as a description of “Publication state of the
product.”, an enum of draft / published / archived (from the
column’s CHECK list), an x-kozou-ai note carrying the @ai text,
and an x-kozou-widget of enum-select. For the full tag grammar —
@ai, @widget, @policy, @example — see
COMMENT conventions.
@policytext is parsed by@kozou/coreand surfaced in the OpenAPI document as anx-kozou-policyextension (on the table/view and column schemas). It is advisory metadata for AI agents and clients —@kozou/apidoes not enforce it. Access control is the job of your PostgreSQL row-level security; see Security boundary below.
Security boundary
Section titled “Security boundary”@kozou/api runs zero-auth on loopback by default, and adds an
opt-in JWT + RLS layer when you configure one. Its defaults are built
for a trusted boundary — local development, or a private compose
network:
- Loopback by default. The server binds to
127.0.0.1. When you run it throughkozou dev, it is reached only by the Admin UI’s server-side fetch on the same host. A loud warning is printed if the server is ever bound to a non-loopback host. - Zero-auth unless configured. With no
authconfig, there is no authentication layer — do not expose it beyond a trusted boundary.
Two safety properties hold regardless of where it runs:
- Identifier allowlisting. Table, view, and column identifiers are validated against the introspected Schema Context — an allowlist — before any query is built, and are quoted defensively.
- Parameterized values. All user-supplied values are passed as parameterized query arguments; values are never interpolated into SQL text.
Opt-in JWT + RLS
Section titled “Opt-in JWT + RLS”Pass an auth config (see kozou.config.yaml)
and @kozou/api verifies a signed JWT (HS256 / RS256 / remote JWKS) on
each request, then runs that request inside a transaction under
SET LOCAL ROLE <role-from-claim> with the claims published via
request.jwt.claims. Your own PostgreSQL row-level-security (RLS)
policies then decide what each request can read and write. A request
with no token is rejected with 401 unless you configure an anonymous
role (anonRole), in which case it runs under that role with empty
claims; a present-but-invalid token is always 401.
Kozou verifies tokens and switches role; issuing identity (registration, login, token minting) is delegated to an external provider — see Authentication and authorization.
Where to next
Section titled “Where to next”- The three surfaces — how the REST API sits alongside the Admin UI and MCP context.
- COMMENT conventions — the
@ai,@widget,@policy, and@exampletags that shape the OpenAPI descriptions. - The dev command — the full set of
kozou devoptions, including--adapter. kozou.config.yaml— theadapterandauthconfig the backend reads.