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 kora agents-mdCommit 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.
useQueryreturns data synchronously from the local store, so there is nothing to wait on.
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, syncing, synced, offline, clock-error, error, or schema-mismatch.
import { useCollection, useMutation, useQuery, useSyncStatus } from '@korajs/react'
const todos = useCollection('todos')
const rows = useQuery(todos.where({ completed: false }).orderBy('createdAt'))
const addTodo = useMutation((data) => todos.insert(data))
addTodo.mutate({ title: 'x' }) // errors land in addTodo.error
await addTodo.mutateAsync({ title: 'x' }) // resolves with the result; throws on failure
const status = useSyncStatus() // status.status, status.pendingOperationsThe 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 are synchronous |
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 three-tier merge engine; add 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, add-wins for arrays, and character-level CRDT for t.richtext() fields. When a field needs domain-specific merging, such as an inventory counter, add a resolve function to that field in the schema. 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.

