Skip to content

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:

bash
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.txt is a curated index of every page with descriptions, following the llms.txt convention.
  • https://korajs.dev/llms-full.txt is the entire documentation set concatenated into one file.
  • Any page is available as raw markdown by appending .md to its URL. For example, https://korajs.dev/guide/react-hooks.md returns 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 ​

  1. The schema is the source of truth. Collections are defined with defineSchema and the t.* field builders, by convention in src/schema.ts. To change a data shape, edit the schema first; types flow from it. Never hand-write record types.
  2. 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.
  3. Await readiness outside components. Call await app.ready before touching app.<collection> methods in non-UI code. The framework bindings handle readiness inside components.
  4. Offline must keep working. Every feature must function with the network off. Never gate a read or write on connectivity. Checking navigator.onLine before a data operation is a sign you have taken a wrong turn.
  5. Surface mutation errors. Fire-and-forget mutate folds errors into the mutation state, so render mutation.error, or use mutateAsync and handle the promise.
  6. 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.
  7. Do not add loading spinners for local reads. Local queries need no network: useQuery renders [] on the very first render and the rows arrive right after mount. Use useQueryState's ready when "not loaded yet" must look different from "no rows".

The data API ​

ts
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.

tsx
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 forIn Kora you write
fetch('/api/todos') then cache the JSONapp.todos.where(...) or useQuery(...), already local and reactive
A REST or GraphQL endpoint for CRUDapp.todos.insert / update / delete, synced automatically
react-query, SWR, or a Redux slice for server stateReactive queries; they are the store
A loading spinner for a data readNothing; local reads need no network (useQueryState().ready if you must tell "not loaded yet" apart)
useEffect to fetch on mountSubscribe with useQuery; it fires immediately and on change
navigator.onLine guards before writingWrite unconditionally; offline writes queue and sync later
Hand-written TypeScript interfaces for recordsTypes inferred from defineSchema
Manual conflict handling on the serverThe built-in per-field merge; declare .merge('counter') and similar, or a resolve function, only for domain-specific merges
localStorage or IndexedDB glueThe 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.