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.
Start the Admin UI
Section titled “Start the Admin UI”kozou dev runs the bundled @kozou/svelte-ui Admin UI alongside an
MCP HTTP server, both wired up from your kozou.config.yaml:
kozou devThe Admin UI listens on port 3333 and the MCP HTTP server on 3334
by default; open the UI at:
http://localhost:3333Ctrl-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 withkozou dev --adapter api; see Experimental API. Either way the Admin UI is unchanged — it talks to a backend through one adapter interface.
The dashboard
Section titled “The dashboard”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.
The list view
Section titled “The list view”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:
| Parameter | Meaning | Default |
|---|---|---|
?q=<text> | Case-insensitive search across the resolved text-like columns | (none) |
?sort=col:asc,col2:desc | One or more sort segments | (none) |
?page=<n> | 1-based page number | 1 |
?pageSize=<m> | Rows per page | 50 |
Practical notes:
- Search. Type in the search box; the URL gains
?q=…and the rows narrow. Search runs anilikematch 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
asc→desc. 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.
Create and edit rows
Section titled “Create and edit rows”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.
Widget-aware inputs
Section titled “Widget-aware inputs”Each column maps to a widget, and the widget decides the input control. The common mappings:
| Column shape | Widget | Input control |
|---|---|---|
Enum (a CHECK ... IN (...) set or a PostgreSQL enum type) | enum-select | Dropdown of the allowed values |
| Boolean | boolean | Checkbox |
date | date | Native date picker |
timestamp / timestamptz / time | datetime | Date-and-time input |
| Foreign key | relation-select | Searchable relation picker (see below) |
Number (int, numeric, float, …) | number | Numeric input |
json / jsonb | json | JSON text input |
uuid | uuid | UUID text input |
text whose name looks like a URL/image | image-url | URL text input |
text flagged as long-form | textarea | Multi-line text area |
| Anything else | text | Single-line text input |
A couple of these are worth seeing in action:
- Enum → dropdown. A
productstable with astatus text NOT NULL CHECK (status IN ('draft', 'published', 'archived'))column rendersstatusas a dropdown whose options are exactlydraft,published, andarchived. The same happens for a native PostgreSQLenumtype. If a column is optional, the dropdown adds a blank--choice so you can leave it unset. - Boolean → checkbox. A
published booleancolumn renders as a single checkbox. - Date → date picker. A
published_at datecolumn renders as a native browser date picker.
The relation picker (foreign keys)
Section titled “The relation picker (foreign keys)”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.)
Read-only views
Section titled “Read-only views”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.
How @widget and ui-hints shape the inputs
Section titled “How @widget and ui-hints shape the inputs”The widget for a column is resolved with a clear precedence, so you can start from zero configuration and tighten only what needs tightening:
- ui-hints — an explicit
widgetfor a column inui-hints.yamlwins. @widgetin the columnCOMMENT— the next authority.- Heuristic — otherwise Kozou infers the widget from the column:
a foreign key becomes
relation-select, an enum becomesenum-select, aboolbecomesboolean, adatebecomesdate, numeric types becomenumber, 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_nameui-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.
Where to next
Section titled “Where to next”kozou dev— the command that serves the Admin UI, its flags, and the experimental--adapter apioption.- COMMENT conventions — the
@widgettag and the rest of the COMMENT vocabulary. - ui-hints — project-level overrides for labels, display fields, and widgets.