# Clank complete documentation Source: https://docs.clank.run Framework version: 0.7.0 --- # Getting started with the npm package Canonical URL: https://docs.clank.run/docs/getting-started Raw source: https://docs.clank.run/raw/getting-started.md Install one npm package, create an authenticated full-stack application, and run it locally. You do not need to clone the Clank repository or assemble a framework toolchain. ## Requirements - Node.js 22.16 or newer. - npm 10 or newer. - A modern browser. The official package is `@clank.run/framework`. The unscoped `clank` package on npm is a different project. ## 1. Install Clank Install the package globally to make the `clank` command available in every project: ```sh npm install --global @clank.run/framework clank --version ``` The package contains the framework runtime, TypeScript and TSX compiler, project starter, development commands, deployment client, and type declarations. It has no transitive npm dependencies. Prefer a project-local CLI? Install the same package in an existing project and run its binary through npm: ```sh npm install @clank.run/framework npx clank --version ``` The rest of this guide uses the global `clank` command. ## 2. Create an application ```sh clank create my-app cd my-app npm install ``` `clank create` starts with a working product rather than a blank component. The generated application already has: - email and password registration, login, logout, secure sessions, and CSRF protection; - private per-user Todo data in SQLite; - server rendering and node-preserving browser hydration; - live updates across tabs and browsers; - validated queries and mutations with inferred TypeScript types; - Tailwind utility styling; - an initial immutable database migration; - health checks and deterministic deployment configuration; and - `README.md` and `AGENTS.md` instructions for people and coding agents. The generated `package.json` has one application dependency: ```json { "dependencies": { "@clank.run/framework": "^0.7.0" } } ``` The scaffold uses the version of the CLI that created it. Commit the generated lockfile so every human, agent, and deployment uses the same resolved package. ## 3. Run it ```sh npm run dev ``` Open `http://127.0.0.1:3000`, register an account, and create a Todo. Open the same URL in a second browser and sign in with the same account; committed changes update both sessions. The starter stores local data in `app.sqlite`. That file, generated JavaScript, and local deployment state are ignored by Git. ## Your application files This is the structure of the app created from npm, not the structure of the Clank framework repository: ```text my-app/ ├── src/ │ ├── backend.ts auth, database schema, queries, and mutations │ ├── view.tsx accessible server/client UI │ ├── app.tsx hydration, live data, and browser interactions │ └── server.tsx routes, SSR, security headers, and static files ├── migrations/ │ └── 0001_app_metadata.sql ├── AGENTS.md app map, invariants, and definition of done ├── README.md human setup and deployment instructions ├── clank.deploy.json build, database, health, and artifact contract ├── package.json scripts and the Clank package dependency └── tsconfig.json ``` Start in `src/view.tsx` when changing what the app looks like. Put trusted data rules in `src/backend.ts`, browser coordination in `src/app.tsx`, and HTTP or SSR behavior in `src/server.tsx`. Never hand-edit `dist/`; Clank generates it. ## Everyday commands The generated npm scripts keep the normal workflow short: ```sh npm run dev # build and start the local server npm run build # compile src/ into dist/ npm run doctor # check Node, config, migrations, login, and project link npm run deploy:check # build and verify an offline deployment artifact npm run deploy # build, migrate, health-check, and deploy ``` You can call the package CLI directly when you need more control: ```sh clank build src dist clank watch src dist clank doctor --json clank help --json ``` The JSON forms are stable interfaces for coding agents and automation. ## Make the first change Components are ordinary functions that return typed TSX. Reactive values use `.value`, and Clank updates only the DOM bindings that read them: ```tsx /* @clankImportSource @clank.run/framework */ import { computed, signal } from "@clank.run/framework"; export function Counter() { const count = signal(0); const label = computed(() => `Count: ${count.value}`); return ( ); } ``` The `@clankImportSource` comment tells Clank's compiler where JSX primitives come from. `agentId` gives an important control a stable machine-readable identity, while `agentLabel` explains its purpose without changing the visible UI. ## Add data safely The starter's `src/backend.ts` is the source of truth for data and authorization: ```ts import { defineDatabase, defineTable, s } from "@clank.run/framework"; export const schema = defineDatabase({ todos: defineTable({ title: s.string({ min: 1, max: 160 }), done: s.boolean(), }).owned(), }); ``` `.owned()` scopes records to the signed-in user. Define validated queries and mutations beside the schema; the browser client infers their arguments and results without a code-generation step. Read [Full-stack applications](full-stack.md), [Authentication](auth.md), and [Database revisions and correctness](database.md) before expanding the starter's data model. Add a new numbered SQL file for every schema change: ```text migrations/0002_add_due_dates.sql ``` Never edit or reorder an applied migration. Clank checks migration history before activation and backs up the database before production migrations. See [SQLite migrations](migrations.md). ## Use Tailwind The starter is already configured for Tailwind utility classes, so you can edit `class` values in TSX immediately. Clank does not wrap or reinterpret Tailwind; classes are emitted as normal HTML attributes. For a compiled production stylesheet, follow [Tailwind CSS](tailwind.md). ## Build with an agent Open the generated directory in your coding agent and describe the product you want. For example: ```text Turn this starter into a shared meal planner. Keep authentication, make every record user-owned, add immutable migrations, preserve live updates, and run npm run build, npm run doctor, and npm run deploy:check when finished. ``` The generated `AGENTS.md` tells the agent where each concern belongs, which security and migration invariants it must preserve, and how to prove the app is deployable. The documentation is also available as [a compact agent map](https://docs.clank.run/llms.txt), [the complete Markdown corpus](https://docs.clank.run/llms-full.txt), and [structured JSON](https://docs.clank.run/api/docs.json). ## Deploy Sign in once, check the app, and deploy: ```sh clank login --server=https://clank.run clank whoami npm run doctor npm run deploy ``` The first deployment creates and links an isolated project automatically. The CLI builds locally, packages the exact framework runtime and application files, verifies their digests, applies migrations, waits for the health check, and then activates the release. Use `npm run deploy:check` whenever you only want to build and inspect the artifact. It does not require login or network access. Continue with the [Deployment CLI](cli.md) for custom domains, secrets, logs, rollback, backups, organizations, and automation. ## Package imports Import from the root for most apps: ```ts import { createApp, defineBackend, renderDocument, signal, } from "@clank.run/framework"; ``` Focused public entry points are also available: ```ts import { signal } from "@clank.run/framework/core"; import { render } from "@clank.run/framework/dom"; import { createRouter } from "@clank.run/framework/router"; import { createForm } from "@clank.run/framework/forms"; import { defineAuth } from "@clank.run/framework/auth"; import { defineBackend } from "@clank.run/framework/backend"; import { createApp } from "@clank.run/framework/server"; import { serve } from "@clank.run/framework/node"; ``` Imports do not mutate global state. The package ships its TypeScript declarations and supports strict editor type checking with `"jsx": "preserve"`. ## Next steps - [Application recipes](application-recipes.md): choose the right client, server, data, and deployment shape. - [Reactivity](reactivity.md): learn signals, computed values, effects, stores, resources, and transactions. - [Rendering and components](rendering.md): understand TSX, keyed lists, SSR, and hydration. - [Routing](routing.md): add URL-driven pages, parameters, loaders, guards, and navigation. - [Authentication](auth.md): customize profiles, sessions, authorization, and the default auth UI. - [Deployment CLI](cli.md): ship and operate the application. - [Contributing to Clank](../CONTRIBUTING.md): clone the framework repository only when you want to change Clank itself. --- # Application recipes for humans and agents Canonical URL: https://docs.clank.run/docs/application-recipes Raw source: https://docs.clank.run/raw/application-recipes.md This guide is the shortest path from an application request to a readable Clank implementation. ## Choose the shape first | Application need | Clank starting point | | --- | --- | | Static marketing or content | TSX, signals for small interactions, router only when multiple client routes add value | | Rich client application | TSX, `createRouter`, forms, headless UI state, resources | | CRUD product | `defineDatabase`, `defineBackend`, typed queries/mutations, live client | | Private user application | `defineAuth`, owned tables, `createClient`, `AuthGate` | | Agent-operable workflow | schemas, named actions, semantic native controls, explicit `agentAction` | | Deployable product | `clank create`, migrations, health endpoint, `clank deploy` | Do not add a backend to a page that has no server-owned state. Do not keep private or shared state only in browser signals. ## Contract-first build order For AI-generated applications, this order minimizes rewrites: 1. Write the user journeys and data ownership rules. 2. Define schemas for persisted data, form input, and agent actions. 3. Define database tables and backend functions. 4. Build shared semantic views. 5. Add forms and interaction controllers. 6. Add Tailwind classes after the HTML structure works. 7. Add stable agent IDs to important actions. 8. Test validation, keyboard use, narrow screens, auth isolation, and live updates. 9. Add migrations and deployment configuration. Runtime schemas are the shared truth. TypeScript inference and JSON Schema both flow from them. ## Forms Prefer one `createForm` controller per independently validated user task. Good: ```ts const profile = createForm({ /* profile fields */ }); const password = createForm({ /* password fields */ }); ``` Less readable: ```ts const everySettingOnTheAccountPage = createForm({ /* unrelated sections */ }); ``` Use native `label`, `input`, `select`, `textarea`, and `button` elements. Spread the field helper props, then add classes and application-specific attributes. Remote uniqueness, authorization, inventory, pricing, and other server-owned checks belong in the submission handler or backend—not in client-only validation. ## Reusable interaction logic - Expandable content: `createDisclosure`. - Modal task: `createDialog`. - Section switching: `createTabs`. - Long tables or catalogs: `createPagination`. - Async data: `resource`. - Async side effect: `actionRunner` or a form submission. - Stable list identity: ``. - Route-level async data: router `load`. Keep controllers beside the feature that owns them. A controller should have a specific ID such as `invite-member`, not `dialog-1`. ## Agent semantics Agents already understand native controls when the HTML is accessible: ```tsx ``` Add Clank metadata where native semantics cannot express the application capability: ```tsx ``` Rules: - IDs must be deterministic and unique in the mounted surface. - Labels describe the action, not its color or position. - `agentAction` should match a discoverable server action when one exists. - Metadata never replaces authorization. - Password, file, and secret values must not be exposed through labels or state. ## CRUD and live collaboration Define the table: ```ts const schema = defineDatabase({ projects: defineTable({ name: s.string({ min: 1, max: 120 }), status: s.enum(["active", "archived"]), }).owned(), }); ``` Define inferred functions: ```ts const backend = defineBackend({ schema, auth }).functions(({ query, mutation }) => ({ projects: { list: query({ args: {}, handler: ({ db }) => db.table("projects").collect(), }), create: mutation({ args: { name: s.string({ min: 1, max: 120 }) }, handler: ({ db }, { name }) => db.table("projects").insert({ name, status: "active" }), }), }, })); ``` Use `client.live()` in the browser. The query cache reruns only when a committed change intersects its recorded dependencies, and every subscribed tab receives the new snapshot. ## Commerce Typical composition: - signals/computed values for local filters and cart preview; - server-owned price and inventory validation at checkout; - `createDialog` for the cart or quick view; - `createForm` for address and payment-provider handoff; - named actions for add/remove/checkout where agents should operate the store; - owned order tables after authentication. See `examples/commerce`. ## Dashboard Typical composition: - `createTabs` for major workspace sections; - computed filtering plus `createPagination`; - semantic native table markup; - `createDialog` + `createForm` for create/edit flows; - live queries for operational data; - role checks in backend functions, not hidden buttons alone. See `examples/dashboard`. ## Booking or wizard Use one form per step when steps have independent validation. Keep selections such as a room or plan in a signal, then compute the summary and total. The final backend mutation must recompute availability and price from server-owned data. Client totals are display state, not an authority. See `examples/booking`. ## Readability checklist - Feature names are domain names: `invite`, `checkout`, `selectedRoom`. - Components represent meaningful regions, not arbitrary visual fragments. - Event handlers are short and named when they contain business rules. - Validation schemas sit beside the workflow that consumes them. - Shared behavior uses a controller instead of repeated event-listener code. - Tailwind classes do not hide missing semantic HTML. - Comments explain security or lifecycle constraints, not obvious syntax. - Examples compile under strict TypeScript. ## Verification checklist For each generated application: - run strict TypeScript; - run unit tests for schemas and state transitions; - exercise every form's invalid and successful paths; - test keyboard dismissal/focus for dialogs; - inspect the semantic agent tree; - verify password/file values are absent; - test desktop and narrow viewport layouts; - check browser console and page errors; - for full-stack apps, test two users and two simultaneous tabs; - test deployment health failure and migration rollback before production. --- # AI-first contracts Canonical URL: https://docs.clank.run/docs/ai-first Raw source: https://docs.clank.run/raw/ai-first.md Clank separates model providers from application contracts. It does not dictate which model or SDK to use. Instead, it makes capabilities discoverable, inputs deterministic, side effects explicit, and the rendered interface machine-readable. ## Runtime schemas ```ts const CreateTask = s.object({ title: s.string({ min: 1, max: 120, description: "Short task title" }), priority: s.optional(s.enum(["low", "normal", "high"] as const)), estimate: s.nullable(s.number({ min: 0 })), tags: s.array(s.string(), { max: 10 }), }); const value = CreateTask.parse(modelOutput); const result = CreateTask.safeParse(modelOutput); const jsonSchema = CreateTask.toJSONSchema(); ``` Available builders are `string`, `number`, `boolean`, `literal`, `enum`, `unknown`, `array`, `object`, `optional`, `nullable`, and `union`. Objects are strict by default and aggregate nested validation issues with paths. Set `{ strict: false }` to preserve unknown properties. ## Actions ```ts const createTask = defineAction({ name: "tasks.create", description: "Create one task in the current workspace.", input: CreateTask, output: s.object({ id: s.string(), created: s.boolean() }), sideEffects: "write", confirmation: "write", authorize: (_input, context) => context.user != null, handler: async (input, context) => { return database.create(input, context.user); }, }); const output = await createTask(input, { user, signal }); ``` Action names use letters, digits, `.`, `_`, and `-`. Input is always validated before authorization and execution; output is validated when an output schema exists. Side-effect levels are: - `none`: pure computation. - `read`: external or private data may be read, but not changed. - `write`: state may be changed. - `destructive`: deletion, irreversible mutation, or similarly sensitive behavior. Confirmation policy is `never`, `write`, or `always`. The HTTP bridge requires `x-clank-confirmation: confirmed` for applicable write/destructive calls and returns `428 CONFIRMATION_REQUIRED` when it is absent. The host or agent is responsible for obtaining real user confirmation before setting that header; the header is not a substitute for authentication or authorization. ## Discovery and invocation bridge ```ts const bridge = createAgentBridge([createTask, archiveTask]); bridge.manifest(); await bridge.invoke("tasks.create", input, context); await bridge.handle(request, context); ``` The manifest protocol is `clank-agent/1`. The Fetch handler supports: - `GET /.well-known/clank` or `GET /manifest` - `POST /actions/:name` with a JSON input body Write/destructive example: ```ts await fetch("/actions/tasks.create", { method: "POST", headers: { "content-type": "application/json", "x-clank-confirmation": "confirmed", }, body: JSON.stringify(input), }); ``` Success is `{ ok: true, output }`. Failures have `{ ok: false, error: { code, message, details? } }` and appropriate 4xx or 500 status codes. Requests require JSON content type, are size-bounded, may be restricted to exact origins, and redact validation input values. Since the bridge consumes and returns Web `Request`/`Response`, it can be mounted in Node, edge, worker, Bun, or compatible server environments. ## Action UI state ```tsx const save = actionRunner(saveDocument); ``` An action runner exposes `pending`, `data`, `error`, `canRun`, `run`, and `reset`. Revision tracking prevents an older execution from overwriting newer UI state. ## Semantic UI Opt-in DOM properties become `data-clank-*` attributes: ```tsx ``` `agentHidden: true` removes an area from semantic inspection. It does not visually hide the element and is not an authorization boundary. On interactive HTML elements, `agentLabel` is also mirrored to `aria-label`. The same explicit name is therefore available to assistive technology, browser automation, and the Clank semantic surface; non-interactive elements retain only the `data-clank-label` metadata. ## Inspect and operate ```ts const surface = createAgentSurface(document.querySelector("#app")!); surface.inspect(); surface.input("task-title", "Ship documentation"); surface.activate("add-task"); ``` The inspection tree contains interactive or explicitly semantic elements only. It understands explicit agent labels plus native `label`, `aria-labelledby`, roles, IDs, names, required/readonly/invalid/expanded/checked/multiple state, placeholders, form values, link targets, and semantic children. Password and file-input values are never included. This is smaller and more stable than raw HTML, CSS selectors, or screenshots. `activate` and `input` target explicit `agentId` values or native element IDs. Those methods dispatch ordinary browser events so human and agent interaction use the same application behavior. File inputs are refused; password controls are write-only through this surface. Schema-aware `createForm()` controllers also expose a `clank-form/1` manifest containing field schemas and suggested controls without live values. ## Agent-described views `defineView({ name, description, props, render })` attaches `viewManifest` metadata to a component and can validate its props. Use it for reusable surfaces that an external planner or UI generator needs to discover. ## Security model - Schemas validate shape; authorization still belongs in `authorize` and the handler's data layer. - UI semantic IDs expose controls; they do not grant server permission. - Never place secrets in labels, descriptions, manifests, validation details, or rendered DOM. - Hosts must obtain meaningful user confirmation before sending the bridge confirmation header. - Treat all model-generated action input as untrusted even when the model previously inspected the UI. --- # Public beta readiness Canonical URL: https://docs.clank.run/docs/public-beta Raw source: https://docs.clank.run/raw/public-beta.md Clank's public beta is suitable for controlled self-hosted evaluation once every gate below passes. “Beta” means the contracts are usable and tested, while operators should expect upgrade work and should not place irreplaceable or highly regulated workloads on the platform without an independent review. ## Go/no-go gate - `npm run check` passes from a clean clone on Node 22.16 and Node 24. - GitHub CI and CodeQL pass with no accepted critical or high-severity finding. - The release tag exactly matches `package.json`; the tarball is attested and published through npm OIDC. - Private vulnerability reporting, branch protection, secret scanning, and protected release environments are enabled. - Browser signup/login, verification/recovery/MFA/passkey policy, CLI device login, organization RBAC, scoped token, deploy, domain, backup, rollback, and two-browser live sync are smoke-tested. - Production TLS, allowed hosts, proxy trust, CSP, rate limits, telemetry export, alerting, quotas, and runner isolation are configured. - A recent encrypted off-host backup has been restored into a clean environment. - An incident owner, status channel, rollback decision maker, and security contact are named. Any known cross-tenant access, authentication bypass, remote code execution across the documented runner boundary, secret disclosure, unrecoverable data loss, stale fenced commit, or release-provenance failure is a no-go. ## Known beta limitations - Process mode trusts deployed application code as the platform Unix user. Public multi-tenancy requires Docker at minimum and preferably dedicated VMs or microVMs. - Distributed leases, desired state, worker authentication, durable operations, and fencing are implemented, but the built-in process supervisor is not a turnkey multi-region HA control plane. Operate one active supervisor per project/data directory until leader election and remote runner integration are deployed. - The control-plane catalog uses SQLite. It supports durable coordination on one shared transactional store, not globally distributed consensus. - Built-in application data and live queries are SQLite-first. The external PostgreSQL driver/provisioner is available, but generated backend tables do not transparently switch engines. - Managed ingress performs exact-host HTTP proxying, automatically reconciles customer DNS routing with durable bounded leases, and supplies a restricted Caddy certificate-permission lookup. It does not itself issue or store certificates, change customer DNS, provide a WAF/DDoS edge, or proxy WebSocket upgrades. Put it behind the documented production edge. - Local file and email drivers are development/reference implementations. Configure durable object storage and a production email provider for hosted workloads. - Verified encrypted backups run automatically every 24 hours by default and coordinate through durable leases, but repositories remain local unless the operator replicates them. The encryption key must be backed up separately. - Release files and pre-deploy snapshots are locally retained behind enforced per-project count/byte ceilings. Pre-upgrade releases begin with upload-byte accounting; clean or redeploy old artifacts when exact extracted-size accounting is required. - Permanent site deletion removes platform-managed local project state and releases its quota, slug, port, and domains, but it does not discover or erase external databases, off-host backup replicas, copied artifacts, or edge certificate storage. Operators need a cross-system retention and erasure runbook. - Tailwind's browser build is for development; production apps should compile and serve CSS. - Passkey support accepts `none` attestation and does not perform enterprise authenticator attestation policy. - Application-specific authorization, privacy, retention, moderation, payments, regulatory compliance, and abuse prevention are not inferred by the framework. ## Suggested beta operating targets These are targets to validate in the chosen topology, not guarantees from the package: - recovery point objective: no more than the configured backup interval; - recovery time objective: demonstrated by a clean restore drill; - deploy rollback: prior healthy release restored automatically after candidate failure; - control-plane availability: measured at storage-backed `/healthz` or `/readyz`; use `/livez` only for process liveness; - app availability: measured through the public host and representative authenticated transaction; - security response: acknowledge complete private reports within three business days. ## Rollout 1. Start with maintainers and synthetic applications. 2. Add a small invited cohort with per-project quotas and Docker isolation. 3. Review incidents, failed deploys, restore drills, support load, and security findings weekly. 4. Expand only when restore time, deployment success, auth failure rates, and isolation evidence remain inside the published operating targets. 5. Preserve an immediate rollback path for framework, control-plane, schema, and edge changes. --- # Architecture Canonical URL: https://docs.clank.run/docs/architecture Raw source: https://docs.clank.run/raw/architecture.md ## Module boundaries ```text core.ts platform-neutral reactive graph and ownership dom.ts direct DOM mounting, bindings, components, context, control flow tsx.mjs compile-time JSX parsing and reactive-expression lowering router.ts URL matching, navigation state, loaders, guards ai.ts schemas, actions, discovery, semantic UI contract forms.ts schema-aware form state, validation, submission, manifests ui.ts disclosure, dialog, tabs, pagination, browser directives server.ts Fetch request router and middleware auth.ts password/session service, cookies, CSRF, roles, auth UI/client backend.ts inferred schema/functions, SQLite documents, RPC, live queries deploy.ts deterministic artifact config, packaging, verification, extraction migrations.ts immutable SQL ledger, backup, restore, transactional application platform.ts device auth, projects, secrets, audit, releases, supervision security.ts shared URL, origin, validation-redaction, and bounded JSON helpers ssr.ts escaped HTML rendering, document templates, serialized state node.ts dependency-free Node HTTP and static-file adapters index.ts public aggregate exports ``` The reactive kernel imports nothing. DOM depends only on the kernel. Routing depends on the kernel and DOM types. AI uses the kernel for action UI state and DOM types for described views. Forms depend on the kernel and schema contracts. Headless UI depends only on the kernel and browser standards. The server uses the routing matcher. There are no package imports. ## Reactive graph A source owns a set of observers. During an observer execution, signal and computed reads register both sides of the dependency. Before reevaluation, prior dependencies are detached. Computed observers invalidate synchronously and propagate dirtiness; effects enter a deduplicated queue. The outermost batch drains that queue. `Computed.peek()` prevents the caller from subscribing while still evaluating the computed under its own observer. This distinction keeps imperative snapshots fresh without accidentally wiring the surrounding effect to the computed. Ownership is a separate tree. Roots collect cleanup callbacks. Components create child roots, so unmounting a subtree disposes its event handlers, directives, effects, computed values, resources, and nested mounts together. ## DOM strategy Static element structure is created once. The TSX compiler wraps each dynamic child and property in a `ReactiveExpression`; the renderer turns each marker into one narrow effect. Primitive child changes mutate the existing `Text.data` and retain the exact node. Conditional regions get start/end markers and replace only their controlled nodes. `For` builds a key-to-row map. An array update is reconciled in O(n): removed keys are disposed, new keys mount once, retained object keys update lazy per-property row signals, and only reordered row ranges receive DOM move calls. With `by="id"`, immutable object replacement updates changed row fields without replacing the row or its text nodes. This avoids virtual-tree allocation and whole-tree diffing for ordinary state changes. See [Performance model](performance.md) for the exact update guarantees. ## Async consistency Resources and router loaders use two defenses: 1. Abort the prior operation with `AbortController`. 2. Associate every run with a monotonically increasing revision and ignore any result that is no longer current. This prevents stale state even when a promise cannot be physically canceled. ## Agent protocol The action manifest and semantic DOM tree are complementary: - The manifest describes callable application capabilities and their policies. - The semantic tree describes the currently mounted interactive surface. An agent may choose a named action directly or operate an explicit semantic control. Both paths still pass through application validation and, when correctly configured, authorization. ## Build strategy Clank's compiler parses TSX directly, lowers elements to `jsx()` calls, and lowers dynamic expression sites to lazy `expression()` markers. Node's `stripTypeScriptTypes(..., { mode: "transform" })` then removes TypeScript syntax. The build changes local `.ts`/`.tsx` import suffixes to `.js` and emits source maps. No bundling means browser module boundaries remain visible and debuggable. The checked-in declaration files are the stable consumer contract. Type tests instantiate real schemas and backend functions to ensure errors appear at call sites without generated types. The repository's strict `tsconfig.json` validates source and the examples when a TypeScript compiler is available, while the normal build itself remains package-free. ## Full-stack data flow The schema is both a runtime validator and the root TypeScript value. Table documents, branded IDs, mutation arguments, handler results, zero-codegen API references, server calls, and browser calls are conditional types derived from that value. SQLite stores canonical validated document JSON beside framework metadata. Owned tables store an immutable owner column outside application JSON. A mutation runs synchronously inside `BEGIN IMMEDIATE`; document writes, output validation, the persisted global revision, and journal records commit together. Queries use consistent read transactions and record table, document, and owner dependencies. Same-host processes catch up through the persisted journal and publish one current snapshot. A retained-history gap causes conservative full invalidation. Auth uses internal SQLite tables through a private database capability. Password work runs outside transactions; user/session writes then enter the same revision and notification system. The backend selects the auth cache partition and database owner scope and refreshes long-lived callers before operations. Auth journal changes close affected live streams across processes, so role downgrades and revocations reconnect with current authorization. SSR executes the same component tree without a DOM. Dynamic regions and keyed lists receive comment boundaries. Hydration walks those boundaries and installs the fine-grained effects on the existing nodes. A structural mismatch is surfaced as a warning and safely remounted instead of leaving partially attached behavior. ## Deployment data flow The deployment config is normalized before its build runs. The CLI executes the build command as an argument array, walks only explicit include roots, rejects links and sensitive paths, and creates a deterministic gzip document with individual file hashes. It vendors the current Clank package so execution does not depend on mutable global installation state. The control plane hashes device and access credentials, intersects organization role with project-token scope on every request, stores encrypted secrets and audit metadata in its own SQLite database, and allocates a persistent project data directory. Release directories are immutable after extraction except for a platform-generated launcher. A deployment takes the project lock, verifies and extracts a staged artifact, quiesces the previous process, snapshots SQLite, applies immutable migrations, starts a candidate, and waits for health. Activation is the final state transition. Any earlier failure restores the snapshot and previous process. Project mutations also acquire durable distributed leases. Authenticated deployment nodes, desired generations, idempotent durable operations, lease expiry, retries, draining, capacity placement, and monotonic fences reject stale workers. The included child-process supervisor still owns processes in memory and therefore runs as one active leader per project/data directory; remote agents can implement the durable orchestration contract. Child processes support trusted operation, while Docker adds a constrained container boundary for mutually untrusted applications. --- # Reactivity Canonical URL: https://docs.clank.run/docs/reactivity Raw source: https://docs.clank.run/raw/reactivity.md Clank tracks reads while a computed value or effect is running. A signal write synchronously invalidates computed values and reruns only the effects that read the changed source. Effects are deduplicated inside a batch. ## Signals ```ts const count = signal(0, { name: "cart.count" }); count.value; // tracked read count.get(); // tracked read count.peek(); // untracked read count.value = 2; // write count.set(3); // write and return the new value count.update(value => value + 1); const unsubscribe = count.subscribe((next, previous) => { console.log(previous, "→", next); }); ``` Signals use `Object.is` equality by default. Supply `{ equals: false }` to notify on every assignment or provide a custom equality function. ## Computed values ```ts const subtotal = signal(12); const taxRate = signal(0.08); const total = computed(() => subtotal.value * (1 + taxRate.value)); ``` A computed value is lazy: it evaluates on its first read, caches the result, and runs again only after a dependency changes and the value is read. Recursive computed evaluation throws a descriptive cycle error. ## Effects and cleanup ```ts const stop = effect((cleanup) => { const controller = new AbortController(); fetch(`/search?q=${query.value}`, { signal: controller.signal }); cleanup(() => controller.abort()); }); stop(); ``` The previous cleanup runs before the next effect execution and again when disposed. Effects are synchronous. Use an async resource for request state and stale-result protection instead of making an effect callback itself async. ## Batches and transactions ```ts batch(() => { firstName.value = "Grace"; lastName.value = "Hopper"; }); // dependent effects run once here ``` `transaction()` adds rollback semantics: ```ts transaction(() => { balance.value -= amount; inventory.value -= 1; if (inventory.value < 0) throw new Error("Out of stock"); }); ``` If the callback throws, every signal written by that transaction is restored before effects flush. Nested transactions merge their journals into the parent on success. ## Untracked reads ```ts effect(() => { console.log(activeId.value, untrack(() => debugSettings.value)); }); ``` Changing `debugSettings` does not rerun this effect. ## Ownership `createRoot()` owns computed values, effects, resources, and registered cleanup callbacks created inside it. Disposing the root releases all of them in reverse creation order. ```ts const feature = createRoot(dispose => { const stopPolling = startPolling(); onCleanup(stopPolling); return { dispose }; }); ``` Components automatically receive an owned root; application code usually does not need to create one. ## Stores ```ts const state = store({ profile: { name: "Ada" }, tags: ["math"], }); effect(() => console.log(state.profile.name)); state.profile.name = "Grace"; state.tags.push("compiler"); const serializable = snapshot(state); ``` Stores are lazy deep proxies. Each accessed property gets an independent signal, and object-key iteration has its own dependency. `toRaw()` returns the original object for an individual proxy; `snapshot()` recursively returns plain current values. ## Async resources ```ts const user = resource( async (id: string | undefined, { signal }) => { const response = await fetch(`/api/users/${id}`, { signal }); return response.json(); }, { immediate: false }, ); await user.reload("42"); user.data.value; user.status.value; // idle | loading | refreshing | ready | error user.loading.value; user.error.value; user.mutate(current => ({ ...current!, optimistic: true })); user.abort(); ``` Reloading aborts the prior request. Revision tracking also prevents a stale promise from overwriting newer data even when its underlying operation ignores abort signals. ## Streams `consumeStream(iterable, initial, reduce)` folds an `AsyncIterable` into a signal. It is useful for token streams, event streams, and progressive server output. --- # Rendering and components Canonical URL: https://docs.clank.run/docs/rendering Raw source: https://docs.clank.run/raw/rendering.md Clank compiles TSX into direct DOM construction and fine-grained reactive bindings. There is no virtual DOM and no whole-component rerender loop. A component executes once to establish structure; expressions update independently afterward. ## TSX components ```tsx interface BadgeProps { tone: "green" | "amber"; children: Renderable | Renderable[]; } function Badge(props: BadgeProps) { return {props.children}; } function Status() { return (
Ready <>One fragment can contain several siblings.
); } ``` Lowercase tags create HTML/SVG elements. Uppercase or member-expression tags call components. Fragments create no wrapper element. `h(type, props, ...children)` remains available as a lower-level integration API, but application code normally uses TSX. ## Automatic reactivity Any nonliteral expression in a TSX child or ordinary element property is compiled into a lazy reactive marker: ```tsx const count = signal(0); const doubled = computed(() => count.value * 2); function Counter() { return ( ); } ``` The compiler creates separate bindings for `disabled`, `count.value`, and `doubled.value`. The click handler remains a normal event callback. Updating `count` mutates the existing button property and text nodes without calling `Counter` again. Component prop expressions are lazy too: ```tsx function Greeting(props: { name: string }) { return

Hello {props.name}

; } ``` The runtime exposes expression-backed props through getters. Reading `props.name` inside the heading's compiled expression observes the latest parent value. JavaScript executed imperatively in the component body still runs once. Use TSX expressions, `Show`, `Switch`, or an `effect` for behavior that must react. ## Classes and styles ```tsx
``` Dynamic `class` updates the existing element. `classList` diffs active tokens, including space-separated token groups. Dynamic style objects remove missing properties and update current ones. Static Tailwind class strings pass through unchanged. ## Events ```tsx ``` Event expressions are not reactive wrappers. Listeners attach once and are removed on unmount. The `Capture`, `Once`, and `Passive` suffixes map to standard event-listener options. ## Forms `bind:` creates a two-way connection to a signal: ```tsx const email = signal(""); const accepted = signal(false);

