Skip to content

Experimental: the @kozou/api REST layer

Experimental (Kozou v0.2). @kozou/api is 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/api is 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.

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/api enabled, 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 experimental on purpose. It is a preview of the v1.0 data layer, not a stable contract — treat it accordingly.

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

Terminal window
# Default: Admin UI + MCP, REST served by PostgREST
kozou dev
# Experimental: Admin UI + MCP, REST served by the in-house @kozou/api
kozou dev --adapter api

With --adapter api, 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 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.

FlagDefaultDescription
--adapter api(omitted = PostgREST)Use the in-house @kozou/api backend. The only supported value is api.
--api-port <n>3335Port for the @kozou/api server (used when --adapter api is set).
Terminal window
# Move the @kozou/api port off 3335
kozou dev --adapter api --api-port 4000

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

@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 table row by its single-column primary key. It returns the row, or 404.

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

The item routes (get, update, delete) require a single-column primary key; a request against a table with a composite primary key returns 400.

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 (and get) endpoint only. 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.

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"]. 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.

@policy text is parsed by @kozou/core but is not enforced, and the v0.2 @kozou/api does not surface it in the OpenAPI document. Policy enforcement is a future concern.

@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 through kozou 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/api 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.

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.

  • The three surfaces — how the REST API sits alongside the Admin UI and MCP context.
  • COMMENT conventions — the @ai, @widget, @policy, and @example tags that shape the OpenAPI descriptions.
  • The dev command — the full set of kozou dev options, including --adapter api.