Skip to content

Using the Admin UI

The Admin UI is one of the three surfaces Kozou generates from your database. You do not scaffold or hand-write it: @kozou/svelte-ui reads the schema that @kozou/introspect and @kozou/core build from your CREATE TABLE, CREATE VIEW, and COMMENT ON statements, then renders a browsable, editable interface over every table and view in the introspected schema. Add a column, ship a new COMMENT, and the UI reflects it on the next start — there is no separate UI codebase to keep in sync.

This page is a practical walkthrough: start the server, read the dashboard, work a list view, and create or edit a row. For how COMMENT text becomes the labels and inputs you see here, see COMMENT conventions. For project-level overrides, see ui-hints.

kozou dev runs the bundled @kozou/svelte-ui Admin UI alongside an MCP HTTP server, both wired up from your kozou.config.yaml:

Terminal window
kozou dev

The Admin UI listens on port 3333 and the MCP HTTP server on 3334 by default; open the UI at:

http://localhost:3333

Ctrl-C (SIGINT / SIGTERM) tears both servers down. If you scaffolded your project with create-kozou, this is the same command that the kozou service runs inside the generated docker-compose.yml, so docker compose up brings the UI up on the same port. To change the port, set server.ui.port in kozou.config.yaml. For the full command surface and flags, see kozou dev.

In v0.1.1 the REST surface behind the UI is served by PostgREST. The v0.2 line adds an experimental in-house REST layer, @kozou/api, selectable with kozou dev --adapter api; see Experimental API. Either way the Admin UI is unchanged — it talks to a backend through one adapter interface.

The root route (/) is the dashboard. It lists every table and every view in the introspected schema in two sections:

  • Tables — each entry links to that table’s list view, where you can read, create, edit, and delete rows.
  • Views — each entry links to a read-only list view.

Each entry shows a label and a short description. Both come from your schema: the label is the table or view’s display name, and the description is the prose from its COMMENT. Because Kozou treats a COMMENT as prose first and @-prefixed tags as opt-in structure, the human-readable text is what surfaces here. If a table or view has no COMMENT, only its label is shown.

If the dashboard reports no tables or no views, the introspected schema is empty — check your DATABASE_URL and the database.schemas list in kozou.config.yaml.

Clicking a table opens its list view at /tables/<table>. It renders a table of rows with a search box, sortable column headers, and Prev / Next pagination. Every piece of list state lives in the URL, so any view is bookmarkable and shareable:

ParameterMeaningDefault
?q=<text>Case-insensitive search across the resolved text-like columns(none)
?sort=col:asc,col2:descOne or more sort segments(none)
?page=<n>1-based page number1
?pageSize=<m>Rows per page50

Practical notes:

  • Search. Type in the search box; the URL gains ?q=… and the rows narrow. Search runs an ilike match over the text-like columns Kozou resolves for the table. Non-text columns — uuid, for example — are not searched, so a search term never errors on a column that has no text match operator.
  • Sort. Click a column header to sort by it; clicking the same header again toggles the direction ascdesc. The ?sort= parameter updates to match.
  • Pagination. Use Prev / Next to page through results. Adjust ?pageSize= in the URL to change page size.

The columns shown are chosen for you: Kozou leads with the table’s display field (the human-friendly column it resolves for each row, such as name or title) and shows a handful of leading columns beside it. You can change which column is the display field through ui-hints.

Each row links to a detail page, and the list view carries a + New button plus per-row Edit and Delete controls.

From a table’s list view, + New (at /tables/<table>/new) opens a create form; from a row’s detail page, Edit (at /tables/<table>/<id>/edit) opens the same form pre-filled with the current values. Both generate one input per column and pick the input control from each column’s widget (covered below). Submitting a valid create form lands you on the new row’s detail page; submitting an edit returns you to the detail page with the change applied. Delete on the detail page removes the row and returns to the list.