{email.value}

``` `bind:value` listens to `input`; other bound properties listen to `change`. The compiler passes the signal itself rather than wrapping it. For validation, error state, cancellation, server errors, resets, and accessible field relationships, use `createForm()` rather than assembling those mechanics from individual signals. See [Forms](forms.md). Clank's strict JSX declarations type native tags, element properties, event `currentTarget`, reactive values, ARIA/data attributes, bind/ref/use protocols, and hyphenated custom elements. ## Refs and directives ```tsx const input = signal(null); { const observer = new ResizeObserver(report); observer.observe(element); return () => observer.disconnect(); }} /> ``` `ref` accepts a callback or signal. `use` accepts one directive or an array. A directive may return an unmount cleanup. ## Lifecycle and context ```tsx const Theme = createContext("system"); function Shell() { provideContext(Theme, "dark"); onMount(() => { console.log("mounted"); return () => console.log("unmounted"); }); return ; } function Page() { const theme = useContext(Theme); return

Theme: {theme}

; } ``` Providers affect descendant components mounted from that component's output. Contexts always have a default value. Component scopes own nested effects, resources, directives, event listeners, and cleanup callbacks. ## Conditional control flow ```tsx }> Ready Failed ``` Only the selected dynamic region is mounted. Changing the condition disposes the previous region before mounting the next. ## Keyed lists ```tsx No tasks

}> {(task, index) => (
{index() + 1}. {task.title}
)}
``` `For` calls the row function once per new key. Retained object rows receive a stable reactive proxy, so immutable replacements update property bindings without recreating the row. The second argument is an index accessor because reordering can change it. Use `by="id"` for a record property or `by={(item) => item.id}` for a custom key. Duplicate keys throw instead of producing ambiguous DOM. See [Performance model](performance.md) for identity guarantees. ## Lazy components and promises `lazy()` accepts a dynamic component import. A Promise can also be rendered directly; its pending marker is replaced when it resolves. Rejection renders the error message unless the surrounding application supplies its own error UI. ## Raw HTML and hydration `dangerouslySetInnerHTML={{ __html }}` is deliberately explicit and bypasses child mounting. Sanitize untrusted HTML first. Ordinary URL attributes reject executable schemes and unsafe data URLs. Inline `on*` attributes and `iframe srcdoc` are rejected; use function event listeners and an explicitly reviewed raw-HTML path instead. `renderToString()` emits comment boundaries around dynamic values and keyed `For` regions. `hydrate(root, view)` walks that marker structure, attaches reactive effects and event handlers, and preserves matching elements, rows, and text nodes. It records `data-clank-hydration="attached"` on success. If the client tree differs structurally from the server tree, hydration warns, disposes the partial ownership scope, and remounts the root with `data-clank-hydration="remounted"`. Treat that fallback as a development signal: render deterministic initial data and seed live clients from `readState()` before hydration. Only structural `HydrationMismatch` failures trigger that fallback. Errors from components, directives, event bindings, or lifecycle callbacks are cleaned up and rethrown so application bugs remain visible instead of running the tree a second time. Clank also removes listeners, effects, and directive cleanups from any partially attached subtree before it remounts. Event handlers and `onMount` callbacks do not run during SSR. They attach or run during hydration. Use `serializeState()`/`renderDocument({ state })` rather than interpolating JSON into scripts; it escapes HTML- and script-significant characters. For a strict Content Security Policy, generate a fresh nonce per response and pass it to both the policy and document renderer: ```tsx const page = await renderDocument(, { nonce, state, scripts: ["/app.js"], }); ``` The nonce is applied to Clank's generated state and module scripts. Inline scripts supplied in `head` must receive the same `nonce` property. ## Compiler escape hatches `expression(() => value)` manually creates the same reactive boundary emitted by the compiler. `jsx()` and `h()` are public for tooling and generated code. Most application code should not need them. --- # Routing Canonical URL: https://docs.clank.run/docs/routing Raw source: https://docs.clank.run/raw/routing.md ## Define and start a router ```tsx const router = createRouter({ routes: [ { path: "/", component: Home, title: "Home" }, { path: "/users/:id", component: User, title: match => `User ${match.params.id}`, load: async ({ params, signal }) => { const response = await fetch(`/api/users/${params.id}`, { signal }); return response.json(); }, }, { path: "/docs/:page?", component: Docs }, { path: "*", component: NotFound }, ], loading: Loading, error: LoadError, }); const stop = router.start(); render(root, ); ``` Routes are checked in declaration order. Put a wildcard last. ## Patterns - `/users/:id` captures one required segment. - `/docs/:page?` captures an optional segment. - `/files/*` captures the remainder as `wildcard`. - `*` matches every path. Captured values are URI-decoded. `matchPath()` and `matchRoutes()` are exported for server and test use. ## Component props The matched component receives `{ route, params, query, data }`. Repeated query keys become arrays; a single value remains a string. ## Links and navigation ```tsx Open user await router.navigate("/users/42"); await router.navigate("/login", { replace: true, state: { from: "/private" } }); ``` Router links render ordinary anchors with `data-clank-link`. Modified clicks, downloads, explicit targets, external origins, and already-prevented events retain native browser behavior. ## Loaders and cancellation Navigation sets the route to `loading`, passes an `AbortSignal` to its loader, then commits `ready` data. The previous loader is aborted on a newer navigation, and revision checks reject stale results even if the loader ignores abort. ## Guards ```ts guard: ({ from, params }) => { if (!session.value) return `/login?next=/projects/${params.id}`; return true; } ``` A guard may return `true`, `false`, or a redirect URL, synchronously or asynchronously. `false` cancels navigation. A string recursively navigates with replacement. ## Base paths and non-browser resolution Set `base: "/app"` when hosted below an origin root. `matchRoutes()` and `resolve(url)` also work without a browser; relative URLs use a safe internal origin. --- # Forms Canonical URL: https://docs.clank.run/docs/forms Raw source: https://docs.clank.run/raw/forms.md Clank forms are headless: the framework owns state, validation, accessible control wiring, submission, and cancellation while the application owns its HTML and Tailwind classes. This keeps generated code readable. There is no component DSL to learn and no required visual style. ## Create a form ```tsx import { createForm, s } from "@clank.run/framework"; const signup = createForm({ id: "signup", initial: { name: "", email: "", plan: "starter" as "starter" | "team", accepted: false, }, schema: s.object({ name: s.string({ min: 2, max: 80 }), email: s.email({ max: 160 }), plan: s.enum(["starter", "team"]), accepted: s.literal(true), }), validateOn: "blur", onSubmit: async (values, { signal }) => { await saveAccount(values, { signal }); }, }); ``` The initial object is the inference root. Field names, values, `setValue`, validation output, and `onSubmit` values retain those types. The `id` is deterministic so SSR and hydration produce the same control, error, and description IDs. ## Render native controls ```tsx function SignupForm() { const name = signup.field("name"); const email = signup.field("email"); const plan = signup.field("plan"); const accepted = signup.field("accepted"); return (

{name.message.value}

{email.message.value}

); } ``` Field helpers return ordinary Clank/HTML props: - `input()` handles text, date, email, password, number, range, and other native inputs. - `textarea()` handles multiline text. - `select()` handles single or multiple selection. - `checkbox()` is available only on boolean fields in TypeScript. - `radio({ value })` creates one option for a field. - `error()` provides a stable ID, polite live region, and reactive `hidden` state. `aria-invalid` remains explicitly `"true"` or `"false"`. When errors exist, the control automatically references its error element with `aria-describedby`. ## State The form controller exposes: ```ts signup.values.value; signup.dirty.value; signup.valid.value; signup.pending.value; signup.submitted.value; signup.submitCount.value; signup.status.value; // idle | invalid | submitting | success | error signup.result.value; signup.error.value; signup.formErrors.value; ``` Each field exposes `value`, `errors`, `message`, `touched`, `dirty`, and `invalid`. Imperative updates remain explicit: ```ts signup.setValue("plan", "team"); signup.setValues({ name: "Ada", email: "ada@example.com" }); signup.reset(); signup.reset({ name: "Grace", email: "grace@example.com", plan: "team", accepted: true, }); ``` Reset values must contain exactly the original fields. Unknown field errors throw instead of disappearing silently. ## Validation Schema validation is synchronous and deterministic: ```ts signup.validate("manual"); ``` Validation issues are mapped by their first path segment. Nested issues remain attached to their top-level form field, which works well when nested editors are composed as separate form controllers. Cross-field validation is a plain function: ```ts const stay = createForm({ id: "stay", initial: { checkIn: "", checkOut: "" }, schema: s.object({ checkIn: s.date(), checkOut: s.date(), }), validate(values) { return values.checkOut <= values.checkIn ? { checkOut: "Check-out must be after check-in." } : undefined; }, }); ``` `validateOn` may be `submit`, `blur`, or `input`. Expensive remote checks belong in `onSubmit`; the server can return field errors through `setErrors`. ## Submission and server errors ```ts const invite = createForm({ id: "invite", initial: { email: "" }, schema: s.object({ email: s.email() }), onSubmit: async (values, { signal, setErrors }) => { const response = await fetch("/api/invitations", { method: "POST", signal, body: JSON.stringify(values), }); if (response.status === 409) { setErrors({ email: "That person is already invited." }); return; } if (!response.ok) throw new Error("Invitation failed."); }, }); ``` New submissions abort older submissions by default and ignore their stale results. Set `concurrency: "ignore"` when a second submit should do nothing while the first is pending. An invalid submit marks every field touched and focuses the first invalid named control unless `focusFirstError: false` is configured. `resetOnSuccess: true` restores initial values after a successful submit. A manual `reset(values)` establishes a new baseline. ## Form manifests for agents Every controller exposes a stable manifest: ```ts signup.manifest; ``` The protocol is `clank-form/1`. It includes the form ID, JSON Schema, field names, required state, and a suggested native control type. The manifest does not contain live field values. Rendered controls remain discoverable through native IDs, associated labels, semantic roles, and optional `agentId`/`agentAction` metadata. Password and file-input values are deliberately omitted from semantic inspection. ## Multi-step and nested forms Compose one controller per independently validated step: ```ts const dates = createForm({ /* … */ }); const room = signal("standard"); const guest = createForm({ /* … */ }); ``` This produces smaller contracts, clearer error ownership, and simpler generated code than a single controller with conditional nested paths. The booking example under `examples/booking` demonstrates the pattern. ## Security notes - Client validation improves UX; server actions must validate again. - Never render `form.error.value` directly when it may contain a sensitive server exception. Map it to a user-safe message. - Do not place secrets in initial values, labels, agent metadata, or form manifests. - File uploads require an explicit upload transport. `createAgentSurface().input()` refuses file inputs. - Form cancellation is cooperative. Pass the provided `AbortSignal` into Fetch or other cancellable work. --- # Headless UI behavior Canonical URL: https://docs.clank.run/docs/ui Raw source: https://docs.clank.run/raw/ui.md Clank provides small state machines for behavior that is deceptively difficult to implement correctly. They return ordinary HTML props and directives rather than styled components, so Tailwind and application markup remain fully under your control. ## Disclosure Use one disclosure for an accordion item, filter panel, navigation drawer, or expandable summary: ```tsx const filters = createDisclosure({ id: "product-filters", initialOpen: false, });
Filter controls
``` The trigger receives `aria-controls`, a reactive boolean `aria-expanded`, native button type, and disabled state. The panel receives a deterministic ID, label relationship, and reactive `hidden`. Methods are `show()`, `hide()`, and `toggle()`. ## Modal dialog ```tsx const invite = createDialog({ id: "invite-dialog" });

