Experimental: the @kozou/api REST layer
Experimental (Kozou v0.2).
@kozou/apiis not published to npm yet. Its API and wire format may change without notice while the package is stabilising. In the v0.2 line, PostgREST remains the default REST layer;@kozou/apiis strictly opt-in.
@kozou/api is Kozou’s own REST layer. 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.
It is the in-house data layer that a future Kozou v1.0 is expected to
make the default.
This page covers what @kozou/api is and why it exists, how to turn it
on, the endpoint shapes it generates, the OpenAPI document it derives
from your COMMENT text, and the security boundary you must respect.
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”In v0.1, Kozou wires up an external PostgREST container to serve REST,
and the Admin UI talks 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.
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/apienabled, 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.
@kozou/api is experimental on purpose. It is a preview of the v1.0
data layer, not a stable contract — treat it accordingly.
How to enable it
Section titled “How to enable it”@kozou/api is gated behind a flag on kozou dev.
Without the flag, kozou dev behaves exactly as before and uses the
default REST adapter (PostgREST).
# Default: Admin UI + MCP, REST served by PostgRESTkozou dev
# Experimental: Admin UI + MCP, REST served by the in-house @kozou/apikozou dev --adapter apiWith --adapter api, kozou 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 only thing that changes versus the default is the data path: the
Admin UI’s server-side fetches now reach @kozou/api (in-process)
instead of a PostgREST container, and @kozou/api issues SQL to
PostgreSQL directly. The Admin UI’s own code is unchanged.
| Flag | Default | Description |
|---|---|---|
--adapter api | (omitted = PostgREST) | Use the in-house @kozou/api backend. The only supported value is api. |
--api-port <n> | 3335 | Port for the @kozou/api server (used when --adapter api is set). |
# Move the @kozou/api port off 3335kozou dev --adapter api --api-port 4000Passing --adapter with any value other than api is an error; omit
it entirely to use the default REST adapter. For the full set of
kozou dev options, see the dev command page.
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 table row by its single-column
primary key. It returns the row, or 404.
curl -s "$B/products/42" | jqThe item routes (get, update, delete) require a single-column
primary key; a request against a table with a composite primary key
returns 400.
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 (and get) endpoint only. 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.
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"]. Tables get list, create, get, update, and delete
paths; views get the read-only list and get paths.
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/corebut is not enforced, and the v0.2@kozou/apidoes not surface it in the OpenAPI document. Policy enforcement is a future concern.
Security boundary
Section titled “Security boundary”@kozou/api ships with no authentication in v0.2. It is built for
use inside a trusted boundary — local development, or a private
compose network — and its defaults reflect that:
- Zero-auth. There is no authentication layer.
- Loopback by default. The server binds to
127.0.0.1. When you run it throughkozou dev --adapter api, 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. - Trusted local/dev use only. Because it is zero-auth, do not expose
@kozou/apibeyond 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.
JWT authentication and PostgreSQL row-level security (RLS) are a future
v1.0 concern, not part of the v0.2 experimental layer. Until they
land, keep @kozou/api inside a trusted boundary.
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 api.