Skip to content

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.

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 of kozou dev.
  • A COMMENT-native OpenAPI document. Because @kozou/api reads the Schema Context, the OpenAPI it emits carries the descriptions, enum values, AI notes, and widget hints you wrote in COMMENT. See the OpenAPI section below.

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

Terminal window
# Default: Admin UI + MCP, REST served in-process by @kozou/api
kozou dev

kozou dev starts three surfaces with one command:

SurfaceDefault portNotes
Admin UI3333The generated SvelteKit app (@kozou/svelte-ui)
MCP HTTP3334The MCP server for AI agents (@kozou/mcp)
@kozou/api3335The 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.

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:

Terminal window
# Opt out of the in-house backend for one run
kozou dev --adapter postgrest

With PostgREST selected, the Admin UI’s server-side fetches reach a PostgREST container instead, through the same data-adapter seam.

FlagDefaultDescription
--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>3335Port for the @kozou/api server (used when the backend is api).
Terminal window
# Move the @kozou/api port off 3335
kozou dev --api-port 4000

For the full set of kozou dev options, see the dev command page; for the adapter.type config field it reads, see kozou.config.yaml.

@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:

Terminal window
B=http://127.0.0.1:3335

GET / returns service info and the list of available resources.

Terminal window
curl -s "$B/" | jq

GET /<resource> lists rows of a table or view. It supports:

  • page (1-based) and pageSize for pagination,
  • sort=field.asc,other.desc for ordering (multiple keys allowed),
  • search=<text> for a free-text ILIKE across text columns,
  • <column>=<value> for equality filters.

It returns { rows, total, page, pageSize }.

Terminal window
# Published products, newest first, 20 per page
curl -s "$B/products?page=1&pageSize=20&sort=created_at.desc&status=published" | jq
# Free-text search across text columns
curl -s "$B/products?search=keyboard" | jq

Free-text search covers text-typed columns; uuid columns are excluded from it, since uuid ILIKE text has no operator in PostgreSQL.

GET /<resource>/<id> fetches a single row by its primary key. It returns the row, or 404.

Terminal window
curl -s "$B/products/42" | jq

The 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:

Terminal window
# Composite key (tenant_id, id) = (7, 42)
curl -s "$B/order_lines/7,42" | jq

A 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.)

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.

Terminal window
curl -s -X POST "$B/products" \
-H 'content-type: application/json' \
-d '{"name":"Mechanical keyboard","status":"draft"}' | jq

PATCH /<resource>/<id> updates the supplied columns and returns the updated row, or 404.

Terminal window
curl -s -X PATCH "$B/products/42" \
-H 'content-type: application/json' \
-d '{"status":"published"}' | jq

DELETE /<resource>/<id> deletes by primary key and returns the deleted row, or 404.

Terminal window
curl -s -X DELETE "$B/products/42" | jq

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 with ILIKE).
  • limit — the maximum number of options to return.

It returns { options: [{ id, label }] }.

Terminal window
# Options for an orders form's "product" foreign key
curl -s "$B/products?as=options&label=name&fields=name,sku&q=key&limit=20" | jq

This corresponds to the relation search the Admin UI uses when you type into a foreign-key combobox.

  • Views are read-only. A CREATE VIEW is published as a list endpoint only — a view has no primary key, so it has no item route. A write to a view returns 405.
  • Unknown columns are rejected. A create or update body that names a column not in the table returns 400.
  • Unknown resources return 404.

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.

Terminal window
# Run the approve_order(order_id uuid) action
curl -s -X POST "$B/rpc/public.approve_order" \
-H 'content-type: application/json' \
-d '{"order_id":"…"}' | jq

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

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.

Terminal window
# The document version and the schema component names
curl -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 schema descriptions.
  • @ai notes become an x-kozou-ai extension.
  • CHECK lists and ENUM members become an enum.
  • The resolved widget becomes an x-kozou-widget extension, alongside the JSON Schema type / 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.

@policy text is parsed by @kozou/core and surfaced in the OpenAPI document as an x-kozou-policy extension (on the table/view and column schemas). It is advisory metadata for AI agents and clients — @kozou/api does not enforce it. Access control is the job of your PostgreSQL row-level security; see Security boundary below.

@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 through kozou 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 auth config, 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.

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.