Invite teammate

Send a secure workspace invitation.

``` The dialog behavior includes: - `role="dialog"` and `aria-modal="true"`; - deterministic title and description relationships; - initial focus on the first usable control; - Tab and Shift+Tab focus wrapping; - Escape dismissal; - optional backdrop dismissal; - body scroll locking; - focus restoration to the opening control. Configure `closeOnEscape`, `closeOnBackdrop`, `restoreFocus`, and `lockScroll` when a workflow needs different behavior. The dialog does not impose a portal. Place its markup where it is structurally understandable and use CSS positioning when it should visually escape the page flow. ## Tabs ```tsx const settings = createTabs({ id: "settings", tabs: [ { value: "profile" }, { value: "billing" }, { value: "danger", disabled: true }, ] as const, });
``` Tabs provide roles, selection state, roving tab index, panel relationships, disabled state, and keyboard navigation: - horizontal: Left/Right; - vertical: Up/Down; - Home/End; - Enter/Space in manual activation mode. Tab values are inferred by TypeScript and must produce unique DOM-safe IDs. ## Pagination ```tsx const page = createPagination({ total: computed(() => filteredRows.value.length), pageSize: 20, siblingCount: 1, }); const visible = computed(() => filteredRows.value.slice(page.start.value - 1, page.end.value) ); ``` The controller exposes `page`, `pageSize`, `total`, `pageCount`, `start`, `end`, `canPrevious`, `canNext`, and a compact `pages` array containing page numbers and `"ellipsis"`. Methods are `setPage`, `setPageSize`, `previous`, `next`, and `dispose`. Changing totals automatically clamps the active page. ## Directives `clickOutside(handler)` listens for captured pointer activity outside an element: ```tsx
menu.hide())}>…
``` `autoFocus` focuses a connected element in the next microtask: ```tsx ``` Directives return cleanup functions and are disposed with their component. ## Accessibility rules Headless helpers make the relationships correct, but application markup still matters: - use visible labels for form controls; - preserve meaningful heading order; - do not make noninteractive elements clickable; - keep focus indicators visible; - use `agentLabel` when the visible name is ambiguous, not as a substitute for human text; - test keyboard operation and narrow viewports. Boolean ARIA states are emitted explicitly as `"true"` and `"false"` in both DOM rendering and SSR. --- # Tailwind CSS Canonical URL: https://docs.clank.run/docs/tailwind Raw source: https://docs.clank.run/raw/tailwind.md Clank preserves class strings exactly and keeps styling outside its reactive kernel. Tailwind scans ordinary TSX such as: ```tsx ``` ## Zero-install development The example uses Tailwind v4's browser build: ```html ``` Tailwind documents the Play CDN as a development-only option. It is ideal for this zero-install demo, but production should serve compiled CSS. See the official [Play CDN guide](https://tailwindcss.com/docs/installation/play-cdn). When using the browser build with a Content Security Policy, allow the exact CDN script origin and the development-time style injection it performs. The authenticated example uses a per-response script nonce and limits the exception to `style-src 'unsafe-inline'`. Compiled production CSS can remove both CDN access and that style exception. ## Production without project packages Use Tailwind's standalone CLI executable, then create: ```css /* src/styles.css */ @import "tailwindcss"; ``` Compile it to a static file with the standalone binary for your platform: ```sh ./tailwindcss -i ./src/styles.css -o ./public/styles.css --watch ``` Then link `/styles.css` from HTML. The official [Tailwind CLI guide](https://tailwindcss.com/docs/installation/tailwind-cli) notes that a standalone executable is available for users who do not want a Node package install. ## Static class discovery Keep complete class tokens in source: ```ts // Good: both complete tokens are discoverable. const tone = danger ? "bg-red-600" : "bg-emerald-600"; // Avoid: a static scanner cannot infer every interpolated result. const tone = `bg-${color}-600`; ``` Reactive `classList` keys are also static strings and are discoverable. If a class truly comes from external data, map the allowed values to complete class names or configure the Tailwind source/safelist mechanism. ## Why there is no Clank plugin Tailwind needs source files containing class strings and an HTML file containing its generated stylesheet. Clank supplies both without transforming the class attribute, so an adapter would add dependency and indirection without adding capability. --- # Performance model Canonical URL: https://docs.clank.run/docs/performance Raw source: https://docs.clank.run/raw/performance.md Clank uses compilation and fine-grained subscriptions instead of rerendering component trees. A component function runs once when mounted. Its TSX expressions become independent bindings that subscribe only to the signals they actually read. ## Update guarantees | State change | DOM work | | --- | --- | | Text expression changes | Mutate the existing `Text.data`; node identity is retained | | Attribute or property changes | Set that property on the existing element | | One style object changes | Diff its property names and update changed/current entries | | One `classList` object changes | Diff its active tokens; add/remove only changed tokens | | Conditional changes branch | Dispose and mount only the nodes inside its markers | | Keyed item field changes | Update bindings in that retained row | | Keyed list reorders | Move retained DOM ranges; do not recreate them | | Keyed list insertion/removal | Mount/dispose only the affected keys | There is no virtual DOM tree, component rerender, or full-list diff of child VNodes on a normal signal update. ## Compiler-created granularity ```tsx

