The web framework that hands your agent the fix — database to DOM.
Kovo is built from the ground up so AI coding agents get a precise error and know exactly what to fix. And it's delightful for your users: pages are real HTML, interactive at first paint.
✓ No JS required on loadpnpm create kovo my-app$ git diff db/schema.ts - price: integer('price'), + priceCents: integer('price_cents'), $ kovo check ✗ server/queries/product.ts:14 QUERY projection reads dropped column price → select priceCents, or alias: price: products.priceCents ✗ src/product-card.tsx:13 BINDING data-bind "product.price" has no source in the query → bind product.priceCents — format in a derive ✗ src/cart/checkout.tsx:31 FORM field price is not in the cart/add mutation schema ✗ src/routes/sale.ts:8 ROUTE redirect builds /sale?max=price against a dropped param4 errors · 4 files · each with its fix0 guesses
How it works
Every layer below is checked against the next at build time. Don't take our word for it — break something:
Database
products = table({ details: nullable(json), price: integer() })
Server query
query('product', { reads: [product], load: … → shape })
Client data
<script fw-query="product"> {"price": 1299}
Rendered UI
<h2 data-bind=
"product.price">✗ FW402 — query 'product' reads a column that no longer exists server/queries/product.ts:14 — select(products.price) → the column is now priceCents — select it, or alias: price: products.priceCents every query is compiled against the live schema, so a rename can't reach production
✗ FW223 — the page depends on data the query no longer ships src/product-card.tsx:13 — data-bind="product.price" → the projection now ships priceCents — update the binding, or restore the field bindings are typed against the query's emitted shape, not against hope
✗ FW227 — binding path 'product.pricee' does not exist src/product-card.tsx:13 — data-bind="product.pricee" → did you mean product.price? the DOM is part of the type system: a typo in an attribute is a build error
this demo is plain HTML and CSS — radio buttons and :has(). that's the point.L0 on the interaction ladder
For agents
Every diagnostic teaches: the line, the reason, the fixes — so the loop is edit → check → fixed, not edit → deploy → bug report. The behavior graph is queryable too: kovo explain mutation cart/add answers "what refreshes?" with diffable output for CI.
13 │ render: () => <h2>{product.details.name}</h2> ✗ FW227 — product.details can be null here fix 1 {product.details?.name} fix 2 make the projection non-null in the query ✓ caught in 0.4s — before anything ran
For users
No hydration means no window where the page looks ready but isn't. A button works the moment it paints.
Typical SPAinteractive at 3.2s
0ms paint⚠ looks ready, ignores clicks3.2s
Kovointeractive at first paint
0ms paint✓ every click works — tiny loader, handlers on demand
With JavaScript off, every page still renders and every form still posts. This site runs on Kovo — try it.
● all build gates green│ loader 1,913 B gzip — measured this build│ TTI = first paint│ JS-off: every page│ fixpoint compile see how it's verified →