Validation is derived from the schema before the form ever submits: NOT NULL becomes a required field, and a column’s data type and any extracted enum values constrain what you can enter. Columns the database can populate on its own are not asked of you: a column with a DEFAULT (for example a gen_random_uuid() primary key, or a timestamp like created_at DEFAULT now()) renders as read-only on the form and is left for the database to fill.

Each column maps to a widget, and the widget decides the input control. The common mappings:

Column shapeWidgetInput control
Enum (a CHECK ... IN (...) set or a PostgreSQL enum type)enum-selectDropdown of the allowed values
BooleanbooleanCheckbox
datedateNative date picker
timestamp / timestamptz / timedatetimeDate-and-time input
Foreign keyrelation-selectSearchable relation picker (see below)
Number (int, numeric, float, …)numberNumeric input
json / jsonbjsonJSON text input
uuiduuidUUID text input
text whose name looks like a URL/imageimage-urlURL text input
text flagged as long-formtextareaMulti-line text area
Anything elsetextSingle-line text input

A couple of these are worth seeing in action:

  • Enum → dropdown. A products table with a status text NOT NULL CHECK (status IN ('draft', 'published', 'archived')) column renders status as a dropdown whose options are exactly draft, published, and archived. The same happens for a native PostgreSQL enum type. If a column is optional, the dropdown adds a blank -- choice so you can leave it unset.
  • Boolean → checkbox. A published boolean column renders as a single checkbox.
  • Date → date picker. A published_at date column renders as a native browser date picker.

A foreign-key column renders as a searchable relation picker rather than asking you to type a raw id. The picker pairs a search box with a dropdown of candidate rows: as you type, Kozou queries the referenced table and refreshes the options, and each option is shown by the related row’s display field — not its primary key — so you pick a human-readable label and Kozou stores the underlying id.

For example, in a books table with an author_id foreign key into authors, the author_id input lets you search authors by name and select one; the form submits the matching author’s id. On a detail page, the same relationship is resolved back to the author’s label so you read a name instead of an opaque id. (v0.1 edits relations one level deep; there is no nested create-the-related-row-inline flow.)

A CREATE VIEW shows up under Views on the dashboard and opens at /views/<view>. A view list behaves like a table list for reading — the same search, sort, and pagination — but it is read-only: there are no + New, Edit, or Delete controls, and rows do not link to an editable detail page. Views are the right place to expose a curated, joined, or filtered projection of your data for browsing without exposing write access to it.

The widget for a column is resolved with a clear precedence, so you can start from zero configuration and tighten only what needs tightening:

  1. ui-hints — an explicit widget for a column in ui-hints.yaml wins.
  2. @widget in the column COMMENT — the next authority.
  3. Heuristic — otherwise Kozou infers the widget from the column: a foreign key becomes relation-select, an enum becomes enum-select, a bool becomes boolean, a date becomes date, numeric types become number, and so on (the table above lists the defaults).

Reach for @widget when the heuristic cannot see your intent. The most common case is a free-text column that should be a fixed set of choices, or one PostgreSQL stores as text but you want rendered as a text area:

COMMENT ON COLUMN products.status IS
'Lifecycle state of the product.
@widget: enum-select';
COMMENT ON COLUMN products.description IS
'Long-form product copy.
@widget: textarea';

@widget is valid on columns only, and the tag line is stripped from the prose Kozou displays — the description you write above it is what appears as the field’s help text. The full tag vocabulary (@ai, @widget, @policy, @example) is documented in COMMENT conventions.

When the override belongs to the project rather than the schema — a nicer table label, a different display field, or a forced widget you would rather not encode in SQL — put it in ui-hints.yaml:

tables:
products:
label: Catalog
displayField: name
columns:
description:
widget: textarea
authors:
displayField: full_name

ui-hints is entirely optional: your DDL and COMMENT already give Kozou enough to render a usable UI. See ui-hints for the full set of keys and how the override layer composes with your schema.

  • kozou dev — the command that serves the Admin UI, its flags, and the experimental --adapter api option.
  • COMMENT conventions — the @widget tag and the rest of the COMMENT vocabulary.
  • ui-hints — project-level overrides for labels, display fields, and widgets.