{document.value.title}

{document.value.summary}

``` This creates three subscriptions: one for `class`, one for the heading text, and one for the paragraph text. Changing `summary` does not execute the component, recreate the article, touch its class, or update the heading. Event callbacks, `ref`, `use`, `key`, `bind:*`, and literal values are not wrapped in reactive effects. ## Keyed lists ```tsx No todos

}> {(todo) => ( )}
``` Keys must be unique within the list. A property key such as `by="id"` or `(item) => item.id` is recommended for immutable records. Clank gives every retained object row a stable proxy with lazily created property signals. Replacing `{ id: "a", done: false }` with `{ id: "a", done: true }` preserves the row and notifies `todo.done` bindings without invalidating bindings that only read `todo.id` or `todo.title`. Without `by`, object identity is the key. Primitive values use value plus index and are intended for simple display lists. ## Batching Signal writes are synchronous. Wrap related writes in `batch()` so each dependent effect executes once after the final write. `transaction()` provides the same coalescing plus rollback on failure. ## Measuring identity The renderer regression suite asserts identity, not merely final HTML. It stores references to list elements and text nodes, edits and reorders immutable records, and verifies that the same objects remain mounted. This prevents a visually correct remount from being mistaken for a fine-grained update. Run the performance invariants with: ```sh npm test -- tests/dom.test.mjs ``` --- # API reference Canonical URL: https://docs.clank.run/docs/api-reference Raw source: https://docs.clank.run/raw/api-reference.md This is the compact index of every public export. The focused guides contain behavioral details and examples. ## Core - `signal(value, options?)` → `ReactiveSignal`: mutable tracked value. - `ReactiveSignal`: `.value`, `.get()`, `.peek()`, `.set()`, `.update()`, `.subscribe()`, `.toJSON()`. - `isSignal(value)`: detects signals and computed values. - `computed(derive, options?)` → `Computed`: lazy cached derived value. - `effect(callback, options?)` → disposer: tracked synchronous side effect with cleanup. - `batch(callback)`: coalesces dependent effects. - `transaction(callback)`: batch with signal rollback on throw. - `untrack(callback)`: disables dependency capture in the callback. - `createRoot(callback)`: creates an ownership scope. - `onCleanup(callback)`: registers owned cleanup. - `getOwner()` / `runWithOwner(owner, callback)`: capture and restore ownership for advanced integrations. - `store(object)`: creates a lazy deep reactive proxy. - `isStore(value)`, `toRaw(value)`, `snapshot(value)`: store inspection and serialization. - `resource(loader, options?)`: async state with abort and stale-result protection. - `consumeStream(iterable, initial, reduce?)`: folds an async iterable into a signal. - `SIGNAL`, `STORE`: global protocol symbols for integrations. ## DOM - TSX: preferred component syntax; dynamic braces become fine-grained bindings automatically. - `h(type, props?, ...children)` / `createElement`: lower-level VNode construction. - `jsx`, `jsxs`, `jsxDEV`: compiler runtime entry points. - `expression(read)`, `isExpression(value)`: compiler/runtime reactive boundary. - `Fragment`: groups children without an element. - `render(root, view)` → disposer: mounts an application. - `hydrate(root, view)` → disposer: attaches to marker-compatible SSR DOM; warns and remounts on a structural mismatch. - `isVNode(value)`: VNode detection. - `onMount(callback)`: post-mount lifecycle with optional cleanup. - `createContext(defaultValue)`, `provideContext(context, value)`, `useContext(context)`. - `Show`, `Match`, `Switch`: reactive conditional control flow. - `For`: O(n) keyed reconciliation with row identity preservation; use `by="id"` or a key function. - `lazy(loader)`: promise-backed component. - Types: `Renderable`, `Component`, `VNode`, `ReactiveExpression`, `KeyedBlock`, `ElementType`, `ClankContext`. Element protocols include `onClick`/`on:click`, `bind:value`, `classList`, object `style`, callback/signal `ref`, directive `use`, `dangerouslySetInnerHTML`, and the `agent*` properties. ## Forms - `createForm(options)` → typed headless form controller. - Controller state: `values`, `dirty`, `valid`, `pending`, `submitted`, `submitCount`, `status`, `result`, `error`, `formErrors`. - Controller methods: `field`, `setValue`, `setValues`, `setErrors`, `validate`, `submit`, `reset`, `focusFirstError`, `props`. - Field state: `value`, `errors`, `touched`, `dirty`, `invalid`, `message`. - Field helpers: `input`, `textarea`, `select`, `checkbox`, `radio`, `error`. - `manifest`: `clank-form/1` schema and field contract without live values. - Types: `FormController`, `FormField`, `FormManifest`, `FormErrorMap`, `FormStatus`, `CreateFormOptions`. ## Headless UI - `createDisclosure(options)`: expandable state with trigger/panel ARIA props. - `createDialog(options)`: modal state, focus trap, Escape/backdrop handling, scroll lock, and focus restoration. - `createTabs(options)`: inferred tab values, panel relationships, roving tab index, and keyboard navigation. - `createPagination(options)`: page clamping, ranges, controls, and compact page items. - `clickOutside(handler)`: outside-pointer directive. - `autoFocus(element)`: mount-time focus directive. - Types: `DisclosureController`, `DialogController`, `TabsController`, `PaginationController`. ## Compiler - `clank build [input] [output]`: compile `.ts`/`.tsx` and copy static files once. - `clank watch [input] [output]`: rebuild after source changes. - `--jsx-import-source=specifier`: choose the generated runtime module. - `compile(source, options?)`: programmatic TypeScript/TSX compilation. - `transformTSX(source, options?)`: programmatic TSX-only lowering. ## Deployment artifacts - `readDeploymentConfig(root, filename?)`: read and normalize `clank.deploy.json`. - `parseDeploymentConfig(value)`: validate a config already in memory. - `createDeploymentBundle(root, config, options?)`: deterministic gzip artifact with checked files and provenance. - `decodeDeploymentBundle(bytes, limits?)`: bounded protocol, path, size, base64, and SHA-256 verification. - `extractDeploymentBundle(bundle, directory)`: exclusive extraction into a release root. - `deploymentDigest(bytes)`: SHA-256 artifact digest. - Types: `DeploymentConfig`, `DeploymentBundle`, `DeploymentFile`, `BundleLimits`. ## Migrations - `loadMigrations(directory, options?)`: ordered SQL files and SHA-256 checksums. - `planMigrations(path, migrations)`: applied/pending state with immutable-history verification. - `applyMigrations(options)`: apply pending SQL in one immediate transaction. - `assertSafeMigrationSql(sql, id?)`: reject cross-database and transaction controls. - `backupSQLite(source, destination)`: consistent built-in SQLite backup. - `restoreSQLiteBackup(source, destination)`: replace a stopped database and clear WAL sidecars. - Types: `Migration`, `MigrationRecord`, `MigrationPlan`, `ApplyMigrationsOptions`. ## Deployment platform - `openPlatform(options)`: browser dashboard, workspace people/invitation administration and activity, device authorization, tokens, projects, transactionally enforced limits, ingress metrics, DNS/domain lifecycle, TLS eligibility, encrypted secrets, role-filtered audit, release transaction, logs, rollback, and supervision. - `PlatformRuntime`: Fetch `.handle`, `.publicUrl`, `.dataDirectory`, async `.close()`. - Runners: dependency-free process runner or constrained Docker runner. - Types: `ClankPlatformOptions`, `PlatformLimits`, `PlatformRunnerOptions`, `ProcessRunnerOptions`, `DockerRunnerOptions`. ## Managed data plane - `createManagedIngress(options)`: exact-host reverse proxy with fixed upstream origins, bounded streaming request bodies, hop-header stripping, safe retries, circuits, health, and request observation. - `inspectDomainRouting(hostname, target, resolver?)`: compare live CNAME/A/AAAA results to a configured edge target. - `createDomainManager(options)`: project-bound random TXT ownership challenges. - `createMemoryDomainStore()`: in-memory domain challenge store for local use and tests. - Types: `IngressRoute`, `IngressRequestMetric`, `ManagedIngress`, `DomainChallenge`, `DomainDnsResolver`, `DomainRoutingReport`. ## AI - `defineApp(input)`: normalize and freeze a `clank-app/1` application blueprint. - `parseAppBlueprint(source, filename?)`: statically parse a JSON or constrained TypeScript data module without executing it. - `generateAppFiles(blueprint, options?)`: return deterministic full-stack application files. - `createAppPlan(blueprint, options?)`: checksum every generated file and return a `clank-plan/1` review artifact. - `explainApp(blueprint)`: summarize identity, data, routes, services, deployment requirements, and warnings. - `s`: runtime schema builders and JSON Schema generation. Includes string, email, URL, date, date-time, number, boolean, literal, enum, array, record, object, optional, nullable, default, refinement, union, and numeric/boolean coercion. - `ValidationError`: aggregate issues with paths. - `defineAction(definition)` → callable `Action` with `.manifest` and `.definition`. - `ActionError`: explicit code/status/details error. - `createAgentBridge(actions, options?)`: registry, discovery, bounded/origin-aware invocation, confirmation enforcement, and Fetch handler. - `actionRunner(action)`: reactive pending/data/error execution state. - `defineView(definition)`: component with machine-readable `viewManifest`. - `inspectAgentSurface(root)`: compact semantic UI tree with native labels/roles and form state; omits password/file values. - `createAgentSurface(root)`: inspect, activate, and input operations through explicit agent IDs or native element IDs. - Types: `Schema`, `Action`, `ActionContext`, `AgentBridge`, `ActionRunner`, `AgentNode`, `AgentSurface`. ## Router - `createRouter(options)` → router with `state`, `current`, `navigate`, `resolve`, `start`, `View`, and `Link`. - `matchPath(pattern, pathname)`: parameter matcher. - `matchRoutes(routes, URL, base?)`: route selection and URL decoding. - `redirect(to, status?)`: Fetch redirect response. - Types: `RouteDefinition`, `RouteMatch`, `RouteState`, `RouteLoadContext`, `RouteGuardContext`, `Router`. ## Server - `createApp(options?)` → Fetch request router with redacted errors and an error hook. - `.use`, `.route`, `.get`, `.post`, `.put`, `.patch`, `.delete`, `.handle`. - `json(value, init?)`, `text(value, init?)`, `html(value, init?)`. - `cors(options?)`, `securityHeaders(options?)`, `logger(write?)`: built-in middleware. - Types: `RequestContext`, `RequestHandler`, `Middleware`, `RequestApp`. ## Authentication - `defineAuth(options?)`: default or custom-profile auth contract. - `openAuth(definition, database, options?)`: low-level SQLite auth runtime; normally opened automatically by `openBackend`. - `authState(requestAuth)`: safe serializable SSR subset. - `createAuthClient(options?)`: reactive auth-only client. - `createClient(options?)`: combined typed API, auth, CSRF mutation, seeding, and live client. - `AuthGate`: reactive signed-in boundary with default auth screen. - `AuthForm`: default accessible email/password registration/login UI. - `AuthRuntime`: `.resolve`, `.handle`, `.middleware`, `.setRole`, `.disableUser`, `.revokeUserSessions`, `.verifyCsrf`, current-session refresh and subscription/status, `.close`. - `AuthClient`: `.user`, `.session`, `.authenticated`, `.loading`, `.error`, `.reload`, `.register`, `.login`, `.logout`, `.logoutAll`, `.changePassword`. - `AuthRequest`: `.user`, `.session`, `.csrfToken`, `.requireUser()`, `.requireRole()`. - `AuthError`: explicit safe auth code, status, and optional retry delay. - Types: `AuthDefinition`, `AuthDefinitionOptions`, `AuthUser`, `AuthSession`, `AuthState`, `AuthRegisterInput`, `AuthLoginInput`, `AuthUserId`, `DefaultAuthProfile`. ## Full-stack backend - `defineTable(fields)`: validated document table definition; `.index(name, fields)` declares SQLite JSON expression indexes; `.owned()` scopes documents to the authenticated user. - `defineDatabase(tables)`: preserves table names and field schemas as the inference root. - `DocumentFor`: inferred fields plus branded `_id`, `_creationTime`, `_version`, and `_ownerId` for owned tables. - `Id` / `DocumentId
` / `s.id(table)`: nominal table-specific IDs. - `openSQLite(schema, options?)`: opens Node's built-in synchronous SQLite engine. - `createSQLiteDatabase(schema, connection, options?)`: wraps a compatible connection. - `SQLiteDatabase`: `.read`, `.tracked`, `.transaction`, `.subscribe`, `.version`, `.close`. - Read table: `.get`, `.query`, `.collect`. - Write table: `.insert`, `.patch`, `.replace`, `.delete`. - `DocumentWriteOptions`: `{ ifVersion }` optimistic concurrency for patch, replace, and delete. - `DatabaseConflictError`: stale-write error exposed as HTTP `409 VERSION_CONFLICT`. - Query builder: `.where`, `.orderBy`, `.limit`, `.collect`, `.first`. - `defineBackend({ schema, auth? }).functions(builders)`: inference-first nested function tree. Auth backends make `query`/`mutation` required and expose explicit `publicQuery`/`publicMutation`. - `createApi()`: zero-codegen typed function-reference proxy. - `openBackend(definition, options?)`: consistent query cache, owner-scoped/persisted dependency invalidation, atomic mutations, manifest, bounded RPC, and SSE handler. - `BackendRuntime`: `.auth`, `.caller(request)`, `.query`, `.mutation`, `.subscribe`, `.handle`, `.version`, `.close`. - `createSyncClient(options?)`: typed browser/Fetch client with `.query`, `.mutate`, `.live`, and `.seed`. - `createClient(options?)`: authenticated combined client with `.api` and `.auth`. - `BackendClientError`: safe RPC error with code/status. - `LiveQuery`: reactive `.data`, `.loading`, `.error`, `.version`, plus `.dispose()`. - `functionPath`, `functionKey`, `stableStringify`: reference and canonical argument helpers. - Types: `DatabaseSchema`, `TableDefinition`, `DocumentWriteOptions`, `DatabaseChange`, `SQLiteOptions`, `QueryBuilder`, `ReadDatabase`, `WriteDatabase`, `BackendFunction`, `BackendDefinition`, `FunctionReference`, `ApiOf`, `BackendRuntime`. ## SSR - `renderToString(view, options?)`: escaped async HTML rendering with hydration markers by default. - `renderDocument(view, options?)`: complete document template with title, head content, stylesheets, state, module scripts, and optional CSP nonce. - `serializeState(value)`: JSON serialization safe for an HTML script element. - `readState(id?, root?)`: reads a serialized application state script. - Types: `RenderStringOptions`, `RenderDocumentOptions`. ## Node - `serve(app, options?)`: bounded Fetch-standard Node HTTP server with streaming, timeouts, Host allowlists, proxy controls, and redacted errors. - `staticFiles(root, options?)`: traversal/symlink-aware static GET/HEAD handler with dotfile policy. - Types: `FetchApplication`, `ServeOptions`, `ServerHandle`, `StaticFilesOptions`. --- # Full-stack SSR, SQLite, and live sync Canonical URL: https://docs.clank.run/docs/full-stack Raw source: https://docs.clank.run/raw/full-stack.md Clank's full-stack layer follows one rule: write the runtime contract once and let TypeScript infer the rest. There is no generated client, ORM model, RPC interface, or duplicate DTO type to maintain. The implementation uses only platform modules: Node's built-in SQLite and HTTP APIs, Fetch `Request`/`Response`, Web `ReadableStream`, and browser `EventSource`. Tailwind remains a CSS choice and does not become a framework dependency. ## Define data once ```ts import { defineDatabase, defineTable, s, type DocumentFor } from "@clank.run/framework"; export const schema = defineDatabase({ todos: defineTable({ title: s.string({ min: 1, max: 160 }), done: s.boolean(), note: s.optional(s.string()), }).index("by_done", ["done"]), }); export type Todo = DocumentFor; ``` `Todo` is inferred as the declared fields plus `_id`, `_creationTime`, and `_version`. An optional validator becomes an optional property. `s.id("todos")` produces a branded string compatible only with the `todos` table, which catches cross-table ID mistakes without changing the JSON wire format. `defineTable().index(name, fields)` creates a SQLite expression index over the stored JSON fields. Table and field names are checked at startup; all existing rows are revalidated when a database opens. Add `.owned()` when every document belongs to one authenticated user: ```ts const schema = defineDatabase({ todos: defineTable({ title: s.string(), done: s.boolean(), }).owned(), }); ``` Owned documents also include `_ownerId`. Auth-scoped database views add the owner predicate automatically to reads and writes. ## Define server functions ```ts import { defineBackend, s } from "@clank.run/framework"; import { schema } from "./schema.ts"; export const backend = defineBackend({ schema }).functions(({ query, mutation }) => ({ todos: { list: query({ args: { done: s.optional(s.boolean()) }, handler: ({ db }, { done }) => { const rows = db.table("todos").query().orderBy("_creationTime"); return done === undefined ? rows.collect() : rows.where("done", done).collect(); }, }), add: mutation({ args: { title: s.string({ min: 1, max: 160 }) }, handler: ({ db }, { title }) => db.table("todos").insert({ title, done: false }), }), toggle: mutation({ args: { id: s.id("todos"), version: s.number({ integer: true, min: 1 }) }, handler: ({ db }, { id, version }) => { const todo = db.table("todos").get(id); return todo ? db.table("todos").patch(id, { done: !todo.done }, { ifVersion: version }) : null; }, }), }, })); ``` The builders infer handler arguments from `args` and infer results from the handler. Add `returns: someSchema` when the output also needs runtime validation and JSON Schema publication; a separate TypeScript result annotation is not required. Backend queries and mutations are deliberately synchronous and deterministic: - A query receives a read-only database view and records its dependencies. - A mutation receives a writable view inside one `BEGIN IMMEDIATE` transaction. - Insert, patch, replace, and delete are validated before commit. - `ifVersion` rejects stale patch, replace, and delete operations with `DatabaseConflictError`. - A thrown error rolls back every write. - Invalid, non-JSON, or oversized mutation output also rolls back every write. - The global live revision increments inside the same transaction and persists across restarts. - Notifications are emitted only after a successful commit. External network calls and other asynchronous side effects belong in Clank `Action`s. Keeping database functions synchronous prevents a transaction from remaining open across arbitrary awaits and makes the published snapshot unambiguous. ## Query documents ```ts const table = db.table("todos"); const todo = table.get(id); const open = table.query() .where("done", false) .orderBy("_creationTime", "desc") .limit(20) .collect(); const first = table.query().where("title", "eq", "Ship it").first(); ``` Supported comparisons are `eq`, `neq`, `lt`, `lte`, `gt`, and `gte`. `_id`, `_creationTime`, `_version`, and declared fields are queryable. Query values must be SQLite scalar values: string, number, bigint, boolean, or null. `get(id)` tracks that exact document. A builder query or `collect()` tracks the table. The latter is intentionally conservative: any committed write to that table reruns the query, preserving correctness even when a predicate's membership changes. ## Open the backend ```ts import { createApi, openBackend } from "@clank.run/framework"; import { backend } from "./backend.ts"; const api = createApi(); const runtime = await openBackend(backend, { path: "./data.sqlite", wal: true, busyTimeout: 5_000, }); const initial = runtime.query(api.todos.list); const id = runtime.mutation(api.todos.add, { title: "Strongly typed" }).value; runtime.mutation(api.todos.toggle, { id }); ``` `createApi()` is a type-only proxy: property access creates lightweight references such as `todos.list`. It performs no code generation, file watching, or network discovery. TypeScript knows whether each reference is a query or mutation, whether arguments are optional, and its exact result. `openBackend()` defaults to an in-memory database. A file path enables persistent storage; WAL, `synchronous=FULL`, startup integrity checks, private file permissions, cross-process change polling, and a five-second busy timeout are enabled by default. Call `runtime.close()` during shutdown. The lower-level `openSQLite(schema, options)` and `createSQLiteDatabase(schema, compatibleConnection)` APIs are available for direct storage integrations. ## Mount RPC and live streams `runtime.handle(request)` is a complete Fetch-standard backend endpoint. Mount it after application and asset routes: ```ts const app = createApp() .get("/", renderPage) .route("*", "*", ({ request }) => runtime.handle(request)); ``` The default protocol endpoints are: | Endpoint | Purpose | | --- | --- | | `GET /__clank/manifest` | Function names, kinds, argument schemas, and optional result schemas | | `POST /__clank/query/{path}` | Validated one-shot query | | `POST /__clank/mutation/{path}` | Validated atomic mutation | | `GET /__clank/live/{path}?args=...` | Server-sent query snapshots and heartbeats | Change the prefix with `openBackend(backend, { prefix: "api" })`. Requests reject cross-site origins by default, JSON bodies and live arguments are bounded, cache size and live connections are capped, and internal failures are redacted. For private applications, pass `auth: defineAuth()` to `defineBackend`. Clank then mounts `/__clank/auth`, makes normal queries/mutations auth-required, verifies CSRF on mutations, partitions query caches by session, scopes `.owned()` tables, and revalidates live sessions. See [Authentication](auth.md). The live transport uses standard SSE framing, sends the persisted revision as the event ID, disables buffering, bounds payloads, and emits a configurable heartbeat. Slow consumers are disconnected and EventSource reconnects with a complete current snapshot. ## Use the inferred browser client ```tsx import { createApi, createSyncClient } from "@clank.run/framework"; import type { backend } from "./backend.ts"; const api = createApi(); const client = createSyncClient(); const todos = client.live(api.todos.list); await client.mutate(api.todos.add, { title: "Streams everywhere" }); console.log(todos.data.value, todos.loading.value, todos.error.value); // When the component or application scope ends: todos.dispose(); ``` `live()` exposes four signals: `data`, `loading`, `error`, and `version`. Every committed write is compared with cached query dependencies. Only affected queries rerun; all subscribers to the same function and canonicalized arguments receive the resulting snapshot. `version` is the internal persisted database synchronization cursor. A value such as `36` means 36 change-producing transactions have committed; it is not a record count or connection count. See [Database revisions and correctness](database.md). `createSyncClient({ url, fetch, eventSource })` accepts a base URL and injectable platform implementations for non-browser runtimes and tests. For an authenticated backend, use the combined client: ```ts const client = createClient(); const todos = client.live(client.api.todos.list); await client.auth.login({ email, password }); await client.mutate(client.api.todos.add, { title: "Private and live" }); ``` This client keeps session credentials in `HttpOnly` cookies and adds the in-memory CSRF token to mutations. ## SSR and cache seeding Use one shared component for the server and browser. On the server: ```tsx const initial = runtime.query(api.todos.list); const page = await renderDocument(, { title: "Todos", state: { todos: initial.value, version: initial.version }, scripts: ["/app.js"], }); ``` In the browser, seed the exact live query before it opens and hydrate the same view: ```tsx const initial = readState<{ todos: Todo[]; version: number }>()!; const client = createSyncClient(); client.seed(api.todos.list, {}, initial.todos, initial.version); const todos = client.live(api.todos.list); hydrate(document.querySelector("#app")!, ( )); ``` Seeding prevents a blank loading render and ensures the first client tree matches the server tree. The EventSource still opens immediately and replaces the seed with the authoritative current snapshot. `renderToString(view)` escapes text and attributes and awaits promised renderables. `renderDocument(view, options)` creates the doctype, metadata, root, optional stylesheets, serialized state, and module scripts. Dynamic expressions and keyed lists receive hydration markers by default. `serializeState()` escapes `<`, `>`, `&`, and Unicode line separators so serialized data cannot close its script element. Pass a fresh `nonce` to apply a CSP nonce to the generated state and module-script tags. Hydration attaches bindings, listeners, refs, directives, lifecycle callbacks, and keyed reconciliation to the existing DOM. A mismatch warns and remounts safely. Inspect `root.dataset.clankHydration` for `attached` or `remounted` during diagnostics. When browser code imports the package name, the document import map must use that exact specifier: ```tsx