Kora for AI Agents
This page is written for AI coding agents (Cursor, Claude Code, Copilot, and similar) and for the developers who work alongside them. Kora owns the entire data plane, so the habits that serve a typical REST plus client-cache web app actively work against you here. Read the rules, then the anti-patterns.
If you scaffolded with create-kora-app, an AGENTS.md already sits in your project root with these rules. If you added Kora to an existing project, drop the same file in with:
npx -p @korajs/cli@beta kora agents-md(npx kora alone works inside projects that have @korajs/cli installed; elsewhere it would fetch an unrelated npm package named kora.)
Commit it so agents pick up the conventions automatically. Re-run with --force to regenerate it after an upgrade.
Machine-readable docs
You do not need to scrape the rendered site. Three endpoints exist for programmatic use:
https://korajs.dev/llms.txtis a curated index of every page with descriptions, following the llms.txt convention.https://korajs.dev/llms-full.txtis the entire documentation set concatenated into one file.- Any page is available as raw markdown by appending
.mdto its URL. For example,https://korajs.dev/guide/react-hooks.mdreturns the source of the React Hooks guide. Every HTML page also advertises its markdown twin through a<link rel="alternate" type="text/markdown">tag.
Prefer these over fetching HTML. They are smaller, stable, and free of navigation chrome.
The rules that matter
- The schema is the source of truth. Collections are defined with
defineSchemaand thet.*field builders, by convention insrc/schema.ts. To change a data shape, edit the schema first; types flow from it. Never hand-write record types. - Never fetch application data over HTTP. Do not add REST or GraphQL calls for app data, and do not talk to the sync server directly. Read and write through Kora collections; sync happens in the background.
- Await readiness outside components. Call
await app.readybefore touchingapp.<collection>methods in non-UI code. The framework bindings handle readiness inside components. - Offline must keep working. Every feature must function with the network off. Never gate a read or write on connectivity. Checking
navigator.onLinebefore a data operation is a sign you have taken a wrong turn. - Surface mutation errors. Fire-and-forget
mutatefolds errors into the mutation state, so rendermutation.error, or usemutateAsyncand handle the promise. - Do not add a state library for app data. Reactive queries are the store. Reaching for react-query, SWR, Redux, or Zustand to hold collection data duplicates what Kora already does.
- Do not add loading spinners for local reads. Local queries need no network:
useQueryrenders[]on the very first render and the rows arrive right after mount. UseuseQueryState'sreadywhen "not loaded yet" must look different from "no rows".
The data API
await app.ready
const rec = await app.todos.insert({ title: 'Ship the beta' }) // defaults and .auto() applied
await app.todos.update(rec.id, { completed: true }) // partial update, changed fields only
await app.todos.delete(rec.id)
const one = await app.todos.findById(rec.id)
const unsubscribe = app.todos
.where({ completed: false })
.orderBy('createdAt', 'desc')
.subscribe((rows) => {
// fires immediately with current data, then on every change
})In components, use the hooks instead. The mutation object exposes mutate (fire-and-forget), mutateAsync (awaitable), reset, isLoading, and error. Sync state comes from useSyncStatus().status, which is one of connected, reconnecting, syncing, synced, offline, clock-error, error, schema-mismatch, auth-required or encryption-locked.
import { createKoraHooks } from '@korajs/react'
// Typed hooks: collection names, inserts and rows are checked against the schema.
const { useCollection, useMutation, useQuery, useSyncStatus } = createKoraHooks<typeof app>()
function Todos() {
const todos = useCollection('todos')
const rows = useQuery(todos.where({ completed: false }).orderBy('createdAt'))
const addTodo = useMutation(todos.insert)
const status = useSyncStatus() // status.status, status.pendingOperations
return (
<>
<button onClick={() => addTodo.mutate({ title: 'x' })}>Add</button>
{addTodo.error && <p role="alert">{addTodo.error.message}</p>}
<p>{rows.length} open, sync {status.status}</p>
</>
)
}The Vue and Svelte bindings expose the same names from @korajs/vue and @korajs/svelte. See React Hooks for the full reference.
Translation from a typical web app
If your instinct comes from REST plus a client cache, here is the mapping.
| You would normally reach for | In Kora you write |
|---|---|
fetch('/api/todos') then cache the JSON | app.todos.where(...) or useQuery(...), already local and reactive |
| A REST or GraphQL endpoint for CRUD | app.todos.insert / update / delete, synced automatically |
| react-query, SWR, or a Redux slice for server state | Reactive queries; they are the store |
| A loading spinner for a data read | Nothing; local reads need no network (useQueryState().ready if you must tell "not loaded yet" apart) |
useEffect to fetch on mount | Subscribe with useQuery; it fires immediately and on change |
navigator.onLine guards before writing | Write unconditionally; offline writes queue and sync later |
| Hand-written TypeScript interfaces for records | Types inferred from defineSchema |
| Manual conflict handling on the server | The built-in per-field merge; declare .merge('counter') and similar, or a resolve function, only for domain-specific merges |
localStorage or IndexedDB glue | The local SQLite store, configured through the schema |
Conflict handling
Concurrent edits converge automatically: last-write-wins per field, an element multiset for arrays (concurrent additions and removals both apply, duplicates are kept), per-key merging for objects, and character-level CRDT for t.richtext() fields. When a field needs domain-specific merging, declare it in the schema: .merge('counter') for an inventory quantity (or use op.increment), .merge('max')/'min', or a collection-level resolve: { field: (local, remote, base) => ... }. Rules across records (unique, capacity) are constraints, enforced by the sync server. Do not write your own merge or sync code. See Conflict Resolution for the model.
Verifying you did it right
A correct Kora feature has no data-fetching code, no manual cache, no loading state for local reads, and no hand-written record types. If a diff you are about to write includes any of those, revisit the rules above before committing.

