# 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
}>
ReadyFailed
```
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 (
);
}
```
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
```
An import-map key such as `clank` does not resolve `import ... from "@clank.run/framework"`; if it is mismatched, the browser never loads the client entry and hydration cannot begin.
## Run on Node
```ts
import { serve, staticFiles } from "@clank.run/framework/node";
const assets = staticFiles("./public", { cacheControl: "public, max-age=3600" });
const server = await serve(app, { hostname: "127.0.0.1", port: 3000 });
console.log(server.url);
// Later:
await server.close();
```
The adapter translates Node's built-in HTTP request and streaming response objects to Fetch. It supports streaming SSE responses, multiple `Set-Cookie` values, abort propagation, proxy-aware protocol handling, port `0`, and an error hook. `staticFiles()` handles GET/HEAD, MIME types, index files, cache control, URL decoding, and traversal rejection.
## Tailwind
Clank preserves ordinary `class`, reactive `class`, `classList`, style objects, and CSS variables in both SSR and client rendering. Use a compiled Tailwind stylesheet in production. For a zero-install prototype, the full-stack example loads Tailwind's browser build in the document head; that network script is application content, not a Clank runtime dependency.
## Complete example
The working implementation is split by responsibility:
- [`examples/fullstack/backend.ts`](../examples/fullstack/backend.ts): inferred schema and function tree.
- [`examples/fullstack/view.tsx`](../examples/fullstack/view.tsx): shared Tailwind component.
- [`examples/fullstack/server.tsx`](../examples/fullstack/server.tsx): SQLite runtime, SSR template, routes, and Node server.
- [`examples/fullstack/app.tsx`](../examples/fullstack/app.tsx): state read, live-query seed, hydration, and typed mutations.
Run it with `npm run dev:fullstack`, then open `http://127.0.0.1:4180` in two tabs. Changes committed in one tab stream to the other while keyed todo rows retain DOM identity.
The auth-first version is under [`examples/auth-todo`](../examples/auth-todo):
- [`backend.ts`](../examples/auth-todo/backend.ts): auth definition, owned table, required functions.
- [`server.tsx`](../examples/auth-todo/server.tsx): request auth, private SSR, CSP nonce, secure headers, Tailscale-ready proxy settings.
- [`app.tsx`](../examples/auth-todo/app.tsx): one combined auth/RPC/live client and cleanup.
- [`view.tsx`](../examples/auth-todo/view.tsx): shared Tailwind and agent-semantic UI.
Run it with `npm run dev:auth`.
---
# Database revisions and correctness
Canonical URL: https://docs.clank.run/docs/database
Raw source: https://docs.clank.run/raw/database.md
Clank uses Node's built-in SQLite as a transactional document store. Application fields live as validated JSON, while identity, ownership, creation time, and document version live in dedicated columns.
There are two different revision concepts:
| Value | Scope | Meaning |
| --- | --- | --- |
| `runtime.version` / live `version` | Whole database | The latest committed change transaction observed by this runtime |
| `document._version` | One document | The number of content versions committed for that document |
If a page shows `revision 36`, it means the database has committed 36 change-producing transactions since its revision counter began. It does not mean there are 36 records, 36 users, or 36 browser connections. The number is an internal synchronization cursor, not an authentication state or a user-facing progress metric.
New interfaces should normally display `synced` or `reconnecting` and keep the numeric revision in diagnostics.
## Physical layout
For a declared `todos` table, Clank creates `clank_todos`:
```sql
CREATE TABLE clank_todos (
_id TEXT PRIMARY KEY,
_owner_id TEXT,
_creation_time INTEGER NOT NULL,
_version INTEGER NOT NULL,
_data TEXT NOT NULL CHECK (json_valid(_data))
);
```
`_data` is canonical JSON derived from the table schema. Owned tables store `_owner_id` outside JSON so an application patch cannot change ownership.
Framework state uses:
- `clank_meta`: the persisted global revision;
- `clank_changes`: the bounded cross-process change journal;
- `clank_auth_users` and `clank_auth_sessions`: built-in auth;
- `clank_migrations`: immutable SQL migration history.
The `clank_` and legacy `proact_` SQL namespaces are reserved. Safe application migrations cannot modify them.
## Commit sequence
Every mutation uses one synchronous `BEGIN IMMEDIATE` transaction:
```mermaid
flowchart LR
A["Validate arguments"] --> B["BEGIN IMMEDIATE"]
B --> C["Run synchronous handler"]
C --> D["Validate writes and output"]
D --> E["Increment document versions"]
E --> F["Increment global revision once"]
F --> G["Write change-journal records"]
G --> H["COMMIT"]
H --> I["Invalidate affected queries"]
I --> J["Publish current snapshots"]
```
The application writes, global revision, and change records commit together. Any handler error, schema failure, stale-version conflict, invalid output, non-JSON output, or oversized output rolls back all of them.
Observers run after `COMMIT`. An observer exception is reported through `onError` and cannot turn a successful commit into a failed mutation response.
A transaction that changes several documents advances the global revision once. A transaction that makes no logical change does not advance it.
## Document versions and lost-update protection
Inserted documents start at `_version: 1`. A content-changing patch or replacement increments that document only. Deleting a document removes it. A no-op patch or replacement returns the existing immutable document without changing either revision.
Pass the version the user actually saw:
```ts
const saved = db.table("todos").patch(
todo._id,
{ title: nextTitle },
{ ifVersion: todo._version },
);
```
If another browser changed or deleted the document first, Clank throws `DatabaseConflictError`. RPC converts it to HTTP `409 VERSION_CONFLICT` with the expected and actual versions. The stale mutation commits nothing.
This is optimistic concurrency control. It avoids holding a lock while a person edits and prevents last-write-wins data loss.
Singleton records, such as one profile per user, should accept `version: number | null`. `null` means “I observed no record”; the mutation checks and inserts inside one transaction, so simultaneous creates cannot silently overwrite one another.
## Consistent reads
Queries run inside a short deferred SQLite read transaction. The global revision is read in the same snapshot as the application rows, so a query cannot combine rows from different commits.
Returned documents, cached query outputs, and change metadata are immutable at runtime. One subscriber cannot accidentally alter the snapshot delivered to another subscriber.
Before a cached one-shot query is used, the runtime synchronizes its persisted revision cursor. Correctness therefore does not depend on waiting for the background poll.
## Dependency and ownership invalidation
Clank records what each query reads:
- `get(id)` depends on one table/document pair;
- `query()` and `collect()` depend on the table;
- owned-table dependencies also include the authenticated owner.
Each committed journal record contains a table, document ID, and optional owner ID. Only intersecting cache entries become dirty. Bob's private todo mutation does not rerun or republish Alice's todo query.
The numeric global revision still advances for every committed transaction. It is an operational cursor, not a per-user counter, which is another reason not to present it as product data.
## Multiple server processes
File-backed runtimes poll `clank_changes` every 100 ms by default. Local commits publish immediately. If several commits accumulated, a runtime combines their affected records and publishes one snapshot at the newest revision because SQLite can only read the current database state.
The journal retains 10,000 revisions by default. If a process was offline long enough to miss retained history, Clank performs a conservative full cache invalidation. Authenticated live streams are closed and reconnect with freshly resolved authorization.
Browser `EventSource` reconnects receive a complete current snapshot. The client ignores payloads older than the snapshot it already holds.
This supports several processes sharing one SQLite file on one host. It is not a distributed multi-host database protocol. Network filesystems and independently replicated SQLite files are outside the supported consistency model.
## Auth revisions
Auth user and session writes use the same transaction/revision journal. Role changes, disabling a user, password changes, logout, and session revocation invalidate the affected identity.
Across processes, affected live streams close as soon as the other runtime observes the journal record. Long-lived server callers refresh their session record before each operation, so a role downgrade is not left cached.
## Durability and file safety
File databases default to:
- WAL journal mode;
- `synchronous = FULL`;
- foreign keys enabled;
- extension loading disabled;
- `trusted_schema = OFF`;
- recursive triggers disabled;
- secure deletion enabled;
- a five-second busy timeout;
- startup `quick_check` and foreign-key validation.
Database, WAL, SHM, backup, and restored files are restricted to mode `0600` where the operating system supports POSIX modes. Final database paths cannot be symbolic links. Corrupt databases and semantically inconsistent revision journals stop startup instead of being used.
Set `durability: "normal"` only after accepting weaker power-loss durability. Set `integrityCheck: "full"` for SQLite's more expensive full startup check, or `false` only when another verified operational process owns integrity checking.
## Migrations, backup, and restore
Ordered SQL migrations are checksummed and immutable. All pending files and ledger rows run inside one `BEGIN IMMEDIATE`. Safe mode rejects database attachment, extension loading, PRAGMAs, transaction control, and every `clank_` or `proact_` table reference.
Backup and restore:
1. reject symbolic-link and non-file sources;
2. verify SQLite integrity and foreign keys;
3. write a private temporary file;
4. verify the completed copy;
5. atomically replace the destination;
6. remove stale WAL/SHM sidecars.
The deployment platform stops the application before migration or restore. A failed migration, startup, or health check restores the verified pre-release snapshot.
## Important options
```ts
const runtime = await openBackend(backend, {
path: "./data/app.sqlite",
durability: "full",
integrityCheck: "quick",
busyTimeout: 5_000,
changePollIntervalMs: 100,
changeRetentionRevisions: 10_000,
maxRequestBytes: 64 * 1024,
maxResponseBytes: 4 * 1024 * 1024,
maxLivePayloadBytes: 4 * 1024 * 1024,
onError(error) {
logger.error(error);
},
});
```
All numeric resource limits must be positive integers. Slow SSE consumers are disconnected instead of being allowed to build an unbounded queue; EventSource reconnect then restores the current snapshot.
## Guarantees and boundaries
Clank guarantees atomic local commits, consistent SQLite snapshots, optimistic conflict detection when `ifVersion` is used, owner-scoped invalidation, and persisted same-host process synchronization.
Application code must still:
- pass `_version` for user edits that could race;
- keep network calls and other asynchronous side effects outside mutations;
- authorize non-owned/public data explicitly;
- keep database files outside public static roots;
- run scheduled off-host backups;
- use an external database when requiring multi-host writes or continuous zero-downtime schema changes.
---
# SQLite migrations
Canonical URL: https://docs.clank.run/docs/migrations
Raw source: https://docs.clank.run/raw/migrations.md
Framework document tables and indexes can still be created automatically. Deployment migrations cover relational support tables, columns, constraints, indexes, and SQL data changes.
## Files and ledger
```text
migrations/
0001_create_accounts.sql
0002_add_account_status.sql
```
Names match `<4-12 digits>_.sql`; IDs strictly increase.
Clank records:
```sql
CREATE TABLE clank_migrations (
id TEXT PRIMARY KEY,
name TEXT NOT NULL,
checksum TEXT NOT NULL,
applied_at INTEGER NOT NULL
);
```
The checksum covers exact SQL bytes. Editing, renaming, or removing applied history stops deployment. Fix production with a new migration.
## Transactions and safety
All pending migrations run in one `BEGIN IMMEDIATE`. Either every migration and ledger row commits, or none does.
Defaults reject:
- `ATTACH`, `DETACH`, and `VACUUM`;
- `load_extension`;
- all `PRAGMA` statements;
- top-level `BEGIN`, `COMMIT`, `ROLLBACK`, `SAVEPOINT`, and `RELEASE`.
- references to reserved `clank_` and legacy `proact_` SQL tables.
Extension loading is disabled, foreign keys and `trusted_schema=OFF` are enforced, durability is `FULL`, and integrity plus foreign-key checks run before and after migration.
`allowUnsafeMigrations: true` is only a request. The platform operator must also set `CLANK_ALLOW_UNSAFE_MIGRATIONS=1`; otherwise deployment is rejected. Enabling it lets migration SQL execute with control-plane filesystem authority and is inappropriate for untrusted deployers.
## Backup and failure
Before migration, Clank stops the active app and uses Node's SQLite backup API. Backup and restore reject symbolic links, verify source and destination integrity, keep files private, and replace through a verified temporary file. On migration, startup, or health failure Clank stops the candidate, restores the snapshot, and restarts the prior release.
Same-disk snapshots do not protect against disk loss. Export encrypted backups off-host and test restoration.
## Expand and contract
For code-only rollback:
1. Add compatible nullable structures.
2. Deploy code that reads old/new and writes new.
3. Backfill.
4. Depend on the new form.
5. Remove the old form after the rollback window.
Avoid dropping a required column in the same release that first stops using it.
## Data restore
Snapshot restore discards newer writes. It is available only to the immediately previous release and requires an exact project confirmation.
## Local checks
```sh
clank migrate plan
clank migrate apply
```
Large online backfills should be application jobs rather than one long deployment transaction. JavaScript migration files are intentionally unsupported. External PostgreSQL uses the structured HTTPS driver and immutable transactional ledger described in [Managed ingress and external data](data-plane.md).
---
# Authentication
Canonical URL: https://docs.clank.run/docs/auth
Raw source: https://docs.clank.run/raw/auth.md
Clank auth is built into the same zero-dependency SQLite, Fetch, SSR, and live-query layer as the rest of the framework. An authenticated application needs one auth definition, owned tables for private data, and the auth-first browser client.
## Minimal setup
```ts
import {
defineAuth,
defineBackend,
defineDatabase,
defineTable,
s,
} from "@clank.run/framework";
export const auth = defineAuth();
export const schema = defineDatabase({
todos: defineTable({
title: s.string({ min: 1, max: 160 }),
done: s.boolean(),
}).owned(),
});
export const backend = defineBackend({ schema, auth })
.functions(({ query, mutation }) => ({
todos: {
list: query({
args: {},
handler: ({ db }) => db.table("todos").collect(),
}),
add: mutation({
args: { title: s.string({ min: 1, max: 160 }) },
handler: ({ db }, { title }) =>
db.table("todos").insert({ title, done: false }),
}),
},
}));
```
When `auth` is present, `query` and `mutation` require a signed-in user by default. Their context includes non-null `user`, `auth`, and the correctly scoped `db`. Use `publicQuery` or `publicMutation` only when anonymous access is intentional.
An `.owned()` table automatically writes the current user ID on insert and adds that owner condition to every get, query, update, and delete. A user cannot address another user's row even if they learn its document ID.
## Browser client
```tsx
import { AuthGate, createClient, onCleanup } from "@clank.run/framework";
import type { backend } from "./backend.ts";
const client = createClient();
function Todos() {
const todos = client.live(client.api.todos.list);
onCleanup(() => todos.dispose());
return (
);
}
function App() {
return (
);
}
```
`createClient()` creates all of the authenticated browser mechanics together:
- a zero-codegen typed `client.api` tree;
- reactive user, session, loading, and error state;
- registration, login, logout, logout-all, and password-change methods;
- CSRF headers on authenticated mutations;
- one-shot query and mutation calls;
- seeded live queries over EventSource.
It intentionally accepts the backend as a type argument rather than a runtime value. This prevents server configuration such as a password pepper from entering a browser module.
`AuthGate` renders a default accessible email/password screen for the default profile. Pass `signedOut` when an application has custom profile fields or wants a branded sign-in experience.
## Auth client API
```ts
client.auth.user.value;
client.auth.session.value;
client.auth.authenticated.value;
client.auth.loading.value;
client.auth.error.value;
await client.auth.register({
email: "ada@example.com",
password: "a long unique passphrase",
profile: { name: "Ada" },
});
await client.auth.login({ email, password });
await client.auth.changePassword({ currentPassword, newPassword });
await client.auth.logout();
await client.auth.logoutAll();
await client.auth.reload();
```
The session credential is never exposed to JavaScript. The browser stores it only as an `HttpOnly` cookie. The CSRF token is held in memory and may be included in script-safe SSR boot state.
## SSR
Resolve the request before running private queries:
```tsx
const caller = await runtime.caller(request);
if (!caller.auth) throw new Error("Auth was not initialized.");
const bootAuth = authState(caller.auth);
const initial = caller.auth.user
? caller.query(api.todos.list)
: { value: [], version: runtime.version };
const page = await renderDocument(view, {
state: {
auth: bootAuth,
todos: initial.value,
version: initial.version,
},
scripts: ["/app.js"],
nonce,
});
```
In the browser:
```ts
const initial = readState()!;
const client = createClient({
initialAuth: initial.auth,
});
client.seed(client.api.todos.list, {}, initial.todos, initial.version);
```
`authState()` selects only the serializable user, session metadata, and CSRF token. Never serialize cookies, password hashes, peppers, database handles, or raw request headers.
## Profiles and roles
The default profile is `{ name?: string }`. Define a custom runtime-validated profile when needed:
```ts
const auth = defineAuth({
profile: {
displayName: s.string({ min: 1, max: 80 }),
timezone: s.string({ max: 80 }),
},
defaultRole: "member",
});
```
Inside a handler:
```ts
handler: ({ auth, user }) => {
auth.requireRole("admin", "owner");
return user.profile.displayName;
}
```
Trusted server code can call:
```ts
runtime.auth.setRole(userId, "admin");
runtime.auth.disableUser(userId);
runtime.auth.revokeUserSessions(userId);
```
Disabling a user deletes their sessions. Role changes and session revocations notify active live streams so stale connections close and reconnect through fresh authorization.
## Configuration
```ts
const auth = defineAuth({
signup: true,
defaultRole: "user",
sessionDurationMs: 30 * 24 * 60 * 60 * 1000,
idleTimeoutMs: 7 * 24 * 60 * 60 * 1000,
touchIntervalMs: 5 * 60 * 1000,
cookie: {
secure: "auto",
sameSite: "Strict",
},
password: {
minLength: 12,
pepper: process.env.CLANK_AUTH_PEPPER,
},
rateLimit: {
attempts: 10,
windowMs: 10 * 60 * 1000,
},
});
```
Defaults use scrypt with `N=2^17`, `r=8`, and `p=1`, a random 128-bit salt, a 64-byte derived key, constant-time comparison, and bounded concurrent hashing. A pepper is optional and must remain server-only. Changing or losing it invalidates existing password hashes unless the application implements an explicit migration.
Authentication attempts are rate-limited in process memory by normalized email and the trusted client identity supplied out of band by Clank's Node adapter. Caller headers cannot select this identity. A custom adapter may provide `rateLimit.clientKey(request)` from adapter-authenticated metadata; a multi-instance deployment also needs a shared `rateLimit.store`.
## HTTP endpoints
With the default backend prefix:
| Endpoint | Method | Purpose |
| --- | --- | --- |
| `/__clank/auth/session` | `GET` | Current safe auth state |
| `/__clank/auth/register` | `POST` | Create account and session |
| `/__clank/auth/login` | `POST` | Verify credentials and create session |
| `/__clank/auth/mfa/verify` | `POST` | Complete an email-code MFA challenge |
| `/__clank/auth/email/verify` | `POST` | Consume a single-use email-verification token |
| `/__clank/auth/email/resend` | `POST` | Issue another verification message |
| `/__clank/auth/password/recover` | `POST` | Request a generic password-recovery response |
| `/__clank/auth/password/reset` | `POST` | Consume a recovery token and revoke old sessions |
| `/__clank/auth/passkeys` | `GET` | List the current verified user's passkeys |
| `/__clank/auth/passkeys/register/start` | `POST` | Start passkey registration |
| `/__clank/auth/passkeys/register/finish` | `POST` | Verify and store a passkey |
| `/__clank/auth/passkeys/authenticate/start` | `POST` | Start discoverable passkey authentication without account lookup |
| `/__clank/auth/passkeys/authenticate/finish` | `POST` | Verify an assertion and create a session |
| `/__clank/auth/passkeys/delete` | `POST` | Remove one owned passkey |
| `/__clank/auth/logout` | `POST` | Revoke the current session |
| `/__clank/auth/logout-all` | `POST` | Revoke every session for the user |
| `/__clank/auth/change-password` | `POST` | Verify current password, rotate hash, revoke sessions |
JSON content type and body limits are enforced. State-changing authenticated requests require the exact-origin check and `x-clank-csrf`. Responses are `no-store`.
## Security boundaries and current scope
Built-in auth provides email/password registration, sessions, CSRF protection, roles, owned data, verification, generic recovery, email-code MFA, WebAuthn passkeys, bot-verification hooks, and revocation. Applications supply their email transport and can supply a shared rate-limit store.
OAuth/social identity, enterprise federation, hardware-attestation policy, risk scoring, account-linking policy, and provider-specific bot challenges remain integrations. Organization membership and CLI project authority belong to the deployment platform rather than application auth.
Continue with [Advanced authentication](authentication.md) for delivery hooks, MFA, passkeys, bot protection, distributed rate limits, and origin troubleshooting. See [Security](security.md) for deployment requirements and [the authenticated Todo](../examples/auth-todo/backend.ts) for the complete working example.
---
# Advanced authentication
Canonical URL: https://docs.clank.run/docs/authentication
Raw source: https://docs.clank.run/raw/authentication.md
Clank's advanced authentication features extend the framework's default email/password flow with verification, recovery, MFA, passkeys, bot protection, and distributed rate limits. Authentication uses the application SQLite database. Passwords use bounded scrypt work, session and one-time tokens are stored only as SHA-256 digests, browser sessions use `HttpOnly` and `SameSite=Strict` cookies, and every state-changing browser request requires an origin check plus a session-bound CSRF token.
```ts
import { defineAuth } from "@clank.run/framework/auth";
export const auth = defineAuth({
emailVerification: {
required: true,
async send({ email, token, expiresAt }) {
await mail.sendVerification({ email, token, expiresAt });
},
},
passwordRecovery: {
async send({ email, token, expiresAt }) {
await mail.sendPasswordReset({ email, token, expiresAt });
},
},
mfa: {
required: true,
async send({ email, code, expiresAt }) {
await mail.sendLoginCode({ email, code, expiresAt });
},
},
passkeys: {
rpName: "Orbit Tasks",
rpId: "tasks.example.com",
allowedOrigins: ["https://tasks.example.com"],
requireUserVerification: true,
},
botProtection: {
async verify({ request, action, token }) {
return antiBot.verify({ request, action, token });
},
},
rateLimit: {
store: sharedRateLimitStore,
},
});
```
Email verification and password-recovery links are expiring and single use. Password reset revokes every existing browser session before issuing the replacement session. Recovery requests perform a fixed minimum amount of work and return the same response whether or not an account exists. Recovery delivery is dispatched after the response path so provider latency does not reveal account existence; production delivery hooks should enqueue durably before returning from their own worker boundary.
Required email verification is enforced by backend authorization, not only by UI. `auth.requireVerified()` is also available in custom handlers.
MFA login returns a short-lived challenge only after the password is verified. Codes are hashed, attempt-limited, expiring, and single use. Passkeys use required discoverable credentials, WebAuthn `none` attestation, exact challenge and origin binding, RP ID hashes, user-presence and optional user-verification flags, ES256 or RS256 signature verification, and monotonic authenticator counters. Authentication starts without an account-specific credential list, preventing the start response from becoming an account-enumeration oracle.
The browser client includes:
- `requestEmailVerification()` and `verifyEmail(token)`
- `requestPasswordReset(email)` and `resetPassword(token, password)`
- `verifyMfa(code)`
- `listPasskeys()`, `registerPasskey(name)`, `loginWithPasskey()`, and `deletePasskey(id)`
The default `AuthForm` automatically presents the MFA code step. Product-specific verification, recovery, and passkey-management screens can use the same client methods.
## Distributed rate limits
`rateLimit.store` is the process-independent boundary:
```ts
interface AuthRateLimitStore {
consume(key: string, limit: number, windowMs: number):
number | undefined | Promise;
clear?(key: string): void | Promise;
close?(): void | Promise;
}
```
`consume` returns the retry delay in seconds when the limit is exceeded. A successful password login clears the same key after credential verification, so prior failures do not continue throttling the authenticated user. The built-in application store is safe for a single process; horizontally scaled applications should provide a shared implementation. Clank Deploy supplies its own control-database-backed shared store.
Clank's Node adapter attaches the socket or trusted-proxy address out of band. Authentication never trusts caller-supplied `x-clank-client-ip` or `x-forwarded-for` headers. A non-Node adapter can provide `rateLimit.clientKey(request)`, but that callback must return identity authenticated by the adapter or edge rather than copying an unverified request header.
Passkeys registered by releases before 0.7 used `residentKey: "preferred"`. Most platform authenticators made those credentials discoverable, but an authenticator was allowed not to. A user with a non-discoverable legacy credential must sign in through another configured method and register a new passkey before relying on account-free passkey sign-in.
## Operational rules
- Configure an HTTPS origin and an explicit RP ID before enabling production passkeys.
- Deliver tokens through a service driver; never log them.
- Keep password peppers and delivery credentials in platform secrets.
- Treat account-wide CLI credentials as interactive developer credentials. Use project-scoped tokens for CI.
- Revoke sessions after material identity or authorization changes.
## Troubleshooting origin rejection
`Cross-origin auth request rejected.` means the browser's exact `Origin` did not
match the origin reconstructed by the application server, or Fetch Metadata
identified a cross-site request. Do not disable this check or rewrite the
browser's `Origin`; both would weaken CSRF protection.
For an app deployed by Clank, open the canonical URL reported by `clank status`
and reload it before retrying. Managed ingress configures the generated runtime
automatically and is covered by an end-to-end auth regression. The platform
injects `TRUST_PROXY=1` and the reserved `CLANK_MANAGED_INGRESS=1` marker into
the application process. The Node adapter uses that marker only with trusted
proxy mode, preserving the public host while leaving host admission at Clank's
loopback-only managed ingress. Applications must not set this marker themselves.
During local development, a UI and API on different ports are different
origins even when both use `localhost`. Allowlist the UI origin on the backend,
configure credentialed CORS, and give the auth client the API URL:
```ts
const uiOrigin = "http://localhost:5173";
const runtime = await openBackend(backend, {
allowedOrigins: [uiOrigin],
});
const app = createApp()
.use(cors({ origin: uiOrigin, credentials: true }))
.route("*", "*", ({ request }) => runtime.handle(request));
const auth = createAuthClient({
url: "http://localhost:3000",
});
```
`allowedOrigins` now applies consistently to both backend RPC and its mounted
auth routes. An explicit client `url` uses credentialed requests. This is for
trusted, same-site origins such as localhost ports or sibling subdomains;
Fetch Metadata still rejects genuinely cross-site auth requests. Production
Clank apps should keep the default same-origin client and need no allowlist.
For a self-hosted app behind an exclusive trusted reverse proxy:
```ts
await serve(app, {
hostname: "127.0.0.1",
trustProxy: true,
allowedHosts: ["tasks.example.com"],
});
```
The proxy must replace `X-Forwarded-Host` and `X-Forwarded-Proto` with the
browser-visible host and protocol, and untrusted clients must not be able to
reach the Node listener directly. When `TRUST_PROXY` and `ALLOWED_HOSTS` are
read from environment variables, verify those variables in the running
process—not only in a local shell.
---
# Server primitives
Canonical URL: https://docs.clank.run/docs/server
Raw source: https://docs.clank.run/raw/server.md
Clank's server layer targets the Fetch standard, not one vendor's runtime. The same `app.handle(request)` works anywhere with Web `Request` and `Response` objects.
## Routes
```ts
const app = createApp()
.get("/health", () => text("ok"))
.get("/users/:id", ({ params }) => json({ id: params.id }))
.post("/messages", async ({ request }) => {
const input = await request.json();
return json(await createMessage(input), { status: 201 });
});
const response = await app.handle(request);
```
Literal route paths infer their parameter keys. In `.get("/users/:id", handler)`, `params.id` is a string; optional `:tab?` parameters are optional, and `*` exposes the wildcard value.
Methods are `route`, `get`, `post`, `put`, `patch`, and `delete`. Routes use the same parameter, optional-segment, and wildcard matcher as the client router.
## Request context
Every handler receives:
```ts
interface RequestContext {
request: Request;
url: URL;
params: Record;
state: Record;
}
```
Middleware can use `state` for request IDs, authenticated users, database handles, timing, or other per-request values.
## Middleware
```ts
app.use(async (context, next) => {
context.state.user = await authenticate(context.request);
const response = await next();
const headers = new Headers(response.headers);
headers.set("x-frame-options", "DENY");
return new Response(response.body, { status: response.status, headers });
});
```
Middleware executes in registration order and unwinds in reverse order. Calling `next()` twice throws. Unhandled errors become a generic JSON 500 response. Use `createApp({ onError })` for private logging; unexpected exception text is never returned to the client.
Built-ins:
```ts
app.use(logger());
app.use(cors({ origin: "https://app.example.com", credentials: true }));
app.use(securityHeaders({
contentSecurityPolicy: "default-src 'self'; object-src 'none'; frame-ancestors 'none'",
}));
```
Credentialed CORS rejects wildcard origins at configuration time. Origin arrays and callbacks are available when an exact allowlist is required. CORS controls browser response access; it is not authentication or CSRF protection.
The response helpers are `json`, `text`, and `html`.
## Mount an agent bridge
The minimal adapter is a wildcard route:
```ts
app.route("*", "/api/agent/*", ({ request }) => bridge.handle(request, {
user: authenticatedUser,
}));
```
If you want the well-known manifest at a separate path, add an explicit route to the same handler.
## Node runtime adapter
`clank/node` provides the dependency-free adapter for Node 22.16+:
```ts
import { serve, staticFiles } from "@clank.run/framework/node";
const publicFiles = staticFiles("./public", { prefix: "/assets" });
const app = createApp()
.get("/assets/*", ({ request }) => publicFiles.handle(request))
.get("/health", () => text("ok"));
const server = await serve(app, {
hostname: "127.0.0.1",
port: 3000,
maxBodySize: 1024 * 1024,
});
await server.close();
```
`serve()` translates Node HTTP messages to Fetch objects, streams response bodies for SSE, propagates aborts, preserves multiple cookies, caps bodies and headers, configures timeouts, validates Host headers, and can trust forwarded protocol/client IP only when explicitly enabled. Loopback listeners accept loopback hosts by default; public deployments should set `allowedHosts`.
`staticFiles()` supports GET/HEAD, index files, MIME types, cache headers, dotfile denial, traversal rejection, and post-symlink containment checks.
```ts
await serve(app, {
hostname: "0.0.0.0",
allowedHosts: ["app.example.com"],
trustProxy: true, // only behind a trusted, exclusive reverse proxy
});
```
Edge and worker runtimes can usually accept `app.handle` directly as their fetch handler.
For inferred query/mutation RPC, SQLite, and EventSource live queries, see [Full-stack SSR, SQLite, and live sync](full-stack.md).
For secure cookies, owned data, and reverse-proxy guidance, see [Authentication](auth.md) and [Security](security.md).
---
# Service drivers
Canonical URL: https://docs.clank.run/docs/services
Raw source: https://docs.clank.run/raw/services.md
Clank keeps external services behind explicit, inspectable drivers. Generated blueprints write `src/service-requirements.ts`, which records every named service, kind, capability, and whether production startup should require it.
```ts
import {
createServiceRegistry,
defineServiceDriver,
openJobQueue,
openLocalFileStore,
} from "@clank.run/framework/services";
import { assertServices } from "./service-requirements.ts";
const files = await openLocalFileStore({
directory: ".data/files",
signingKey: process.env.FILE_SIGNING_KEY!,
});
const services = createServiceRegistry([
defineServiceDriver({
name: "uploads",
kind: "files",
capabilities: ["signed-read", "signed-write"],
service: files,
}),
]);
assertServices(services);
```
The registry gives humans and agents one deterministic place to inspect configuration, validate blueprint requirements, run health checks, and close resources.
## Files
`openLocalFileStore` provides integrity-checked local object storage:
- logical keys never become filesystem paths;
- data and metadata are owner-only and written atomically;
- every read verifies size and SHA-256;
- upload size and content type are bounded;
- signed capabilities bind one key, one operation, and one expiry; and
- the built-in HTTP handler supports signed `PUT`, `GET`, and `HEAD`.
The `FileStore` interface is the compatibility target for a future remote object-storage driver. Application code should store file keys and metadata, not local paths.
## Email
`openFileEmailService` writes a development outbox without sending mail. `createHttpEmailService` sends a normalized JSON envelope to an HTTPS delivery service with timeouts, bounded retries, bearer credentials, and idempotency keys.
Email validation rejects header injection and reserved transport headers. Verification, recovery, and MFA callbacks from `defineAuth` can call either driver.
## Durable jobs and cron
`openJobQueue(database, handlers)` stores jobs in SQLite and implements:
- JSON payload validation;
- unique idempotency keys;
- scheduled `runAt` execution;
- transactional worker leases and expired-lease recovery;
- handler timeouts and abort signals;
- exponential bounded retries;
- dead-letter state and explicit retry; and
- multiple cooperating workers against the same database.
Cron schedules should enqueue jobs with stable unique keys such as `daily-report:2026-07-16`. The queue owns delivery state; business handlers remain ordinary async functions.
## Webhooks
`signWebhook` and `verifyWebhook` bind the exact body to a timestamped HMAC-SHA256 signature and enforce a replay window. `createWebhookSender` uses HTTPS, rejects redirects, preserves one delivery ID across retries, and retries only transient responses.
## Production boundaries
Local files and a single SQLite job database are appropriate for a single-node deployment. Horizontal deployments must bind compatible remote object storage and a shared job/database topology. Service capabilities are part of the blueprint so the deployment platform can refuse an incomplete production plan instead of silently degrading.
---
# Observability
Canonical URL: https://docs.clank.run/docs/observability
Raw source: https://docs.clank.run/raw/observability.md
One `createObservability` instance provides structured logs, W3C trace propagation, metrics, and readiness checks without a runtime package.
```ts
import {
createObservability,
createOtlpHttpSpanExporter,
} from "@clank.run/framework/observability";
const observability = createObservability({
serviceName: "orbit-tasks",
serviceVersion: "1.0.0",
environment: process.env.NODE_ENV,
exporter: process.env.OTLP_TRACES_URL
? createOtlpHttpSpanExporter({
url: process.env.OTLP_TRACES_URL,
headers: { authorization: `Bearer ${process.env.OTLP_TOKEN}` },
serviceName: "orbit-tasks",
})
: undefined,
});
const app = createApp()
.use(observability.middleware())
.get("/healthz", () => observability.health.response());
```
Generated apps install the middleware and a database readiness check automatically.
## Logs
Logs are one JSON object per event and automatically include service, trace, span, and request IDs. Keys containing password, secret, token, authorization, cookie, or API-key patterns are recursively redacted. Values, nesting, arrays, event counts, and attribute counts are bounded.
Use child loggers for stable context:
```ts
const log = observability.logger.child({ component: "reminders" });
log.info("Reminder queued.", { taskId });
```
Do not attach email addresses, request bodies, query strings, session IDs, or unbounded user identifiers as metric labels.
## Traces
Incoming `traceparent` headers are validated according to W3C Trace Context. Server spans preserve sampled trace IDs, create a new span ID, return `traceparent`, and attach a safe request ID.
```ts
await observability.tracer.trace("reminder.send", async (span) => {
span.setAttribute("reminder.channel", "email");
await sendReminder();
});
```
Exceptions become bounded span events and error status. The OTLP/HTTP JSON exporter uses protocol byte encoding, HTTPS, explicit headers, a timeout, and redirect rejection.
## Metrics
The registry supports counters, gauges, and histograms and renders Prometheus text:
```ts
const sent = observability.metrics.counter(
"app_reminders_sent_total",
"Reminders accepted by the delivery service.",
["channel"],
);
sent.add(1, { channel: "email" });
```
Metric and label names are validated, label sets must match their declaration, histograms use cumulative buckets, and a global series limit prevents accidental memory exhaustion. HTTP middleware normalizes ID-like route segments before using the path as a label.
Expose `observability.metrics.response()` only on an operator-protected endpoint or private network.
### Deployment ingress metrics
Clank Deploy separately records per-project traffic that passes through managed ingress. Minute rows contain status classes, 5xx errors, fixed method counters, duration sum/max, cumulative latency buckets, and bounded byte counts. The dashboard returns bounded `15m`, `1h`, `24h`, `7d`, and `30d` current/previous series, rates, percentiles, and distributions without storing host, path, IP, user, email, query-string, or user-agent labels.
Ingress latency ends when upstream response headers arrive; it is not streamed-response completion time. Response size is known only when `Content-Length` is present. See [Deployment dashboard, quotas, and domains](platform-dashboard.md) for exact semantics and retention.
## Readiness
Critical checks determine the HTTP status. Optional dependencies remain visible without taking the service out of rotation:
```ts
observability.health.register("database", checkDatabase);
observability.health.register("email", checkMail, { critical: false });
```
Checks run concurrently with individual timeouts. Responses use `no-store` and return `503` when a critical dependency fails.
---
# Managed ingress and external data
Canonical URL: https://docs.clank.run/docs/data-plane
Raw source: https://docs.clank.run/raw/data-plane.md
The data-plane module separates public routing and provider-managed SQL from application processes.
## Managed ingress
`createManagedIngress` routes exact hostnames to fixed upstream origins. Route configuration is trusted control-plane data; request paths never select an arbitrary upstream.
The proxy:
- matches only active, uniquely assigned hosts;
- allows loopback upstreams by default and requires an explicit allowlist for remote upstream hosts;
- strips fixed hop-by-hop headers, `Connection`-nominated headers, and private server headers;
- sets controlled forwarding and project headers;
- bounds request bodies before forwarding;
- applies timeouts and safe-method retries for network failure or transient upstream 5xx responses;
- streams response bodies, including SSE;
- tracks per-route circuit failures; and
- exposes active-route health checks.
- records bounded per-project ingress metrics; and
- constructs every target from the trusted upstream origin before assigning the request path, so a scheme-relative path cannot replace the upstream host.
The Clank platform can enable ingress directly:
```ts
await openPlatform({
publicUrl: "https://console.clank.example",
dataDirectory: "/srv/clank",
ingress: {
baseDomain: "apps.clank.example",
},
});
```
Projects are then available at `https://.apps.clank.example`. TLS should terminate at the edge proxy or load balancer in front of the Clank control/data-plane process.
## Custom domains
```sh
clank domain add tasks.customer.example
# publish the displayed TXT record
clank domain verify
clank domain list
```
Custom hosts do not enter the ingress route set until the exact `_clank.` TXT challenge is present **and** the hostname resolves to the configured CNAME target or edge A/AAAA address. Challenges are project bound, expiring, and unique by hostname. Pending and verified hostnames cannot be claimed by another project.
Ownership, routing, and certificate status are separate. Clank provides a constant-time, token-protected permission endpoint for Caddy On-Demand TLS; Caddy or another edge remains responsible for ACME, certificate/key storage, and renewal. See [Deployment dashboard, quotas, and domains](platform-dashboard.md) for the DNS lifecycle and production Caddy configuration.
The deployment platform refreshes routing in bounded background batches. Expiring SQLite claims prevent duplicate work across control-plane processes, per-domain deadlines prevent a slow resolver from stopping the pass, and manual checks fence out stale automatic results. Ownership verification stays explicit.
## External PostgreSQL over HTTPS
`createHttpPostgresDriver` is the zero-package external SQL boundary:
```ts
const postgres = createHttpPostgresDriver({
url: process.env.DATABASE_HTTP_URL!,
token: process.env.DATABASE_HTTP_TOKEN!,
});
const result = await postgres.query({
text: "SELECT id, title FROM tasks WHERE owner_id = $1",
parameters: [user.id],
});
```
The driver sends structured statements and JSON parameters to an HTTPS database gateway. It never interpolates parameters, rejects redirects, bounds statement counts and response bytes, applies a timeout, and validates every result envelope.
`applyExternalMigrations` creates the same immutable `clank_migrations` ledger and sends all pending migration and ledger statements in one provider transaction. Edited or missing applied migrations stop deployment.
The framework's built-in auth, live document database, and revision journal remain SQLite-first. External PostgreSQL is an explicit service driver for workloads that require horizontal SQL. An application must choose which records are authoritative; it must not dual-write SQLite and PostgreSQL without an outbox or another explicit consistency protocol.
## Provisioning
`createHttpDatabaseProvisioner` defines the control-plane boundary for creating and destroying project databases:
- provisioning requires project, region, engine, and idempotency key;
- provider credentials stay in platform secrets;
- returned connection material is validated before storage;
- destruction requires `destroy `; and
- application code receives only its own binding.
Open-source operators can implement this HTTP contract against their preferred PostgreSQL provider without changing framework application code.
---
# AI application blueprints
Canonical URL: https://docs.clank.run/docs/blueprints
Raw source: https://docs.clank.run/raw/blueprints.md
An application blueprint is the reviewable contract between a human request, an AI planner, generated source, and deployment resources.
The default file is `clank.app.ts`. It must export one data literal:
```ts
export default {
name: "Orbit Tasks",
description: "A live, authenticated task planner.",
auth: {
required: true,
organizations: true,
roles: {
owner: {
description: "Workspace administrator.",
permissions: ["tasks.*", "members.*"],
},
member: {
description: "Workspace member.",
permissions: ["tasks.read", "tasks.write"],
},
},
},
entities: {
tasks: {
description: "Actionable work.",
ownership: "workspace",
realtime: true,
displayField: "title",
completionField: "done",
fields: {
title: { type: "string", min: 1, max: 200 },
done: { type: "boolean", default: false },
},
},
},
routes: [
{ path: "/", view: "TaskList", entity: "tasks" },
],
services: {
reminders: {
kind: "jobs",
description: "Schedule durable reminders.",
required: true,
},
},
deployment: {
database: "sqlite",
scale: "single",
isolation: "container",
},
} satisfies import("@clank.run/framework/blueprint").AppBlueprintInput;
```
## Commands
```sh
clank explain
clank plan
clank plan --output .clank/reviewed-plan.json
clank generate .
clank generate ./new-app --blueprint ./clank.app.ts
clank generate ./new-app --blueprint ./clank.app.ts --framework=local
```
`explain` summarizes identity, data, routes, operations, services, and unresolved production requirements.
`plan` normalizes the contract and prints `clank-plan/1`, including every generated path, byte length, SHA-256 checksum, aggregate digest, and warning. Identical blueprints on the same Clank version produce identical plans.
`generate` writes the authenticated full-stack application, human and agent operating guides, and `.clank/plan.json`. It refuses to replace a changed file unless `--force` is supplied. The source blueprint is preserved when generating into its own directory. `--framework=local` makes a generated app install directly from the current Clank checkout without requiring a published registry version.
## Static safety
Clank does not import or execute `clank.app.ts`. A dedicated parser accepts an exported JSON-like literal with comments, trailing commas, and an optional `satisfies` or `as const` clause. Function calls, computed properties, template expressions, environment reads, imports with runtime behavior, and arbitrary statements are rejected.
This means an AI can prepare a TypeScript-assisted contract without gaining implicit local-code execution during review or generation. Generated application source is still code and must pass normal review, authorization, conformance, and deployment controls.
## Contract surface
A blueprint declares:
- entities, field constraints, ownership, indexes, display/completion semantics, and live behavior;
- relationships and deletion expectations;
- authentication, organizations, roles, and permissions;
- routes, views, access requirements, and user-visible actions;
- immutable SQL migrations;
- files, images, email, jobs, cron, search, and webhook service requirements; and
- database, scaling, isolation, health, region, custom-domain, and public environment requirements.
Generated applications currently include built-in authentication by construction. `auth.required` may be omitted or set to `true`; unauthenticated generation is rejected rather than silently producing a client/backend mismatch.
The generator creates a deterministic baseline, not domain truth. Payment, legal, medical, tax, retention, or other domain-specific behavior still needs explicit contracts and review.
See [`examples/blueprint-todo/clank.app.ts`](../examples/blueprint-todo/clank.app.ts) for a Todoist-style specification.
---
# Deployment CLI
Canonical URL: https://docs.clank.run/docs/cli
Raw source: https://docs.clank.run/raw/cli.md
The `clank` executable contains both the compiler and deployment client. It does not install application dependencies or execute remote build hooks.
## Create
```sh
clank create my-app
clank create my-app --name="Customer workspace"
cd my-app
npm install
npm run dev
```
The generated app includes auth, an owned Todo table, SSR, hydration, live updates, Tailwind, a health route, deployment configuration, its first migration, a human `README.md`, and an agent-oriented `AGENTS.md`. Its only dependency is the official `@clank.run/framework` package, which has no transitive dependencies. The exact CLI runtime is still embedded into deployment artifacts, so the platform never runs an install hook.
Until a Clank version is published, or while changing the framework and an app together, point the scaffold at the current checkout:
```sh
node /path/to/clank/scripts/clank.mjs create my-app --framework=local
cd my-app
npm install
```
`--framework` also accepts an explicit npm dependency spec, tarball, or `file:` path. Blueprint `plan` and `generate` accept the same option so their checksum includes the actual dependency choice.
## Help and readiness
```sh
clank help
clank deploy --help
clank help --json
clank doctor
clank doctor --json
```
Every command supports focused `--help` without authenticating or executing the command. Unknown commands and long options fail non-zero and suggest a close known spelling instead of being ignored.
`doctor` validates the Node version, deployment configuration, compiled entry state, migration names and checksums, package scripts, CLI login, and local project link. Missing login or a first-deploy link is a warning, not a local-build failure. Its `clank-doctor/1` JSON report and the `clank-cli-help/1` command manifest are stable agent surfaces.
## Authenticate
```sh
clank login --server=https://deploy.example.com
clank whoami
clank logout
```
Passwords never pass through the CLI. Profiles are stored under `${CLANK_HOME:-~/.clank}/config.json`. Set `CLANK_HOME` to isolate CI or test credentials.
Existing Proact profiles and project links are imported automatically on first use. See [Renaming from Proact](renaming-from-proact.md).
Credential profiles and project links are size-bounded, structurally validated, canonicalized to one server URL, written with owner-only permissions, and replaced atomically. Invalid local state fails closed without echoing token contents. Ordinary platform requests time out after 30 seconds; deployment uploads and health-gated activation time out after five minutes. JSON responses are UTF-8 validated and capped at 4 MiB before parsing.
## Projects
```sh
clank project create my-app
clank project create "Customer workspace" --slug=customer-workspace
clank project list
clank project link
clank project delete [project-id] \
--confirm="delete-site " \
--acknowledge-data-loss
```
Links are written to `.clank/project.json` and should normally remain uncommitted.
Deletion is permanent and requires an account-wide token plus an owner/admin organization role. Project-scoped tokens are rejected even if they have `tokens` permission. A successful deletion removes the matching local project link, but leaves other directories and off-platform copies untouched. See [Site deletion](deployment-platform.md#site-deletion).
## Workspaces and access
```sh
clank org list
clank org create "Acme Engineering" --slug=acme
clank org members
clank org invite person@example.com --role=developer
clank org invitations
clank org revoke-invite
clank org accept
clank org role
clank org remove
```
Invitation creation prints its email-bound token once. Reissuing for the same email invalidates older tokens. Invitation lists contain identifiers and safe metadata, never tokens or hashes. Only owners and administrators can create/revoke invitations or change/remove members; only an owner can grant or change the owner role, and the last owner is protected.
## Workspace activity
```sh
clank activity
clank activity --org= --limit=100
clank activity --before=
clank activity --json
```
`clank audit` is an alias. The feed is newest first and includes the event ID, action, target, actor, timestamp, and safe audit metadata. `--json` prints the complete stable response for agents and automation. Owners, administrators, and developers can read events for their current organizations; viewers cannot. A project-scoped token needs `audit` permission and receives only that project's events.
## Deploy and inspect
```sh
clank deploy
clank deploy ../another-app
clank deploy --dry-run
clank deploy --output=/secure/path/release.clank.gz
clank deploy --name="Customer workspace" --slug=customer-workspace --org=
clank deploy --json
clank inspect /secure/path/release.clank.gz
```
Deployment validates config, runs the local build without a shell, packages included files plus the exact Clank runtime, verifies the artifact locally, creates and links a project if needed, uploads with a digest/idempotency key, and waits for migration and health. `--name`, `--slug`, and `--org` configure that automatic first project creation, so login plus one deploy command is sufficient.
`--dry-run` is deliberately offline: it builds and writes a verified artifact without reading a login, creating a project, or contacting a platform. `--json` suppresses human progress output and emits one `clank-deploy-result/1` document with artifact, release, URL, and timing data.
Before upload the CLI stores a non-secret attempt record in `.clank/deploy-attempt.json`. If the connection fails after the platform may have accepted the artifact, the next identical deploy within 24 hours reuses the same idempotency key and converges on the original release. A definitive platform response clears the record.
## Status and rollback
```sh
clank status
clank releases
clank releases delete \
--confirm="delete-release "
clank logs --limit=500
clank rollback
clank rollback --restore-data --confirm="restore "
```
`clank releases` reports the retained artifact count and uncompressed runtime/snapshot bytes. Cleanup requires the token's `rollback` permission, never removes the active artifact, and preserves release metadata, logs, and audit evidence. Removing the active release's immediate predecessor also destroys code rollback and its matching data snapshot, so it additionally requires `--allow-rollback-loss`.
Logs are bounded. Every non-empty known secret value is redacted, including short values, with longer overlapping values replaced first. Apps must still avoid logging credentials because transformed, encoded, split, or externally emitted values cannot be recognized reliably.
## Secrets
```sh
printf '%s' "$API_KEY" | clank secrets set API_KEY
clank secrets set API_KEY --from-env=API_KEY
clank secrets list
clank secrets delete API_KEY
```
There is deliberately no `secrets get`.
## Local migrations
```sh
clank migrate plan
clank migrate apply
```
Production migrations always run inside the deployment transaction.
## Automation
Use `clank token create` to issue a short-lived project token containing only the CI job's required permissions, and isolate it with a dedicated `CLANK_HOME`. Membership and token scope are re-evaluated on every request; removing the member or revoking the token stops future access.
Successful commands exit `0`; input, auth, build, upload, migration, or health failures exit non-zero. Commands that document `--json` emit structured failures to standard error with a stable code and message. Failed server revocation prevents `logout` from silently deleting the only local token reference. `--local` is for platform recovery.
---
# Deployment platform
Canonical URL: https://docs.clank.run/docs/deployment-platform
Raw source: https://docs.clank.run/raw/deployment-platform.md
Clank Deploy is the open-source control plane for turning a Clank source directory into a running release. It uses Fetch, Node, SQLite, Web Crypto, and no runtime NPM dependencies.
The platform is intentionally inspectable:
- deployment configuration is checked-in JSON;
- builds run locally as an argument array, never as a server-side shell hook;
- every artifact and file has a SHA-256 digest;
- artifacts contain the exact Clank runtime used by the CLI;
- SQL migrations have ordered, immutable checksums;
- releases, failures, rollbacks, backups, tokens, secret-name changes, and projects are audited;
- secret values are encrypted and never returned by the API;
- failed migrations or health checks restore the prior database and process.
The browser console is also an operating surface, not only a login page. It shows site health, 1-hour through 30-day ingress performance, enforced capacity, releases, scheduled encrypted backups, logs, and the full custom-domain lifecycle. See [Deployment dashboard, quotas, and domains](platform-dashboard.md).
## Five-minute path
```sh
CLANK_PLATFORM_DATA=.clank-platform \
CLANK_PLATFORM_URL=http://127.0.0.1:4200 \
npm run dev:platform
```
Create the bootstrap account at `http://127.0.0.1:4200`, then:
```sh
clank login --server http://127.0.0.1:4200
clank create my-todo
cd my-todo
npm install
clank doctor
clank deploy
```
The first deploy creates and links a project automatically. Pass `--name`, `--slug`, or `--org` on that command when the directory name is not the intended platform identity. Remote platform URLs must use HTTPS; loopback HTTP is accepted only for development.
## Device login
`clank login` follows the interaction in [RFC 8628](https://www.rfc-editor.org/rfc/rfc8628):
1. The CLI requests a high-entropy device code and short user code.
2. The user signs in at the exact platform origin and reviews the client name and code.
3. Approval requires the browser session's CSRF token.
4. The CLI polls at the server-provided interval.
5. The raw access token is returned exactly once. Only its SHA-256 hash is stored by the platform.
6. The CLI stores it in `~/.clank/config.json` with mode `0600`.
Device codes expire after ten minutes by default. Access tokens expire after 90 days by default. `clank logout` revokes the current token before deleting it locally.
Self-registration defaults to `bootstrap`: only the first account can register. Operators may explicitly choose public or disabled registration.
Version 0.7.0 preserves existing Proact accounts, sessions, platform records, and CLI links while moving all writes to Clank names. Review [Renaming from Proact](renaming-from-proact.md) before upgrading a hosted control plane.
## Deployment configuration
Every app has `clank.deploy.json`:
```json
{
"version": 1,
"entry": "dist/server.js",
"include": ["dist", "public", "migrations"],
"build": {
"command": ["clank", "build", "src", "dist"]
},
"database": {
"path": "app.sqlite",
"migrations": "migrations",
"allowUnsafeMigrations": false
},
"health": {
"path": "/healthz",
"timeoutMs": 15000
},
"env": {
"FEATURE_SET": "stable"
}
}
```
Rules:
- `entry` must be compiled `.js` or `.mjs` inside an included path.
- Include paths are literal files or directories, not shell globs.
- Symbolic links, special files, parent traversal, `.env*`, private-key names, and VCS metadata are rejected.
- `build.command` is executed locally without a shell.
- `env` is public artifact configuration; credentials belong in platform secrets.
- `PORT`, `HOST`, `NODE_OPTIONS`, and `CLANK_*` variables are reserved.
- `database.path` is persistent project data outside release directories.
- Changing `database.path` during deployment is rejected to prevent silently forking production data.
## Artifact protocol
The wire media type is `application/vnd.clank.deploy+gzip`. Its document protocol is `clank-deploy/1` and contains:
- normalized configuration;
- builder protocol, Clank version, and Node version;
- a sorted file list with path, size, mode, SHA-256 digest, and base64 content.
The gzip timestamp is fixed, so identical inputs on the same Clank and Node versions produce identical bytes. The CLI also sends an artifact digest and idempotency key.
```sh
clank deploy --dry-run
clank inspect .clank/artifacts/.clank.gz
```
Dry-run artifact creation is offline and does not require platform credentials. Ambiguous upload failures retain a private local attempt record for 24 hours, allowing the next identical command to reuse its idempotency key instead of accidentally creating a second release after a lost response.
The metadata supports the traceability goals of [SLSA provenance](https://slsa.dev/spec/v1.2/provenance), but `clank-deploy/1` is not a signed SLSA attestation. Signing and transparency-log integration are future extensions.
## Release transaction
Deployment runs in this order:
1. Authenticate the bearer token and verify project ownership.
2. Enforce request, gzip, file-count, file-size, and aggregate limits.
3. Verify config, paths, modes, file hashes, and artifact digest.
4. Extract into a non-active release directory.
5. Stop the active process to quiesce SQLite writes.
6. Create a consistent SQLite backup.
7. Verify migration history and apply pending SQL in one `BEGIN IMMEDIATE`.
8. Start the candidate with persistent data and decrypted runtime secrets.
9. Poll the configured health route.
10. Mark it active only after health succeeds.
If backup, migration, startup, or health fails, Clank stops the candidate, restores the snapshot, restarts the prior release, marks the candidate failed, and records an audit event.
This creates a short SQLite write outage. Continuously writable multi-instance systems need an external database and another deployment driver.
Before creating a release directory, Clank checks the site's retained-artifact count and the uncompressed bundle plus current SQLite/WAL footprint under the durable project lock. After taking a pre-deploy snapshot, it records the exact snapshot size. This bounds cumulative deployment storage rather than only bounding each upload.
```sh
clank releases
clank releases delete \
--confirm="delete-release "
```
Cleanup requires `rollback` permission. The active artifact is never removable. Add `--allow-rollback-loss` only when intentionally removing the active release's immediate predecessor; that removes the predecessor's runtime files and the active release's matching data-restore snapshot. Cleanup preserves release metadata and audit history.
## Site deletion
Owners and organization administrators can permanently reclaim a site slot from the dashboard or CLI:
```sh
clank project delete [project-id] \
--confirm="delete-site " \
--acknowledge-data-loss
```
Deletion requires an account-wide browser session or CLI token. A project-scoped token is deliberately insufficient, including one with `tokens` permission. Browser deletion also passes the normal same-origin and CSRF checks. The exact slug-bound phrase and separate acknowledgement make accidental generic confirmation impossible.
The operation holds the same durable project lock used by deploy, rollback, release cleanup, and backup work. It rechecks current membership under that lock, stops the supervised application, validates that the platform project path and its parents are real directories rather than symbolic links, and removes the complete project root. Only then does one control-database transaction revoke active project tokens, remove orchestration placement/operation state, delete project metadata and its cascading domain/release/secret/log/metric/backup-schedule rows, and append a surviving `project.delete` audit event.
If filesystem validation or removal fails, project metadata remains and Clank attempts to restart the prior active release. If the later metadata transaction fails after files were removed, the API reports a fixed recovery-safe error and a retry completes cleanup. A successful response means the platform-managed local application database, releases, rollback snapshots, encrypted recovery points, secrets, logs, metrics, domains, and scoped tokens are gone. External databases, copied artifacts, Caddy certificate storage, replicated/off-host backups, and other operator-managed copies are not discovered or erased.
## Workspace audit history
Every project and organization event carries durable organization attribution in the control database. The Activity dashboard and `clank activity --json` expose the same newest-first, cursor-paginated feed. Owners, administrators, and developers can read history only for organizations where their current role permits `audit`; viewers are excluded. Project-scoped tokens must include `audit`, are rechecked against current membership, and see only their project.
Project deletion removes application state but not its audit rows, organization attribution, actor identity, or safe metadata. The deleted target is therefore still visible through `GET /api/audit` after `/api/projects/:id/audit` naturally becomes unavailable. Upgrading an older control database adds the organization column in place and backfills it from live project rows or the project's recorded create/delete metadata.
The API is append-only, not a cryptographically signed transparency log. A trusted database administrator can still alter SQLite directly; export audit records to an independently controlled append-only sink when that threat is in scope.
## Rollback
Code-only rollback is the default:
```sh
clank releases
clank rollback
```
The target runs against the current database and must pass health before activation. Use expand/contract migrations so earlier code tolerates the newer schema.
Data restore can lose newer writes, so it is constrained:
```sh
clank rollback \
--restore-data \
--confirm="restore my-project"
```
It is available only for the immediately previous release with a pre-deploy backup.
## Secrets
```sh
printf '%s' "$API_KEY" | clank secrets set API_KEY
clank secrets list
clank secrets delete API_KEY
```
Secret names and timestamps are visible; values are never returned. Values use AES-256-GCM under the platform master key and are decrypted only for runtime injection.
Docker launches use name-only environment arguments (`-e NAME`); values are inherited by the Docker client rather than placed in process arguments. Privileged host/container administrators can still inspect runtime environment state. Platform-controlled names such as `PATH`, `HOME`, `HOST`, `PORT`, `NODE_ENV`, and `TRUST_PROXY` are reserved.
Secret changes take effect on the next release or supervised restart.
The local default creates a `0600` master-key file. Production should provide `CLANK_PLATFORM_MASTER_KEY` through separate secret management and back it up independently.
## Runners
The process runner is dependency-free and appropriate only when every app is trusted by the host operator.
The Docker runner adds read-only root, capability dropping, no-new-privileges, non-root execution, PID/memory/CPU limits, a temporary filesystem, and narrow release/data mounts:
```sh
CLANK_RUNNER=docker \
CLANK_DOCKER_IMAGE=node:22-bookworm-slim \
clank-platform
```
Containers improve isolation but are not perfect hostile-code sandboxes. High-risk public multi-tenancy should use microVMs or dedicated nodes, strict egress policy, image digests, and secret mounts or an external secret broker.
## API outline
Device/public:
- `POST /api/device/start`
- `POST /api/device/token`
- `POST /__clank/auth/invited-register` — invitation-bound account creation without public signup
- `GET /livez` — process liveness
- `GET /healthz`, `GET /readyz`, or `GET /_clank/readyz` — storage-backed control-plane readiness;
the reserved path is evaluated before application-host ingress for hosted load balancers
Browser session:
- `GET /api/dashboard`
- `GET /api/audit?limit=100&before=&organizationId=` — role-filtered workspace history that survives project deletion;
- project status, metrics, releases, logs, and domains;
- project and domain creation/removal with CSRF;
- `DELETE /api/projects/:id` with exact confirmation and explicit data-loss acknowledgement;
- `GET /api/device/info`
- `POST /api/device/approve`
- `POST /api/device/deny`
- `/__clank/auth/*`
Bearer:
- account and token listing/revocation;
- project creation/listing/status;
- owner/admin-only permanent project deletion using an account-wide token;
- release upload/history/rollback;
- release storage usage and confirmation-gated inactive artifact cleanup;
- logs, encrypted secrets, scheduled/manual backup operations, and audit events.
Managed edge:
- `GET /_clank/tls/ask` — token-protected, constant-time Caddy certificate permission lookup.
See [CLI](cli.md), [Migrations](migrations.md), [Dashboard and domains](platform-dashboard.md), [Platform security](platform-security.md), and [Self-hosting](self-hosting.md).
---
# Deployment dashboard, quotas, and domains
Canonical URL: https://docs.clank.run/docs/platform-dashboard
Raw source: https://docs.clank.run/raw/platform-dashboard.md
Clank includes a dependency-free browser console for operating projects, traffic, releases, logs, and custom domains. It is served by the same authenticated control plane as the CLI API; there is no separate dashboard service or browser package to install.
## Dashboard
Open the configured `CLANK_PLATFORM_URL` and sign in with a platform account. A valid browser session opens the dashboard directly after a refresh. Expired sessions return to the sign-in view, and every authenticated browser mutation requires the session CSRF token. The first installation account uses the one-time bootstrap unless the operator explicitly enables public registration. Later invitees can choose **Use invitation** to create only the email-bound invited account and join its workspace without reopening public signup.
The console uses normal, refresh-safe URLs rather than keeping navigation only in page memory:
| URL | View |
| --- | --- |
| `/login`, `/signup`, `/invite` | Account access and invitation-assisted registration |
| `/overview` | Account-wide project and traffic summary |
| `/activity` | Authorized workspace audit history |
| `/admin` | Browser-only global analytics, account directory, and support access for allowlisted operators |
| `/workspaces//people` | Members, roles, and invitations for one workspace |
| `/projects//performance` | Project traffic and latency, with `?range=15m\|1h\|24h\|7d\|30d` |
| `/projects//domains` | Domain ownership, routing, and TLS |
| `/projects//deployments` | Immutable releases and storage lifecycle |
| `/projects//backups` | Scheduled and manual encrypted backups |
| `/projects//logs` | Redacted runtime logs |
| `/projects//settings` | Administrative and destructive project controls |
Links update browser history, Back/Forward restores the matching view, and every listed URL can be
opened or refreshed directly. The server recognizes only this bounded route grammar. Before login,
a protected deep link renders the generic account-access shell without confirming that its workspace
or project exists; after authentication, the client resolves the slug only against the caller's
authorized dashboard payload. Unknown sections return `404`, and trailing slashes redirect to their
canonical URL while preserving the query string.
The overview shows:
- projects and current supervisor state;
- requests, 5xx error rate, and known ingress bytes for the last 24 hours;
- enforced project capacity across the account's organizations; and
- a searchable project list with request and p95-latency summaries.
Selecting a project switches the desktop sidebar to contextual project navigation with an explicit
return to **All projects**. Phone and tablet layouts switch to an off-canvas sidebar with a
click-away backdrop; the closed drawer stays out of both the keyboard and accessibility trees.
Project sections become a complete two- or three-column tab grid, so every destination remains
visible without horizontal scrolling. Dense deployment, backup, and operator tables become
labelled record cards, domain instructions and runtime logs wrap safely, and interactive controls
use at least 44-pixel touch targets. Summary cards stay in two columns on ordinary phones and
collapse to one column on extra-narrow screens.
Each project URL loads shared project metadata plus only the selected view's bounded dataset. For
example, opening Performance requests metrics but does not also download domains, releases,
backups, and logs.
The workspace Activity view shows append-only API history across every organization where the current owner, administrator, or developer role permits audit access. Events identify their action, target, actor, timestamp, and expandable safe metadata. Pagination uses a descending event-ID cursor, so concurrent new events do not duplicate or skip older pages. Deleted projects remain named and visibly marked as deleted.
The **People** view creates and switches between the account's workspaces, then shows the current role, members, pending invitations, and project usage. The creation control reflects the transactionally enforced owned-workspace quota and selects the new workspace immediately. A signed-in recipient can paste an email-bound token to join without using the CLI. Owners and administrators can invite by email, copy the single-use token once, revoke pending invitations, change roles, remove collaborators, or leave when last-owner protection permits. Pending invitation addresses are hidden from developers and viewers, including email redaction in developer activity metadata; disabled controls reflect server capabilities, but every operation is authorized again by the API.
Allowlisted platform operators see a separate **Control plane** view. Its range-selectable global
traffic histogram, capacity totals, account growth, and top-project table aggregate the complete
installation. The cursor-paginated account directory exposes only operational identity and usage
metadata and supports bounded email/name search. CLI bearer tokens cannot open these APIs.
An operator may start a 15-minute read-only support session from an eligible account row after
entering a reason and the exact target email. The target's authorized dashboard becomes visible,
but every server-side mutation, identity control, device approval, and operator API is denied. A
persistent warning identifies both accounts, the reason, and expiry. Exit restores the real
operator, and sign-out revokes the support session before ending the account session. Operators
cannot impersonate themselves, disabled accounts, or other platform administrators, and both start
and stop events remain attributed to the real operator.
The operator view keeps the latest 25 start/stop records visible with the real operator, target,
reason, timestamp, and planned expiry; the durable audit table remains the source of truth.
Each project has performance, deployments, domains, logs, backups, and settings views. The project header shows its production URL, runtime state, workspace, and current role. Performance identifies the active production release, presents the exact `clank deploy` next step when no release exists, and supports `15m`, `1h`, `24h`, `7d`, and `30d` metric windows. It includes the complete time window (including idle buckets), previous-period changes, p50/p90/p95/p99 and maximum latency, peak request rate, status classes, HTTP methods, non-overlapping latency buckets, and request/response byte detail. Domains walks through ownership, routing, and TLS eligibility independently so a DNS failure is not reported as a certificate failure. Deployments shows enforced artifact count/byte usage and can remove inactive runtime files while retaining release metadata and audit history. Backups exposes the automatic cadence and next run, retained encrypted restore points, last scheduler failure, and manual create/verify controls. Settings makes operational identity and database provisioning state visible, then gives owners and administrators a confirmation-gated way to permanently delete the project and reclaim its quota. A project without a first deployment reports that its isolated database will be provisioned on first deploy instead of failing the whole screen.
The dashboard uses the same organization membership and project-permission checks as the CLI. API values are inserted with DOM `textContent` or attributes rather than HTML parsing. The page has a nonce-bound script, restrictive CSP, no-store responses, framing denial, and no third-party assets.
## Enforced capacity
Limits are operator configuration, not cosmetic dashboard values:
| Environment variable | Default | Enforcement boundary |
| --- | ---: | --- |
| `CLANK_MAX_ORGANIZATIONS_PER_ACCOUNT` | `5` | Account-owned organization count and insert in one SQLite transaction |
| `CLANK_MAX_PROJECTS_PER_ACCOUNT` | `10` | Account-owned project count and insert in one SQLite transaction |
| `CLANK_MAX_PROJECTS_PER_ORGANIZATION` | `10` | Project count and insert in one SQLite transaction |
| `CLANK_MAX_DOMAINS_PER_PROJECT` | `5` | Domain assignment and count in one SQLite transaction |
| `CLANK_METRICS_RETENTION_DAYS` | `30` | Minute ingress-metric retention |
| `CLANK_MAX_RELEASES_PER_PROJECT` | `50` | Available release-artifact count (valid range 2–100) checked under the distributed project lock |
| `CLANK_MAX_RELEASE_STORAGE_BYTES_PER_PROJECT` | `21474836480` | Uncompressed release files and pre-deploy snapshots checked under the same lock |
The API returns `ORGANIZATION_LIMIT_REACHED`, `ACCOUNT_PROJECT_LIMIT_REACHED`, `PROJECT_LIMIT_REACHED`, or `DOMAIN_LIMIT_REACHED` with HTTP `409` when capacity is exhausted. Account limits count organizations created by the account and projects owned by the account; joining someone else's organization does not consume the invitee's creator quota. A pending or verified hostname belongs to exactly one project; another project cannot replace its challenge. The console hostname, custom-domain target, base domain, and the base-domain application namespace are reserved.
These are fixed installation-wide ceilings today. Account and organization limits are checked in the same SQLite write transactions as their inserts. Release limits are rechecked while holding the project's durable cross-control-plane lease, so concurrent deploys cannot bypass capacity. A hosted service can later resolve plan-specific limits before entering those transactions; billing state must never be the only enforcement layer.
A successful permanent project deletion immediately releases the account/organization project count, slug, application port, and custom-domain assignments. The deletion path uses the same durable per-project lock as deployment and backup work, then removes platform-managed storage and control metadata. Exact confirmation and explicit data-loss acknowledgement are required; only an owner/admin account session or account-wide token can perform it.
## Release storage lifecycle
Each available release counts its extracted file sizes plus its pre-deploy SQLite rollback snapshot. The compressed upload size remains visible separately for protocol accounting. A deployment is rejected with `RELEASE_LIMIT_REACHED` or `RELEASE_STORAGE_LIMIT_REACHED` before activation when it would exceed either ceiling.
The active release cannot be cleaned. Removing the active release's immediate predecessor removes the one-click code target and the active release's matching pre-deploy data snapshot, so the API requires both the exact confirmation text and an explicit rollback-loss flag. Other inactive, failed, or crashed artifacts need the same exact confirmation but no rollback-loss override. Cleanup requires `rollback` permission and removes only runtime files and release-local rollback material; immutable release metadata, logs, and audit evidence remain.
On upgrade, pre-existing releases initialize storage accounting from their recorded upload bytes because older rows did not retain extracted-size totals. New deployments record the exact extracted file total and actual snapshot size. Operators that need exact accounting for old releases can clean obsolete artifacts or redeploy them.
## Ingress metrics
Metrics are recorded only for requests that pass through managed ingress. Clank stores one row per project and minute with:
- request count and 2xx/3xx/4xx/5xx counts;
- 5xx error count;
- latency sum, maximum, and cumulative `50`, `100`, `250`, `500`, `1000`, `2500`, `5000`, and `+Inf` millisecond buckets; and
- request bytes plus response bytes when the upstream declares `Content-Length`; and
- fixed counters for `GET`, `HEAD`, `POST`, `PUT`, `PATCH`, `DELETE`, `OPTIONS`, and every other method.
Reported percentiles are upper bounds of matching fixed buckets, not precomputed quantiles. That makes buckets safely aggregatable across time, following the [Prometheus histogram model](https://prometheus.io/docs/practices/histograms/). Method names are normalized into eight bounded counters. Rows created before method counters were introduced appear as `OTHER`, preserving totals through the automatic schema upgrade. The persisted series has no path, hostname, IP, email, user, query-string, or user-agent labels, avoiding high-cardinality and personal-data growth. The HTTP names and duration/size concepts follow the [OpenTelemetry HTTP metric conventions](https://opentelemetry.io/docs/specs/semconv/http/http-metrics/).
The project API returns a fixed number of chart buckets for the complete selected window, a summary, and the equally sized preceding window. Percentage changes use the previous value as their denominator; a new nonzero series with no preceding value is reported as `null` rather than an infinite percentage. Server-error changes are returned as percentage-point differences. The smallest window is 15 minutes at one-minute resolution; longer windows downsample to keep responses and rendering bounded.
Latency currently measures ingress receipt through upstream response headers. It does not measure completion of a streamed body. Response bytes are zero when their final size is not declared. Application-level business metrics and end-to-end traces remain separate; see [Observability](observability.md).
## Custom-domain lifecycle
Set an edge target before allowing custom domains:
```sh
export CLANK_INGRESS=1
export CLANK_INGRESS_BASE_DOMAIN=apps.example.com
export CLANK_CUSTOM_DOMAIN_TARGET=edge.apps.example.com
export CLANK_CUSTOM_DOMAIN_ADDRESSES=192.0.2.10,2001:db8::10
export CLANK_TLS_ASK_TOKEN="$(openssl rand -hex 32)"
```
Clank tracks three separate states:
1. **Ownership** — the customer publishes the exact random TXT value at `_clank.` and asks Clank to verify it.
2. **Routing** — the hostname must resolve through the configured CNAME target or to one of the configured edge addresses.
3. **Certificate** — a verified, correctly routed hostname on a deployed site becomes eligible for the edge certificate manager.
For a subdomain such as `tasks.customer.example`, prefer:
```text
tasks.customer.example. CNAME edge.apps.example.com.
```
At a zone apex, use provider-supported CNAME flattening/ALIAS/ANAME or the displayed A/AAAA addresses. A conventional CNAME cannot coexist with other data at the same owner name under [RFC 1034](https://www.rfc-editor.org/rfc/rfc1034). If the customer publishes restrictive CAA records, the chosen ACME issuer must also be authorized; see [RFC 8659](https://www.rfc-editor.org/rfc/rfc8659.html).
DNS checks use the host's recursive resolver and may reflect resolver caching. Clank checks routing when a domain is added, when ownership is verified, when **Check DNS** or its API equivalent is requested, and through a background reconciler every five minutes by default. The reconciler claims a bounded batch in SQLite, uses expiring lease tokens so multiple control planes do not duplicate the same check, limits concurrent lookups, applies a per-domain deadline, and schedules the next observation only after the current claim settles. The Domains UI and API expose whether automation is enabled, its cadence and bounds, the last completed pass, and the number of successful or failed checks.
Set `CLANK_DOMAIN_RECHECK_INTERVAL_MS=0` to disable automation, or tune the interval, batch size, and timeout with the variables documented in [Self-hosting](self-hosting.md). Manual checks and automatic checks use the same routing state transition. A stale or timed-out leased result cannot overwrite a later manual check.
Ownership verification remains an explicit action because it consumes the one-time TXT challenge and creates a durable trust decision. Once verified, ownership remains verified after the TXT record is removed, matching common SaaS onboarding behavior. Removing the domain from Clank releases the assignment and stops new routing immediately. An edge may retain already-issued certificate material until its normal cache lifecycle removes it.
## Self-hosted TLS with Caddy
Caddy is the recommended zero-Node-package edge for a self-hosted installation. It supports unknown customer hostnames with On-Demand TLS, caches certificates, renews them in the background, and requires an authorization endpoint to prevent issuance abuse. Caddy explicitly recommends a fast indexed lookup with no DNS or other network work in that endpoint; Clank's permission route does exactly that. See [Caddy's On-Demand TLS documentation](https://caddyserver.com/docs/automatic-https#on-demand-tls).
Example `Caddyfile`:
```caddyfile
{
servers {
strict_sni_host on
}
on_demand_tls {
ask http://127.0.0.1:4200/_clank/tls/ask?token={$CLANK_TLS_ASK_TOKEN}
}
}
deploy.example.com {
reverse_proxy 127.0.0.1:4200
}
https:// {
tls {
on_demand
}
reverse_proxy 127.0.0.1:4200
}
```
The permission endpoint returns `200` only for either:
- a deployed `slug.` site; or
- a deployed custom domain whose ownership is verified and routing is ready.
Every other name receives a non-2xx response. Keep the Clank listener and permission route on loopback or a private network, require the high-entropy token, disable debug logging in production because the permission URL contains it, and persist and back up Caddy's data directory. `strict_sni_host on` rejects a TLS SNI/HTTP Host mismatch instead of permitting domain fronting. Caddy documents both the permission endpoint and this server option in its [global options reference](https://caddyserver.com/docs/caddyfile/options#on-demand-tls).
Point `deploy.example.com`, `edge.apps.example.com`, and `*.apps.example.com` at the edge. The console gets its normal explicit certificate; built-in and customer hostnames are authorized on demand. Test with an ACME staging endpoint before production to avoid CA rate limits.
At larger multi-region scale, put a managed SaaS-domain edge in front and adapt its status/webhook API to the same three Clank states. For example, Cloudflare for SaaS separately exposes hostname ownership and certificate validation, and describes production readiness as an active hostname, active SSL, and DNS pointing to the SaaS target. See its [hostname validation model](https://developers.cloudflare.com/cloudflare-for-platforms/cloudflare-for-saas/domain-support/hostname-validation/). The open-source default stays provider-neutral and does not require customers to delegate their DNS zone.
## Browser/API endpoints
- `GET /api/dashboard` — account, organizations, enforced limits, projects, 24-hour summaries, and domain-edge configuration.
- `GET /api/admin/analytics?range=24h` — browser-platform-admin-only global capacity, traffic, account growth, and top-project summaries.
- `GET /api/admin/users?limit=50&before=&query=` — browser-platform-admin-only redacted account directory.
- `POST /api/admin/impersonation` — recent-auth platform administrator starts a reason-bound read-only support session after exact email confirmation.
- `DELETE /api/admin/impersonation` — revoke the current browser-session-bound support session.
- `GET /api/audit?limit=100&before=&organizationId=` — role-filtered, cursor-paginated workspace activity including deleted projects.
- `POST /api/organizations` — create an owned workspace under the account quota.
- `GET /api/organizations/:id` — workspace capabilities, member roster, and administrator-only active invitation metadata.
- `POST /api/organizations/:id/invitations` — create or replace an email-bound, single-use invitation.
- `POST /__clank/auth/invited-register` — create an invitation-bound account and consume its workspace membership in one flow.
- `DELETE /api/organizations/:id/invitations/:invitationId` — revoke an active invitation.
- `PATCH /api/organizations/:id/members/:userId` — change a member role with last-owner and owner-only protections.
- `DELETE /api/organizations/:id/members/:userId` — leave or administratively remove membership and revoke workspace-scoped credentials.
- `GET /api/projects/:id` — project status, usage, current organization role, and whether the principal may delete the project.
- `DELETE /api/projects/:id` — owner/admin-only permanent deletion with `{ "confirmation": "delete-site ", "acknowledgeDataLoss": true }`.
- `GET /api/projects/:id/metrics?range=15m|1h|24h|7d|30d` — bounded current/previous summaries, comparisons, and complete downsampled points.
- `GET /api/projects/:id/domains` — ownership, routing, observed DNS, TLS state, project limit, and reconciliation status.
- `POST /api/projects/:id/domains` — reserve a hostname and create the ownership challenge.
- `POST /api/projects/:id/domains/:domainId/verify` — verify TXT ownership and refresh routing.
- `POST /api/projects/:id/domains/:domainId/check` — refresh routing without changing ownership.
- `DELETE /api/projects/:id/domains/:domainId` — remove and release the hostname.
- `GET /api/projects/:id/backups` — safe backup metadata and durable scheduler status; host paths are omitted.
- `POST /api/projects/:id/backups` — create and verify an encrypted restore point.
- `POST /api/projects/:id/backups/:backupId/verify` — decrypt, authenticate, checksum, and integrity-check a restore point.
- `POST /api/projects/:id/backups/:backupId/restore` — confirmation-gated restore with an automatic safety copy.
- `GET /api/projects/:id/releases` — release history, artifact availability, cleanup protection, and count/byte usage.
- `DELETE /api/projects/:id/releases/:releaseId` — confirmation-gated inactive artifact and rollback-snapshot cleanup.
- `GET /_clank/tls/ask?token=…&domain=…` — private Caddy permission lookup; not a customer API.
Browser mutations require same-origin session authentication and CSRF. CLI calls use bearer tokens, whose organization role and project scope are re-evaluated on every request.
---
# Organizations and deployment RBAC
Canonical URL: https://docs.clank.run/docs/organizations
Raw source: https://docs.clank.run/raw/organizations.md
The Clank platform isolates projects through organizations. A project belongs to one organization, and every request checks current membership before reading project metadata, logs, releases, secrets, or deployment state.
Roles are intentionally small:
| Role | Access |
| --- | --- |
| `owner` | Full organization and project administration, including permanent site deletion |
| `admin` | Project administration, permanent site deletion, and membership administration, except changing owners |
| `developer` | Read, deploy, rollback, and audit access |
| `viewer` | Read-only project metadata, releases, and logs |
An organization must always retain an owner. Only owners can grant or change the owner role. Removing a member immediately removes project access and revokes every organization- or project-scoped token issued to that member.
## CLI workflow
```sh
clank org list
clank org create "Acme Engineering" --slug acme
clank org invite person@example.com --role developer
clank org accept
clank org members
clank org invitations
clank org role
clank org remove
clank org revoke-invite
clank project create "Todo" --org
clank project delete --confirm="delete-site todo" --acknowledge-data-loss
clank token create --name github-actions --permissions read,deploy
clank token list
clank token revoke
```
Invitation tokens are hashed at rest, expire after seven days by default, are bound to the invited email address, and can be accepted once. The token is returned only by the create response, so copy it immediately and deliver it through the operator's trusted channel. Listing pending invitations never returns a token or hash, and only owners and administrators receive pending-invitation metadata. Historical invitation events remain auditable, but developer activity feeds redact recipient email fields.
Reissuing an invitation for the same workspace and normalized email atomically revokes every older active token. Existing members cannot be reinvited; change their role directly instead. A workspace can retain at most 100 active invitations, and owners or administrators can revoke one before it is accepted. These mutations are recorded in workspace activity.
The browser console exposes the complete flow under **People**. An account below its owned-workspace quota can create and immediately select a workspace there. A signed-in recipient can paste a token into **Join another workspace**; the server requires the token's normalized email to match that account. A recipient without an account chooses **Use invitation** on the sign-in screen and supplies the invited email, a new password, and the token. Clank creates the account, consumes the invitation, and joins the workspace in one flow even when public signup is closed. The page also shows the current role and site usage, lets authorized users update roles or remove members, and reveals a newly created token only in the current page state. Member removal immediately revokes the removed account's organization- and project-scoped tokens. The last owner cannot leave or be demoted, and an administrator cannot grant, change, or remove an owner.
## Organization API
- `GET /api/organizations/:id` returns the organization, access capabilities, members, active invitation limit, and active invitations when the caller may administer them.
- `POST /api/organizations` creates an owned workspace with `{ "name": "...", "slug": "..." }` under the account quota.
- `POST /api/organizations/:id/invitations` creates or replaces an email-bound invitation with `{ "email": "...", "role": "developer" }`.
- `DELETE /api/organizations/:id/invitations/:invitationId` revokes an active invitation.
- `PATCH /api/organizations/:id/members/:userId` changes a role with `{ "role": "admin" }`.
- `DELETE /api/organizations/:id/members/:userId` lets a member leave or an administrator remove them, then revokes workspace-scoped credentials.
- `POST /api/invitations/accept` accepts a single-use token for the currently authenticated account.
- `POST /__clank/auth/invited-register` creates the invitation-bound account and accepts its membership without opening public registration.
Authenticated browser mutations require the session CSRF token. Invitation-assisted registration requires an allowed request origin, the exact invited email, and the single-use secret. Project-scoped tokens cannot call organization endpoints.
## Project-scoped tokens
Project tokens contain an organization ID, project ID, explicit permission set, expiry, issuer, and revocation state. Every project request checks all of:
1. the token is active and unexpired;
2. the requested project matches the token;
3. the issuing user is still an organization member;
4. the current organization role permits the operation; and
5. the token permission set permits the operation.
Available permissions are `read`, `deploy`, `rollback`, `secrets`, `tokens`, and `audit`. The default CI scope is `read,deploy`.
A stolen project token cannot list organizations, create or permanently delete projects, administer membership, read another project, or expand its own scope. With `audit`, it can read only its own project's history; current membership and role are still re-evaluated. The workspace activity feed is available to owners, administrators, and developers, while viewers receive no workspace events and cannot select an organization feed. Permanent deletion requires an account-wide token and a current owner/admin role even when a project token contains `tokens`. Account-wide device tokens remain appropriate for the interactive CLI, but should not be copied into CI.
---
# Durable distributed deployment
Canonical URL: https://docs.clank.run/docs/distributed-deployment
Raw source: https://docs.clank.run/raw/distributed-deployment.md
Clank deployment coordination is persisted in the control database. Process-local maps are still used as an optimization, but correctness is protected by authenticated leases and monotonic fencing tokens.
`openDeploymentOrchestrator` provides four durable contracts:
1. **Distributed project leases** serialize deploy, rollback, backup restore, and other destructive project operations across control-plane workers.
2. **Node sessions** authenticate deployment agents, publish region/capacity/labels, support draining, and expire without heartbeats.
3. **Desired placement state** records release, running/stopped state, assigned node, and a monotonically increasing generation.
4. **Operations** use idempotency keys, explicit queued/leased/retry/succeeded/failed states, retry timing, attempt limits, lease expiry, and fencing.
```ts
const orchestrator = openDeploymentOrchestrator(controlDatabase);
const agent = await orchestrator.registerNode({
id: "iad-node-01",
region: "iad",
capacity: 100,
labels: { runtime: "node24", isolation: "microvm" },
});
const desired = await orchestrator.setDesired({
projectId,
releaseId,
state: "running",
region: "iad",
});
const [operation] = await orchestrator.claim(agent.node.id, agent.token);
```
An agent must renew a leased operation before expiry. Completion and failure compare the node, token digest, lease expiry, and fence. If a worker resumes after another worker has reclaimed the operation, its stale completion is rejected.
Desired-state observations are generation checked. A late report for generation 4 cannot overwrite generation 5.
## Platform behavior
The built-in platform acquires a durable `project:` lease in addition to its local queue. It renews the lease during long operations and returns `PROJECT_BUSY` if another control worker owns it. A lost lease is surfaced as `PROJECT_LEASE_LOST` rather than silently claiming coordinated success.
## Agent loop
A production deployment agent should:
1. register or load its node credential;
2. heartbeat before half of the node TTL;
3. stop claiming new work while draining;
4. claim a bounded operation batch;
5. renew long-running operation leases;
6. make runtime changes using the operation fence;
7. report desired generation observations; and
8. complete or fail with a bounded, non-secret result.
Agent credentials and operation lease tokens are shown only to the worker and stored as digests. Control-plane database access remains privileged and should not be exposed to application processes.
## Failure semantics
- Duplicate API requests converge through idempotency keys.
- Crashed workers leave leased operations that become reclaimable.
- Expired nodes become offline for placement.
- Draining nodes keep current work but receive no new desired placements.
- Retry delay is exponential and bounded; exhausted operations enter `failed`.
- Node capacity is placement based, deterministic, and region aware.
SQLite coordination is suitable for multiple workers on one durable host. Multi-region control planes should bind the same orchestration semantics to the external transactional control database described in the data-plane guide.
---
# Railway production deployment
Canonical URL: https://docs.clank.run/docs/railway
Raw source: https://docs.clank.run/raw/railway.md
This profile runs the Clank control plane and its trusted application processes in one Railway
service. Railway terminates TLS and forwards every control-plane and application hostname to the
same listener; Clank then routes application hosts to their supervised loopback processes.
## Topology
```text
clank.run ───────────────┐
*.apps.clank.run ────────┼─ Railway edge ─ Clank control plane :$PORT
approved custom domains ┘ ├─ app :4300 ─ projects//data/app.sqlite
├─ app :4301 ─ projects//data/app.sqlite
└─ /data/control.sqlite
Railway volume mounted at /data
```
Use exactly one replica. Clank's control database and each application database use SQLite, and the
included application supervisor is single-leader. Horizontal replicas need the external storage,
leader, and remote-runner integrations described in [Self-hosting](self-hosting.md).
## Repository configuration
- `Dockerfile` builds the dependency-free TypeScript sources and copies only the production
control-plane runtime into the final image.
- `railway.json` selects the Dockerfile, probes storage-backed `/_clank/readyz`, gives graceful
shutdown 30 seconds, and restarts failed processes. The reserved path is checked before
application-host dispatch because Railway sends its own health-check hostname.
- A Railway volume must be mounted at `/data`.
The service needs these variables:
```sh
HOST=0.0.0.0
NODE_ENV=production
TRUST_PROXY=1
CLANK_PLATFORM_URL=https://clank.run
CLANK_PLATFORM_DATA=/data
CLANK_PLATFORM_MASTER_KEY=
CLANK_PLATFORM_ADMIN_EMAILS=operator@example.com
CLANK_SIGNUP=bootstrap
CLANK_RUNNER=process
CLANK_INGRESS=1
CLANK_INGRESS_BASE_DOMAIN=apps.clank.run
CLANK_APP_URL_TEMPLATE=https://{slug}.apps.clank.run
CLANK_CUSTOM_DOMAIN_TARGET=apps.clank.run
CLANK_MAX_PROJECTS_PER_ACCOUNT=10
CLANK_MAX_PROJECTS_PER_ORGANIZATION=10
CLANK_MAX_DOMAINS_PER_PROJECT=5
CLANK_METRICS_RETENTION_DAYS=30
CLANK_BACKUP_INTERVAL_MS=86400000
CLANK_BACKUP_MAX_COUNT=30
CLANK_BACKUP_MAX_AGE_MS=7776000000
CLANK_ALLOW_UNSAFE_MIGRATIONS=0
```
Do not put `CLANK_PLATFORM_MASTER_KEY` in source control. Back it up separately from the volume:
losing it makes encrypted secrets and recovery points unreadable.
`bootstrap` lets the first browser user create the only public account, then closes registration.
Additional people join through email-bound invitations created by an owner or administrator.
`CLANK_PLATFORM_ADMIN_EMAILS` is an exact, comma-separated operator allowlist. Matching accounts
receive the separate `platform_admin` role; removing an address revokes that role on the next
control-plane start. Global administration is available only to an interactive browser session,
never to a CLI bearer token. After signing in, an allowlisted operator can open `/admin` for
installation-wide analytics and the redacted account directory. Read-only support impersonation is
short-lived, recent-auth gated, reason-bound, visibly labeled, and audited; it is not a substitute
for limiting and protecting the operator allowlist.
## Domains
Attach both `clank.run` and `*.apps.clank.run` to the Railway service. Publish every routing, ACME,
and TXT validation record Railway returns.
`clank.run` is registered with and delegated to Vercel DNS. Replace Vercel's default apex and
wildcard routing aliases while preserving its CAA records:
- add an apex `ALIAS` targeting the hostname Railway supplies for `clank.run`;
- add the `*.apps` and `_acme-challenge.apps` CNAME records Railway supplies;
- add the `_railway-verify` and `_railway-verify.apps` TXT records Railway supplies.
The wildcard gives every project an immediate `https://.apps.clank.run` URL. Railway must also
know about a customer-owned custom domain before its edge can issue a certificate for that host.
For the current single-tenant deployment, add the hostname to the same service with:
```sh
railway domain customer.example --service clank
```
Then publish Railway's validation records plus the ownership record shown in the Clank console.
Clank will route the host only after both ownership and routing checks succeed.
## First account and CLI
1. Open `https://clank.run` and create the bootstrap owner with a unique password of at least 12
characters.
2. In a local checkout, start browser-assisted device authorization:
```sh
clank login --server https://clank.run
```
3. Create or scaffold an application and deploy it:
```sh
clank create my-app
cd my-app
clank deploy
```
`clank create` scaffolds the authenticated to-do starter by default, including its migrations and
deployment manifest.
Each project receives a separate directory and SQLite database under `/data/projects/`.
Database migrations are planned and applied by Clank during deployment; a failed health check or
migration leaves the active release unchanged.
Managed application processes receive reserved runtime values, including
`TRUST_PROXY=1`, an empty `ALLOWED_HOSTS`, and `CLANK_MANAGED_INGRESS=1`.
Together they let the Node adapter reconstruct each canonical or verified
custom-domain origin while the loopback-only ingress remains responsible for
exact host admission. Application manifests cannot override these values.
## Operations
Before each framework upgrade, download or snapshot the Railway volume and preserve the external
master key. After deployment, verify:
```sh
curl --fail https://clank.run/livez
curl --fail https://clank.run/readyz
curl --fail https://clank.run/_clank/readyz
railway logs --service clank --lines 100
```
Also verify browser session refresh, CLI device authorization, a disposable deployment, its
wildcard application URL, backup creation, rollback, and permanent deletion.
The Railway deployment uses Clank's `process` runner because hosted Railway containers do not expose
a Docker daemon. This runner is intentionally for trusted deployers: application code runs with the
same container and volume authority as the control plane. Use Clank's Docker runner or a remote
sandbox worker before opening deployment access to mutually untrusted users.
---
# Self-hosting Clank Deploy
Canonical URL: https://docs.clank.run/docs/self-hosting
Raw source: https://docs.clank.run/raw/self-hosting.md
Clank Deploy is one Node control-plane process plus one supervised process or container per active project.
## Requirements
- Node 22.16+;
- persistent local storage;
- HTTPS proxy outside loopback;
- Docker for mutually untrusted deployers;
- external master key and off-host backups for production.
## Environment
| Variable | Default | Purpose |
| --- | --- | --- |
| `PORT` | `4200` | Control-plane port |
| `HOST` | `127.0.0.1` | Listener address |
| `CLANK_PLATFORM_URL` | loopback URL | Exact public console origin |
| `CLANK_PLATFORM_DATA` | `.clank-platform` | Persistent root |
| `CLANK_PLATFORM_MASTER_KEY` | generated file | Base64/base64url 32-byte key |
| `CLANK_SIGNUP` | `bootstrap` | `bootstrap`, `public`, or `disabled` |
| `CLANK_RUNNER` | `process` | `process` or `docker` |
| `CLANK_DOCKER_IMAGE` | Node image | Pin by digest in production |
| `CLANK_APP_MEMORY` | `512m` | Container memory |
| `CLANK_APP_CPUS` | `1` | Container CPUs |
| `CLANK_APP_PIDS` | `128` | Container PID limit |
| `CLANK_APP_PORT_START` | `4300` | Port-range start |
| `CLANK_APP_PORT_END` | `4999` | Port-range end |
| `CLANK_APP_URL_TEMPLATE` | loopback with `{port}` | Public app URL pattern |
| `CLANK_MAX_ARTIFACT_BYTES` | `104857600` | Artifact limit |
| `CLANK_INGRESS` | inferred from base domain | Enable managed exact-host ingress |
| `CLANK_INGRESS_BASE_DOMAIN` | none | Built-in `slug.` site namespace |
| `CLANK_CUSTOM_DOMAIN_TARGET` | base domain | CNAME target displayed to customers |
| `CLANK_CUSTOM_DOMAIN_ADDRESSES` | none | Comma-separated edge A/AAAA values accepted for apex routing |
| `CLANK_TLS_ASK_TOKEN` | none | Secret for the private Caddy certificate permission check |
| `CLANK_INGRESS_MAX_BODY_BYTES` | `26214400` | Per-request managed-ingress body limit |
| `CLANK_DOMAIN_RECHECK_INTERVAL_MS` | `300000` | Background custom-domain routing interval; `0` disables it |
| `CLANK_DOMAIN_RECHECK_BATCH_SIZE` | `25` | Maximum domains durably claimed by one routing pass |
| `CLANK_DOMAIN_RECHECK_TIMEOUT_MS` | `10000` | Per-domain DNS lookup deadline |
| `CLANK_BACKUP_INTERVAL_MS` | `86400000` | Verified encrypted-backup cadence; `0` disables automatic runs |
| `CLANK_BACKUP_BATCH_SIZE` | `5` | Maximum projects durably claimed by one backup pass |
| `CLANK_BACKUP_MAX_COUNT` | `30` | Maximum retained backups per project |
| `CLANK_BACKUP_MAX_AGE_MS` | `7776000000` | Maximum retained backup age |
| `CLANK_BACKUP_MAX_DATABASE_BYTES` | `10737418240` | Maximum source database size accepted by backup creation |
| `CLANK_MAX_ORGANIZATIONS_PER_ACCOUNT` | `5` | Transactionally enforced account organization limit |
| `CLANK_MAX_PROJECTS_PER_ACCOUNT` | `10` | Transactionally enforced account-wide site limit |
| `CLANK_MAX_PROJECTS_PER_ORGANIZATION` | `10` | Transactionally enforced site limit |
| `CLANK_MAX_DOMAINS_PER_PROJECT` | `5` | Transactionally enforced custom-domain limit |
| `CLANK_METRICS_RETENTION_DAYS` | `30` | Ingress metric retention, 1–365 days |
| `CLANK_MAX_RELEASES_PER_PROJECT` | `50` | Retained runtime-artifact count per site |
| `CLANK_MAX_RELEASE_STORAGE_BYTES_PER_PROJECT` | `21474836480` | Uncompressed release files plus pre-deploy snapshots retained per site |
| `CLANK_ALLOW_UNSAFE_MIGRATIONS` | `0` | Operator approval for unrestricted SQL |
| `ALLOWED_HOSTS` | loopback | Exact host allowlist |
| `TRUST_PROXY` | `0` | Trust forwarded client/protocol |
`bootstrap` permits one initial account and then closes ordinary registration. Its SQLite claim is shared by control-plane processes using the same data directory. `disabled` blocks ordinary registration immediately. In both modes, an owner/admin-issued invitation can still create only its bound email account through **Use invitation**; revoke outstanding invitations before disabling all intended onboarding.
## Production start
```sh
export CLANK_PLATFORM_URL=https://deploy.example.com
export CLANK_PLATFORM_DATA=/var/lib/clank
export CLANK_PLATFORM_MASTER_KEY="$(your-secret-manager read clank-master-key)"
export CLANK_RUNNER=docker
export CLANK_DOCKER_IMAGE=node@sha256:
export CLANK_APP_URL_TEMPLATE='https://{slug}.apps.example.com'
export CLANK_INGRESS=1
export CLANK_INGRESS_BASE_DOMAIN=apps.example.com
export CLANK_CUSTOM_DOMAIN_TARGET=edge.apps.example.com
export CLANK_CUSTOM_DOMAIN_ADDRESSES=192.0.2.10,2001:db8::10
export CLANK_TLS_ASK_TOKEN="$(your-secret-manager read clank-tls-ask-token)"
export HOST=127.0.0.1
export PORT=4200
export ALLOWED_HOSTS=deploy.example.com,127.0.0.1,localhost
export TRUST_PROXY=1
clank-platform
```
Proxy the console and application hosts to port 4200. Clank performs exact-host project routing; the edge performs public DNS, TLS, WAF/rate limiting, and DDoS controls. The recommended Caddy On-Demand TLS configuration and DNS records are in [Deployment dashboard, quotas, and domains](platform-dashboard.md).
Use `/livez` only to determine whether the process can answer HTTP. Use `/healthz` or `/readyz` for
control-host readiness. Hosted load balancers that send a different `Host` header should use
`/_clank/readyz`, which is evaluated before application-host ingress. These readiness endpoints
execute a control-database probe and return `503` when the platform cannot safely accept work.
`SIGINT` and `SIGTERM` stop new HTTP work, drain an active scheduled backup, close supervised
applications and platform storage, and fail the process if shutdown cannot finish within 30 seconds.
## Tailscale
```sh
CLANK_PLATFORM_URL=https://host.tailnet-name.ts.net:8447 \
HOST=127.0.0.1 PORT=4200 TRUST_PROXY=1 \
ALLOWED_HOSTS=host.tailnet-name.ts.net,localhost,127.0.0.1 \
clank-platform
tailscale serve --https=8447 http://127.0.0.1:4200
```
Expose app ports separately or place a wildcard-capable proxy in front.
## Railway
The checked-in production image, health/restart policy, persistent-volume topology, DNS setup, and
operator runbook are in [Railway production deployment](railway.md).
## Storage
```text
control.sqlite
master.key
projects//
data/app.sqlite
releases//
backups/.sqlite
recovery//
database.enc
manifest.json
```
Use a local filesystem with correct SQLite locking/rename semantics. The platform sets umask `0077`.
Clank automatically creates authenticated, AES-256-GCM encrypted recovery points under `recovery/`. Durable control-database claims prevent duplicate scheduled work when multiple control-plane processes share the store. Release count and byte ceilings separately bound retained extracted runtime files and pre-deploy rollback snapshots. Back up the control database, completed recovery directories, recoverable artifacts/source, and master key through separate paths. Pre-release snapshots remain rollback material, not part of the recovery retention policy. See [Backup and disaster recovery](recovery.md).
Permanent site deletion removes the complete matching `projects//` tree after symlink-safe validation, then removes its control rows and revokes project tokens. It does not reach copied/off-host backups, external databases, artifact mirrors, or Caddy certificate storage. Include those systems in tenant-retention and erasure runbooks, and preserve the control database if deletion audit history must remain available.
Audit rows live in `control.sqlite`, retain organization attribution after project removal, and upgrade in place from earlier schemas. The public API does not mutate them, but SQLite administrators can. Export them to a separate append-only audit system when control-plane operator tampering or longer retention is in scope.
Authentication and CLI device-start rate-limit windows also live in `control.sqlite`. Their client/account keys are HMAC pseudonyms under the platform master key, expire automatically, and remain effective when requests move between control-plane processes or the process restarts. The table is bounded, so keep the console behind production edge rate limiting and DDoS controls.
## Upgrades
1. Back up data and key.
2. Stop new deploys and the platform.
3. Install and verify the new Clank build.
4. Start the selected active supervisor/worker topology.
5. Verify browser login, CLI login, organization and scoped-token access, project status, ingress/domain state, app health, test deploy, backup verification, rollback, and a disposable-site deletion.
Durable distributed locks, authenticated nodes, desired generations, operations/fencing, wildcard base-domain routing, ownership and routing verification, Caddy certificate eligibility, ingress metrics, enforced account/organization/site/domain limits, organization RBAC, scheduled encrypted backups, and external database drivers are implemented. The included child-process supervisor remains single-leader and artifacts/backups are local by default; a hosted multi-region service still needs leader/remote-runner integration, external object storage, globally transactional control storage, shared metric storage, and a multi-region edge service.
---
# Release process
Canonical URL: https://docs.clank.run/docs/releases
Raw source: https://docs.clank.run/raw/releases.md
Clank releases are built from reviewed source, submitted through npm trusted publishing without a long-lived registry token, and approved by a maintainer with two-factor authentication.
## One-time repository configuration
1. Make the GitHub repository public before the first public package release so npm can attach public provenance.
2. Protect `main`: require pull requests, the Node runtime checks, packaged-release conformance, conversation resolution, and a non-stale approval.
3. Protect tags matching `v*`.
4. Create the GitHub Actions environment `npm` and require a maintainer approval.
5. Sign in to npm as an owner of the `clank.run` organization, enable account-level 2FA, and confirm `@clank.run/framework` is still available.
6. Bootstrap the package because npm requires it to exist before a trusted publisher can be attached:
```bash
npm login
npm run check
npm pack --dry-run
npm publish --access public --provenance=false
```
This is the only direct, interactive publish. Inspect the tarball and package page before entering the 2FA code. The first version will not have CI provenance; every subsequent version will.
7. Configure the `@clank.run/framework` trusted publisher:
- provider: GitHub Actions;
- repository: `nearbycoder/clank.run`;
- workflow: `release.yml`;
- environment: `npm`; and
- allowed action: stage publish only.
8. With npm 11.18 or newer, the equivalent authenticated CLI command is:
```bash
npm trust github @clank.run/framework \
--repository nearbycoder/clank.run \
--file release.yml \
--environment npm \
--allow-stage-publish
```
9. Require two-factor authentication, disallow traditional publish tokens, and revoke any bootstrap token or saved npm session that is no longer needed.
10. Enable GitHub private vulnerability reporting.
The release workflow uses Node 24 with npm 11.18.0 and requests `id-token: write` only in the publish job. npm exchanges that GitHub OIDC identity for a short-lived credential and automatically produces package provenance for the public package.
The `clank` npm name belongs to an unrelated project. Do not publish or document it as the framework dependency; the brand command remains `clank`, while package imports use `@clank.run/framework`.
## Release ceremony
1. Confirm `CHANGELOG.md` contains the complete version entry.
2. Update `package.json` to the intended semantic version.
3. Run `npm run check` from a clean checkout.
4. Open and merge the version pull request.
5. Create an annotated, protected `v` tag from that merge.
6. Draft a GitHub release from the tag using the matching changelog entry.
7. Publish the GitHub release.
8. Approve the `npm` GitHub environment deployment after verifying the tag and workflow summary.
9. Download and inspect the staged npm tarball, then approve it with 2FA from npmjs.com or `npm stage approve `.
10. Verify:
- the npm package shows provenance;
- the attached `.tgz` verifies with `gh attestation verify`;
- a fresh consumer can install, scaffold, build, and run; and
- the package contains no database, credential, environment, platform-state, or unrelated generated files.
The GitHub release event runs the complete gate again, packs one tarball, creates a GitHub artifact attestation for it, attaches it to the release, and submits the same source to npm's staged-publishing queue through trusted publishing.
## Failure handling
- Do not reuse or move a published version or tag.
- Deprecate a bad npm version and publish a new patch.
- If publication identity or source provenance is questionable, revoke obsolete tokens, disable publishing, preserve evidence, and follow `SECURITY.md`.
- A GitHub attestation proves which workflow and source produced an artifact; it does not prove the source itself is vulnerability-free.
---
# Security
Canonical URL: https://docs.clank.run/docs/security
Raw source: https://docs.clank.run/raw/security.md
Clank treats browser input, agent input, URLs, cookies, request bodies, and persisted document data as untrusted. Secure defaults are applied in the framework, but deployment configuration and application authorization remain part of the boundary.
## Built-in defenses
### Rendering
- Text and attributes are escaped during SSR.
- Serialized state escapes `<`, `>`, `&`, and Unicode line separators.
- Inline `on*` attributes are rejected case-insensitively.
- `javascript:`, `vbscript:`, `file:`, non-image `data:`, SVG data images, and `srcdoc` attributes are rejected.
- Event listeners must be functions and are installed through `addEventListener`.
- Two-way binding is restricted to `value`, `checked`, `selected`, and `selectedIndex`.
- `renderDocument({ nonce })` applies a validated CSP nonce to generated boot-state and module script tags.
`dangerouslySetInnerHTML` deliberately bypasses escaping. Use it only with trusted static content or an application-selected sanitizer. Clank does not include an HTML sanitizer because safe policies depend on the tags, attributes, and URL schemes an application intends to allow.
The TSX transform is a source-to-source compiler, not a data sandbox. It deliberately preserves application-authored JavaScript and TypeScript expressions in generated modules. Compile only trusted project source, never request or database values; execute mutually untrusted generated applications inside the documented runner isolation boundary.
### Requests and RPC
- JSON endpoints require an `application/json` or `+json` content type.
- Bodies are streamed through hard byte limits and strict UTF-8/JSON decoding.
- Same-origin and Fetch Metadata checks reject cross-site state changes.
- Validation errors omit received values so secrets are not reflected.
- Production 500 responses are generic across request apps, the Node adapter, agent actions, backends, and the deployment platform. Unexpected exception text is available only to private `onError` hooks.
- Backend cache and live-query keys are partitioned by auth session.
- Request, live-argument, live-connection, and cache limits are configurable.
### Authentication and data
- Passwords use versioned scrypt hashes with random salts and optional server-only peppering.
- Session cookies are `HttpOnly`, `SameSite=Strict` by default, `Secure` on HTTPS, and use the `__Host-` prefix when possible.
- Only SHA-256 token hashes are stored in SQLite; raw session tokens exist only in cookies and the immediate response construction path.
- Authenticated mutations require a constant-time CSRF-token comparison.
- Login errors do not reveal whether an account exists or is disabled.
- Rate limits use trusted adapter identity rather than caller-selected IP headers.
- Email verification and password recovery tokens are hashed, expiring, and single-use; recovery delivery is removed from the response-timing path, and password reset revokes prior sessions.
- Email-code MFA challenges are expiring, attempt-bounded, hashed, and single-use.
- Passkeys require discoverable credentials and begin without account lookup; they verify challenge, origin, RP ID hash, user presence/verification, signature, and atomic signature-counter advancement.
- Owned tables enforce the current user in SQL reads and writes.
- Owner IDs also scope live-query invalidation, so one account's private writes do not republish another account's query.
- Disabling users, role changes, and revoking sessions close associated live streams across same-host processes.
- Document `ifVersion` checks reject stale writes instead of silently overwriting newer edits.
- Mutation writes, output validation, revision updates, and journal records share one transaction.
- Query snapshots and change metadata are immutable at runtime.
### Node and files
- The Node adapter caps headers and bodies and configures header, request, and keep-alive timeouts.
- Loopback servers allow only loopback Host values by default.
- `allowedHosts` is available for production and reverse-proxy hostnames.
- `trustProxy` is off by default.
- Static paths are URL-decoded, containment-checked, resolved through the filesystem, checked again after symlinks, and deny dotfiles by default.
- Static responses use MIME types plus `X-Content-Type-Options: nosniff`.
### Agent actions
- Inputs and optional outputs are runtime validated.
- Authorization runs before the action handler.
- HTTP calls to write/destructive actions require `x-clank-confirmation: confirmed` when the action policy is `write` or `always`.
- Semantic inspection omits password, file, hidden, and inaccessible control values.
- Agent input refuses disabled, read-only, hidden, and file controls; file upload must use an application-defined, validated action.
- Confirmation is an accident-prevention protocol, not authorization. A caller able to forge the header still needs application authentication and authorization.
### Deployment platform
- CLI login uses short-lived browser-approved device codes; raw access tokens are returned once and stored only as hashes.
- Local CLI credentials and project links are bounded, structurally validated, URL-canonicalized, owner-only, and atomically replaced; platform responses are time- and byte-bounded before strict UTF-8/JSON decoding.
- Artifacts and every contained file are SHA-256 verified before exclusive extraction.
- Paths, links, special files, sensitive dotfiles, sizes, counts, modes, and decompression output are validated.
- Builds run locally without a shell; uploaded install/build hooks are never executed by the platform.
- Secrets use AES-256-GCM and values are never returned by the API.
- SQL migration history is immutable and pending migrations are transactional.
- Safe migrations cannot modify Clank-managed SQL namespaces.
- SQLite is integrity-checked and backed up after quiescing the active app.
- Database and backup paths reject final symbolic links and use private file permissions.
- Migration, startup, or health failure restores the prior database and process.
- Code rollback is health-gated; data rollback is narrowly scoped and confirmed.
- Organization membership, role, project token scope, and project ownership are checked for every release, log, secret, token, domain, backup, audit, and rollback.
- Account, organization-site, and project-domain quotas are enforced transactionally with their inserts.
- Encrypted backup manifests and ciphertext are authenticated and verified before restore.
- Managed ingress uses exact unique hosts, constrained upstreams, bounded bodies/timeouts, hop-header stripping, safe retries, and circuits.
- Client disconnects abort proxied upstream work and cancel streamed Node responses.
See [Platform security](platform-security.md) for the runner trust boundary.
## Recommended production setup
```ts
const app = createApp()
.use(securityHeaders({
contentSecurityPolicy: [
"default-src 'self'",
"script-src 'self'",
"style-src 'self'",
"connect-src 'self'",
"img-src 'self' data:",
"base-uri 'self'",
"form-action 'self'",
"frame-ancestors 'none'",
"object-src 'none'",
].join("; "),
}))
.route("*", "*", ({ request }) => runtime.handle(request));
await serve(app, {
hostname: "0.0.0.0",
allowedHosts: ["app.example.com"],
maxBodySize: 1024 * 1024,
});
```
Also:
- terminate TLS at the app or a trusted reverse proxy;
- set `trustProxy: true` only when untrusted clients cannot reach the Node listener directly;
- set a strong `CLANK_AUTH_PEPPER` through secret management;
- keep the SQLite file and backups outside static roots with restrictive OS permissions;
- compile and serve Tailwind CSS locally in production;
- add a shared rate limiter when running more than one process;
- configure email verification, recovery, MFA, passkeys, and bot protection to match the application's risk;
- log internal exceptions through `onError` without returning them to clients;
- keep Node and Clank patched and back up the database.
- run one active built-in process supervisor per project/data directory until a remote worker/leader topology is configured;
- use Docker or stronger isolation for mutually untrusted deployers;
- supply the platform master key from external secret management;
- export scheduled backups and pre-release snapshots off-host.
## CSP nonces
Generate a fresh unpredictable nonce for every HTML response:
```tsx
const nonce = crypto.randomUUID().replaceAll("-", "");
const page = await renderDocument(view, {
nonce,
state,
scripts: ["/app.js"],
});
return html(page, {
headers: {
"content-security-policy":
`default-src 'self'; script-src 'self' 'nonce-${nonce}'; object-src 'none'`,
},
});
```
Any inline script supplied through `head` must receive the same `nonce` property. Avoid `unsafe-inline` for scripts.
## Reverse proxies and Tailscale
For the authenticated Todo behind Tailscale Serve:
```sh
PORT=4180 \
TRUST_PROXY=1 \
ALLOWED_HOSTS=localhost,127.0.0.1,nearbyserver.example.ts.net \
node examples/auth-todo/server.js
tailscale serve --https=8446 http://127.0.0.1:4180
```
`TRUST_PROXY=1` is safe here only because the listener remains on loopback and Tailscale is the only proxy that can reach it. `ALLOWED_HOSTS` must contain the public Tailscale DNS name so Host validation succeeds. With forwarded HTTPS enabled, Clank issues the `Secure; __Host-` session cookie.
## Reporting and audit checklist
Before release, verify:
- anonymous requests cannot call required queries or mutations;
- two accounts cannot read or mutate each other's owned rows;
- missing/wrong CSRF tokens fail;
- cross-origin writes fail;
- logout and user disabling revoke current live access;
- malformed paths and oversized bodies return 4xx, not 500;
- internal exceptions do not appear in production responses;
- static traversal, dotfile, and symlink escape attempts fail;
- CSP is present on HTML;
- cookies are `HttpOnly`, `Secure`, `SameSite`, and host-only in production;
- the complete app works in two independent browser contexts.
Clank's test suite contains executable checks for these invariants. Security is iterative: repeat this review when adding a new transport, credential type, storage backend, raw-HTML path, or deployment topology.
See the [ASVS-oriented verification map](security-asvs.md), [threat model](threat-model.md), and [chaos drills](chaos-testing.md) for release evidence and residual responsibilities.
---
# Platform security
Canonical URL: https://docs.clank.run/docs/platform-security
Raw source: https://docs.clank.run/raw/platform-security.md
The deployment platform has three principals: the browser account, an approved CLI bearer token, and deployed application code.
The control plane trusts its machine administrator and master-key holder. It does not trust uploaded paths, artifact metadata, browser input, CLI tokens, migration history, app health, or ownership claims.
Runner choice changes the code boundary:
- `process` trusts apps as much as the platform Unix user;
- `docker` is the minimum supported boundary for mutually untrusted deployers;
- hostile public multi-tenancy should use VMs/microVMs or dedicated nodes.
Never operate the process runner as a public code sandbox.
## Authentication
Browser accounts inherit Clank's scrypt passwords, hardened cookies, CSRF, generic login errors, expiry, idle timeout, verification, recovery, email-code MFA, WebAuthn passkeys, and revocation.
Password registration/login and CLI device-start throttles use atomic sliding windows in the control database, so another control-plane process or restart cannot reset them. Keys are HMAC-SHA-256 pseudonyms under the platform master key; raw client/account combinations are not stored. Expired windows are removed on use, future timestamps caused by clock rollback are conservatively clamped, and high-cardinality state is pruned from 20,000 to 18,000 keys. Keep upstream IP/account abuse controls because bounded local state can still be pressured by a distributed attacker.
Registration defaults to a race-guarded first-account bootstrap. The platform applies its policy to the same normalized auth operation as the low-level router, including repeated-slash compatibility paths. An expiring singleton claim in the control database serializes bootstrap across control-plane runtimes; a stable insertion-order check removes any losing account before its session is returned. Public signup must be enabled explicitly. Organizations include owner/admin/developer/viewer roles, invitations, last-owner protection, and project-scoped CLI tokens whose permissions are intersected with current membership on every request.
Platform administration is a separate operator authority, never an alias for a workspace
administrator. Exact normalized emails are supplied through `platformAdminEmails` or
`CLANK_PLATFORM_ADMIN_EMAILS`; the allowlist is reconciled at startup and after registration, and
removing an address demotes the account. Global user and analytics APIs require a same-origin
interactive browser session. Account-wide and project-scoped CLI bearer tokens are deliberately
denied even when they belong to an operator. The user directory returns identity, status, activity,
membership, project, and aggregate storage metadata but never password hashes, session/CSRF
secrets, token hashes, raw tokens, recovery material, passkeys, or application-database users.
Support impersonation is deliberately narrower than administrator access. Starting it requires the
operator's same-origin browser session, CSRF token, a session created within the last 30 minutes, an
8–500 character audit reason, and exact target-email confirmation. An operator cannot target
themselves, a disabled account, or another platform administrator. The opaque 15-minute capability
is stored only as a hash, bound to the operator's current browser session, and carried in a separate
`HttpOnly`, `Secure`, `SameSite=Strict` cookie. The effective target identity applies only to safe
read methods: tenant, identity, device-approval, and platform-administration mutations are rejected
server-side. A permanent console banner names the operator, target, reason, and expiry; starting and
stopping are attributed to the real operator in the audit log. Signing out first revokes the support
session. These controls reduce accidental change and token replay, but impersonation still exposes
all data that the target can read, so operators need strong account security and a documented
support-access policy.
Sign-out and an authenticated API `401` reload the console from the server instead of reusing an in-memory dashboard. This clears prior-account DOM and recomputes both session state and bootstrap availability before another identity can use the page.
Invitation tokens are email-bound, single-use, expiring, hashed at rest, and returned only by the create response. A valid token is a narrowly scoped account-creation capability even when ordinary registration is closed. The assisted route enforces the configured origin policy before token lookup, uses the normal bounded registration, rate-limit, password-validation, and scrypt path, then transactionally rechecks and consumes the invitation with membership creation. A race or membership failure deletes the new account and its cascaded session before responding; invalid, expired, revoked, mismatched, and replayed tokens receive a generic invitation error.
Reissuing for one workspace/email atomically revokes older active tokens, existing members must use the explicit role-change path, and each workspace is capped at 100 active invitations. Pending addresses are returned only to owners and administrators; developer audit responses also redact invitation-recipient email fields, including for older stored events. Revocation, acceptance, role changes, and removals are audited; removal also revokes organization/project-scoped credentials.
CLI flow follows [RFC 8628](https://www.rfc-editor.org/rfc/rfc8628/): hashed high-entropy device codes, short expiry, rate limiting, visible client identity/code, same-origin CSRF approval, throttled polling, and single use.
Bearer tokens are returned once and hashed at rest. Follow [RFC 6750](https://www.rfc-editor.org/rfc/rfc6750): TLS, no tokens in URLs/logs, revocation, and rotation. Account tokens can create or administer organizations according to membership; project tokens are restricted to one project and explicit `read`, `deploy`, `rollback`, `secrets`, `tokens`, and `audit` permissions.
## Artifact intake
Before extraction Clank bounds HTTP and gzip output; rejects unknown fields, traversal, duplicates, links, special files, sensitive dotfiles, NULs, and unsafe modes; verifies base64, sizes, every file hash, and the artifact hash; and writes exclusively inside a new release root.
The platform never runs uploaded package-install or build hooks.
## Secrets
Secret values use AES-256-GCM authenticated encryption, consistent with [OWASP secrets-management guidance](https://cheatsheetseries.owasp.org/cheatsheets/Secrets_Management_Cheat_Sheet.html).
- Supply the master key from KMS or secret management.
- Back it up separately and restrict decryption authority.
- Rotate application secrets after exposure.
- Never log secrets.
- Platform administrators and the running app can access decrypted values.
- Environment injection can be inspected by privileged host/container administrators.
The generated local key is onboarding convenience, not protection from a compromised host.
## Database and filesystem
- Projects have dedicated contained data directories.
- Platform and apps use umask `0077`.
- SQLite, WAL, backups, CLI config, and master-key files are owner-only.
- Migration history is immutable.
- Cross-database, extension, PRAGMA, and transaction-control SQL is rejected by default.
- App configuration cannot enable unrestricted SQL unless the platform operator separately opts in.
- Database paths are checked component-by-component for symlink substitution before backup or migration.
- Deploy migrations stop the app and take a pre-change snapshot; scheduled recovery points use SQLite's consistent online backup API and are encrypted and verified before publication.
- Scheduled backup work uses expiring durable claims across control planes, and public metadata omits the host database path.
- Per-project release count and byte quotas include extracted runtime files and pre-deploy snapshots; cleanup is contained to derived project/release paths.
- Active artifacts cannot be removed; cleanup requires rollback scope, and deleting the immediate rollback target requires a separate rollback-loss decision that also prunes its now-unusable matching data snapshot.
- Permanent site deletion requires an owner/admin account principal, exact slug-bound confirmation, a separate data-loss acknowledgement, and the durable project lock. Project-scoped tokens cannot invoke it.
- Site storage removal derives the directory from the validated project ID, rejects symbolic-link parents/roots, and occurs before metadata removal. Active project tokens and distributed orchestration rows are cleared, while the deletion audit event survives the project cascade.
- Audit rows carry non-cascading organization attribution. Workspace feeds join current membership, exclude viewers, scope project tokens to one project, cap pages at 200 events, and use parameterized descending-ID cursors.
- Failure restores prior data/code.
- Data rollback is narrow and explicitly confirmed.
Export completed encrypted backup directories off-host and keep the master key in a separate failure domain. Site deletion removes the platform-managed local recovery directory but cannot erase already replicated backups, external databases, copied artifacts, or edge certificate storage; operators must apply the same retention/deletion request to those systems.
The control-plane audit API has no update or delete operation, but the trusted SQLite administrator can modify local history. Replicate events to a separately administered append-only or signed log when operator tampering is part of the threat model.
## Runner hardening
Docker mode adds read-only root, dropped capabilities, no-new-privileges, non-root UID/GID, PID/memory/CPU limits, narrow bind mounts, and a constrained temporary filesystem. Runtime secret values are supplied through the Docker client's environment with name-only `-e NAME` arguments, so values do not appear in the Docker CLI process arguments. Privileged host/container administrators can still inspect runtime environment state.
Also pin image digests, patch the kernel/runtime, apply seccomp/AppArmor/SELinux, restrict network egress, protect the Docker socket, set disk quotas, isolate customer tiers, and prefer microVMs for hostile code.
## Network and scaling
- Bind control/app ports to loopback.
- Terminate TLS at a trusted proxy.
- Permit direct access only from that proxy.
- Enable proxy trust only in that topology.
- Validate allowed hosts.
- Add upstream auth/upload/request rate limits; the built-in shared limiter is a control-plane backstop, not a DDoS edge.
Distributed leases, authenticated workers, desired generations, durable idempotent operations, node draining, retries, and monotonic fences are available. The built-in child-process supervisor still keeps process ownership in memory, so run one active supervisor per project/data directory unless using a remote worker/leader integration.
Managed ingress routes only exact verified hosts to loopback or explicit allowlisted upstreams, strips hop-by-hop and `Connection`-nominated headers, bounds request bodies and timeouts, retries only safe methods, and opens failure circuits. TLS certificates, DNS automation, WAF/DDoS controls, and WebSocket proxying belong at the external edge.
Ingress constructs the upstream URL from trusted route configuration before assigning the untrusted path, preventing scheme-relative path SSRF. The Node adapter exposes request bodies as capped streams, so authentication and smaller route-level limits run without first buffering the deployment-wide artifact maximum. Bodies without `Content-Length` are stopped at the same transport and ingress limits. Metrics are project-only aggregates with fixed status, method, byte, and histogram columns; they do not create host/path/IP/user/query/user-agent label cardinality.
Custom domains require an exact random TXT ownership proof and a separate CNAME/A/AAAA routing check. A pending or verified hostname cannot move between projects, and Clank's own console, target, base domain, and base-domain namespace are reserved. Account organization/site, organization site, and project domain ceilings are enforced inside SQLite transactions.
The Caddy TLS permission route uses a high-entropy shared token and an indexed local lookup. It allows only deployed built-in hosts or deployed custom domains with verified ownership and ready routing; it never performs DNS during a TLS handshake. Keep it on loopback/private networking, persist Caddy certificate storage, avoid logging its token-bearing URL, and enable strict SNI/Host matching at the edge.
## Audit checklist
- External master key and tested off-host backup.
- Docker or stronger isolation for untrusted users.
- Explicit TLS, hosts, proxy trust, resource quotas, and image digests.
- Private token-protected TLS permission route, persistent certificate storage, and strict SNI/Host matching.
- Scheduled token/audit review.
- Destructive site-deletion drill, including off-host retention cleanup and audit review.
- Failed deploy leaves prior app healthy.
- Migration and data rollback rehearsed.
- Full browser-login, CLI-login, deploy, app, and rollback smoke test after upgrades.
- `npm run check`, CodeQL, ASVS evidence review, and the staging chaos/restore drills.
---
# Threat model
Canonical URL: https://docs.clank.run/docs/threat-model
Raw source: https://docs.clank.run/raw/threat-model.md
This model covers the Clank framework, generated authenticated applications, CLI, control plane, deployment artifact path, managed ingress, service drivers, and backup system.
## Assets
- account credentials, sessions, passkeys, recovery tokens, and CLI tokens;
- organization membership, project permissions, audit history, and deployment authority;
- application source/artifacts, migrations, secrets, databases, files, email, jobs, and webhooks;
- control-plane master keys, encrypted backups, release history, and signing/provenance data;
- availability and integrity of active application processes and routes.
## Principals
- anonymous browser or agent;
- authenticated application user;
- organization owner, admin, developer, or viewer;
- allowlisted control-plane platform administrator;
- browser account approving a CLI device;
- account-wide or project-scoped CLI token;
- deployment control-plane process;
- authenticated deployment worker;
- deployed application process/container;
- machine, container, database, DNS, email, object-storage, and TLS operators.
## Trust boundaries
1. Browser/agent to application HTTP and live-stream APIs.
2. Browser to auth, recovery, MFA, and passkey ceremonies.
3. CLI to browser-approved device flow and control-plane bearer API.
4. Artifact bytes to extraction, migration, candidate startup, and activation.
5. Control plane to application process/container and project filesystem.
6. Managed ingress to host routing and application upstream.
7. Framework to external email, file, job, webhook, database, and provisioning providers.
8. Live database to encrypted backup repository and restore target.
9. Git source to CI, attestation, GitHub release, and npm publication.
10. Trusted application source through the TSX compiler to generated executable modules.
## Primary abuse cases
| Threat | Representative attack | Principal controls | Residual responsibility |
| --- | --- | --- | --- |
| Account takeover | Credential stuffing, reset replay, stolen session, cloned authenticator | Scrypt, generic login errors, shared HMAC-keyed control-plane rate limits, single-use recovery, MFA, WebAuthn verification/counters, revocation | Upstream abuse controls, bot defense, email security, user/device risk policy |
| Cross-site action | CSRF, forged Origin, cross-site device approval | Strict cookies, CSRF token, Fetch Metadata/origin checks | Correct proxy scheme/host configuration and CSP |
| Tenant escape | Guess project/user IDs, reuse scoped token, stale membership | Owned SQL, membership/role checks, project/scope checks on every request, revocation | Domain-specific row/resource authorization |
| Privilege escalation | Admin grants excess scopes, removes last owner, uses viewer token to deploy | Role matrix, scope intersection, last-owner protection, audit | Periodic access review and separation of duties |
| Platform-admin abuse | Workspace admin assumes global authority, stolen CLI token lists tenants, stale allowlist retains access | Separate operator role, exact startup reconciliation, browser-only global APIs, same-origin sessions, bounded redacted directory | Protect operator email accounts, require strong authenticators, review the allowlist and global audit trail |
| Impersonation abuse | Operator silently edits tenant data, targets another operator, replays a support token, or denies accessing an account | Recent-auth and CSRF gate, exact target confirmation, required reason, admin/self/disabled-target denial, hashed 15-minute session-bound capability, safe-method-only effective identity, visible banner, real-actor start/stop audit | Limit the operator allowlist, alert on support sessions, require ticket-linked reasons and customer approval where policy or law requires it |
| Invitation replay or disclosure | Reuse a superseded token, create an uninvited account while signup is closed, scrape pending addresses or audit metadata, flood active invitations | Hashed email-bound single-use account-creation capability, pre-hash origin check, transactional consume/membership, failed-account cleanup, atomic replacement/revocation, administrator-only pending metadata, developer audit redaction, 100-active cap, audit | Deliver tokens through a trusted channel and protect invited mailboxes |
| Artifact compromise | Traversal, symlink, decompression bomb, digest swap, malicious install hook | Bounded deterministic bundle, path/type/mode validation, SHA-256 verification, no remote install/build hooks | Review trusted source and isolate runtime execution |
| Migration/data loss | Edited history, unsafe SQL, failed migration, destructive rollback | Immutable ledger, restricted SQL, quiesced backup, transactional apply, safety restore, confirmation | Schema review, off-host backups, restore drills |
| Secret disclosure | API response/log leak, filesystem exposure, package publication | AES-GCM, no secret reads, recursive log redaction, private umask, npm package audit | KMS, rotation, OS/operator access, provider logging |
| SSRF/proxy confusion | Attacker-chosen upstream, scheme-relative path, duplicate host, hop-header smuggling | Loopback/allowlist upstreams, target origin assigned before path, exact unique hosts, `Connection`-nominated header stripping, manual redirects | Network egress policy and trusted DNS/TLS edge |
| Domain/certificate takeover | Reassign pending hostname, spoof TXT, route elsewhere, trigger certificates for arbitrary SNI | Exact random TXT proof, immutable cross-project assignment, separate routing state, reserved namespaces, indexed TLS allow check restricted to deployed sites | Private edge link, CAA/ACME policy, certificate storage and CA monitoring |
| Worker split brain | Expired worker completes after reassignment | Authenticated leases, monotonic fences, idempotent durable operations | Highly available backing store and supervisor integration |
| Backup tampering or omission | Ciphertext/manifest alteration, duplicate schedulers, missed recovery point, restore wrong copy | AES-GCM envelope, manifest HMAC/AAD, digest/integrity checks, durable leased scheduling, bounded retention, explicit confirmation | Monitoring, separate key custody, off-host replication, restore drills |
| Destructive project action | Stolen scoped token, developer error, path substitution, partial site deletion | Owner/admin account principal, scoped-token denial, CSRF, exact slug confirmation, separate data-loss acknowledgement, durable lock, derived symlink-safe paths, token revocation, retained audit event | Account-token protection, off-platform copy deletion, legal retention policy, deletion drills |
| Audit repudiation or tenant disclosure | Hide a destructive event, read another workspace, reuse stale elevated scope | Non-cascading organization attribution, current membership/role joins, project-token intersection, bounded cursor pagination, no audit mutation API, deleted-target retention | Trusted SQLite admins can alter local rows; replicate or sign events independently when operator tampering is in scope |
| Supply-chain compromise | Mutable CI action, leaked npm token, package includes local state | Commit-pinned actions, least privilege, OIDC trusted publishing, attestation, package allowlist, zero dependencies | GitHub/npm account security and protected release environment |
| Compiler boundary confusion | Treat attacker-controlled data as TSX source or assume generated code is sandboxed | Compiler accepts project source only, performs no build-time evaluation, and emits reviewable modules | Never compile request/database values; isolate mutually untrusted app execution |
| Denial of service | Chunked oversized request, CBOR/artifact bomb, repeated retained releases, scrypt/device-code exhaustion, high-cardinality limiter keys, failing upstream, unbounded metric labels, site/domain exhaustion | Streaming byte/count/time bounds, CBOR depth/collection limits, per-project artifact count/byte ceilings, password queue, bounded durable rate-limit state, circuits, transactional quotas, fixed-cardinality metrics, leases/retries | Edge rate limits, whole-volume monitoring, compute quotas, autoscaling, capacity planning |
## Explicit assumptions
- The operating-system administrator and master-key holder are trusted.
- The process runner executes trusted applications. Use Docker or stronger isolation for mutually untrusted deployers.
- TypeScript and TSX files are trusted executable application source. The compiler is not a sanitizer for attacker-controlled data.
- TLS termination, certificate/key custody, ACME issuer policy, DDoS protection, WAF rules, and public network policy are external to the core package. Clank only decides hostname eligibility.
- An application process can read its own decrypted environment and database.
- SQLite is a strong single-node transactional default, not a globally replicated database.
- External drivers are trusted only to the authority represented by their narrowly scoped token and endpoint.
## Review triggers
Repeat this threat review when adding a credential type, raw HTML path, file parser, public protocol, proxy rule, external provider, database engine, runner, multi-node coordinator, privileged role, destructive action, or release channel.
---
# ASVS-oriented verification
Canonical URL: https://docs.clank.run/docs/security-asvs
Raw source: https://docs.clank.run/raw/security-asvs.md
Clank uses [OWASP ASVS 5.0.0](https://owasp.org/www-project-application-security-verification-standard/) as a control vocabulary for the framework, generated applications, CLI, and deployment platform. This is an engineering evidence map, not an OWASP certification and not a claim that every application built with Clank automatically satisfies ASVS.
The current target is an ASVS Level 2 posture for Clank-owned internet-facing surfaces. Application-specific business authorization, privacy, content policy, provider configuration, and infrastructure remain the deployer's responsibility.
## Release evidence
| Control area | Clank control | Automated evidence | Operator or application evidence |
| --- | --- | --- | --- |
| Encoding and injection | Escaped SSR/DOM output, executable-URL rejection, parameterized SQLite access, restricted migration SQL, shell-free build commands | DOM, SSR, migration, compiler, and security-audit tests | Review every use of raw HTML and external SQL |
| Validation and business logic | Runtime schemas, exact input objects, bounded bodies/files/artifacts, immutable migration checksums, confirmed destructive actions | Schema, forms, backend, deployment, conformance, and fuzz-oriented tests | Define domain rules and authorization for every action |
| Web frontend security | CSP nonce support, safe state serialization, typed events, no inline handler strings | SSR, DOM, server, and strict type tests | Deploy a restrictive CSP and compiled production CSS |
| API and web services | Content-type enforcement, UTF-8/JSON limits, same-origin checks, generic 500 responses, scoped tokens | Security, backend, auth, platform, and conformance tests | Configure allowed hosts, TLS, proxy trust, quotas, and edge limits |
| File handling | Containment and realpath checks, dotfile denial, symlink rejection, bounded file store, signed capabilities | Node, deploy, services, and conformance tests | Add malware/content scanning for domain-specific uploads |
| Authentication | Scrypt passwords, generic failures, session expiry/idle expiry, verification, recovery, MFA, WebAuthn passkeys | Auth and WebAuthn tests | Configure email delivery, pepper/KMS, bot defense, and recovery policy |
| Session management | Hashed tokens, `HttpOnly`/`Secure`/`SameSite` cookies, CSRF, revocation, session-aware live streams | Auth, backend, platform, and conformance tests | Use TLS everywhere and shared revocation/rate-limit stores when scaled |
| Authorization | Required server functions, owned tables, organization RBAC, project-scoped permissions, re-check on every request | Backend, auth, and platform isolation tests | Add resource and business-state authorization inside each app |
| Tokens and secrets | High-entropy bearer tokens, hashes at rest, AES-256-GCM platform secrets, redacted structured logs | Platform, observability, services, and security-audit tests | Use external secret management, rotation, least privilege, and access review |
| Cryptography | Node cryptographic randomness, scrypt, SHA-256, HMAC, AES-256-GCM, WebAuthn signature verification | Auth, WebAuthn, recovery, deploy, and service tests | Configure approved TLS and managed key lifecycle |
| Secure communication | HTTPS-only external service drivers, exact ingress hosts, hop-header stripping, bounded proxying, verified DNS routing, restricted TLS eligibility, and one-year HSTS on an HTTPS control-plane hostname | Data-plane, platform, and chaos tests | Supply TLS termination/key custody, preserve HSTS at the edge, and provide WAF and egress policy |
| Configuration | Private umask, safe defaults, explicit unsafe-migration escape hatch, immutable CI actions | Platform, deployment, security audit, CI, and CodeQL | Harden OS/container, pin images, and separate control/data authority |
| Data protection | Per-app database paths, owner-scoped documents, encrypted backups, authenticated manifests | Backend, recovery, platform, conformance, and chaos tests | Define retention, residency, deletion, classification, and off-host copies |
| Logging and error handling | Structured redacted logs, traces, metrics, health checks, bounded stored logs, generic public failures | Observability, platform, auth, and server tests | Export, alert, retain, and protect telemetry |
| Secure coding and architecture | Zero runtime dependencies, strict public types, deterministic artifacts, signed release provenance | Build, package-consumer, conformance, security audit, CodeQL | Review changes to trust boundaries and third-party service code |
## Automated gate
Run:
```sh
npm run check
```
That command builds from source, verifies the dependency contract, runs the complete test suite including chaos scenarios, exercises a packed release through the conformance lifecycle, inspects the npm allowlist for state/credential leaks, and checks immutable least-privilege GitHub workflows.
The audit fails closed when a dependency appears, a workflow action is not pinned to a commit, a sensitive file enters the package, a high-confidence credential pattern is found, or required security evidence is missing.
## Manual verification required before a public release
- Review the [threat model](threat-model.md) for every changed boundary.
- Triage CodeQL and all vulnerability reports; ship with no known critical or high-severity issue.
- Exercise the [chaos drills](chaos-testing.md) in the intended deployment topology.
- Restore a recent off-host backup into a clean environment.
- Verify TLS, cookies, CSP, host validation, rate limiting, alerting, and runner isolation against production configuration.
- Review organization roles, project tokens, secrets, audit records, and operator access.
- Record accepted residual risks and an owner/date for each one.
## Framework versus application responsibility
Clank can make safe mechanics concise, but it cannot infer whether a particular user may approve an invoice, view a medical record, refund a payment, or invite an administrator. Generated actions must still name their authorization and confirmation policy. Regulations, privacy notices, data retention, abuse response, and business continuity are deployment-specific.
---
# Code and product audit
Canonical URL: https://docs.clank.run/docs/code-audit
Raw source: https://docs.clank.run/raw/code-audit.md
Audit date: 2026-07-25
This document records what was inspected, what changed, and what remains intentionally out of scope. It is evidence for maintainers, not a claim that any framework can make every application correct automatically.
## Scope
The audit covered:
- reactive ownership, computed invalidation, DOM updates, SSR, and hydration;
- TSX public typing and event/control ergonomics;
- runtime schemas and agent contracts;
- form state, validation, submission, focus, and cancellation;
- accessible disclosure, dialog, tabs, pagination, and directives;
- semantic agent inspection and operation;
- auth, passkeys, organization RBAC, data ownership, requests, files, deployment artifacts, migrations, secrets, ingress, backup, and release supervision;
- examples, strict types, package contents, documentation, desktop rendering, and narrow viewports.
## Findings resolved
| Finding | Resolution | Evidence |
| --- | --- | --- |
| A computed first read through `peek()` did not retain its own dependencies | `Computed.peek()` now suppresses only the caller subscription while evaluating with its own observer | Core regression test |
| Boolean ARIA `false` was removed | DOM and SSR now serialize explicit `"true"`/`"false"` states | DOM and SSR tests |
| Forms required repeated ad hoc signal/error code | Added schema-aware headless forms with cancellation, focus, server errors, reset, manifests, and typed controls | Form tests and three site variants |
| Agent inspection relied heavily on custom labels | Native IDs, labels, roles, required/readonly/invalid/checked/multiple state, and placeholders are understood | Agent-surface tests |
| Semantic inspection could expose password input values | Password and file values are omitted; file input automation is refused | Agent-surface tests |
| Reusable interactive controls were application-specific | Added disclosure, modal dialog, tabs, pagination, outside-click, and autofocus primitives | UI tests and site variants |
| Common web schemas were verbose | Added email, URL, date, date-time, record, defaults, refinement, and numeric/boolean coercion | Schema tests |
| JSX intrinsic elements were effectively `any` | Added native element/property/event typing, reactive attributes, bind/ref/directive types, ARIA/data attributes, and custom-element support | Strict type tests |
| Existing examples covered mostly todos | Added commerce, SaaS dashboard, and multi-step booking applications | Browser verification |
| Concurrent build/watch output could briefly disappear | Framework and public compiler builds now replace files atomically and remove only stale outputs | Concurrent-build regression tests |
| Documented clean example URLs returned 404 | The development server now resolves trailing-slash directories to `index.html` | Browser and HTTP verification |
| Generated apps did not declare their runtime for local development and editor types | Scaffolds now depend only on their matching Clank release and include build, dev, start, and deploy scripts | Package-consumer scaffold verification |
| A refreshed authenticated deployment page still displayed the `Sign in` heading | The server-rendered and client-rendered auth card now derive their heading from the same session state | Platform regression test and browser refresh verification |
| The Proact name remained embedded across package, CLI, storage, protocol, and UI surfaces | Renamed the product to Clank with in-place data migration and narrowly scoped legacy readers | Rename compatibility tests and migrated production-state copy |
| Authentication lacked production recovery and phishing-resistant credentials | Added email verification, generic single-use recovery, bounded MFA, WebAuthn passkeys, and atomic counter advancement | Auth and synthetic WebAuthn tests |
| Project authority was account-wide | Added organizations, invitations, four roles, scoped tokens, permission intersection, and removal-time revocation | Platform RBAC and isolation tests |
| Deploy coordination was local-only | Added durable authenticated leases, fences, nodes, desired generations, idempotent operations, retry, and stale-worker rejection | Orchestration and chaos tests |
| Backups were release-local snapshots or manual recovery points only | Added encrypted authenticated backup repositories, durable scheduling, cross-control-plane claims, retention, verification, restore confirmation, safety copies, path-safe API/console surfaces, and CLI | Recovery, platform-backup, conformance, and chaos tests |
| The platform lacked a managed host/data-plane layer | Added exact-host ingress, DNS ownership challenges, external PostgreSQL transactions/migrations, and database provisioning contracts | Data-plane and platform tests |
| Release security evidence was manual | Added ASVS-oriented mapping, threat model, package/secret audit, immutable CI actions, CodeQL, chaos tests, and beta gate | `npm run check` and GitHub workflows |
| Hosted quotas could be bypassed through extra organizations and rejected domains could survive rollback | Added account organization/site limits and moved domain capacity checks into the insert transaction | Platform quota and direct SQLite row-count regressions |
| Caller IP headers, passkey start responses, and recovery delivery timing exposed authentication side channels | Bound rate limits to trusted adapter identity, switched to discoverable passkeys, and removed delivery from the response path | Authentication enumeration, spoofing, and blocked-delivery regressions |
| Binary uploads and remote SQL responses could buffer past route limits | Added shared bounded streaming readers that cancel at the first over-limit chunk | Artifact, file-service, and PostgreSQL streaming regressions |
| Disconnects did not cancel every downstream stream and Docker arguments contained secret values | Propagated cancellation through ingress/Node and changed Docker to name-only environment arguments | Ingress, Node disconnect, and fake-Docker process-argument regressions |
| Repository leak checks covered only the package and a small credential set | Expanded credential patterns and scan current repository files plus all reachable Git history | `npm run security:audit` |
| A green release did not enforce coverage or documentation/declaration integrity | Added minimum line/branch/function coverage and checked local links, guide indexing, declaration parity, and export targets | `npm run check` and `npm run docs:audit` |
| CLI state writes and control-plane responses lacked complete interruption/resource bounds | Added strict bounded config/link parsing, atomic private replacement, request deadlines, strict UTF-8 JSON parsing, and response byte caps | CLI regression tests and packaged conformance |
| `/healthz` reported a constant result and signal shutdown could skip platform cleanup after an HTTP close failure | Added storage-backed readiness, separate liveness, and bounded cleanup that attempts both layers | Platform readiness tests and conformance process shutdown |
| Repeated deployments retained unbounded extracted files and rollback snapshots | Added locked per-project count/byte ceilings plus path-derived, rollback-scoped cleanup that preserves immutable evidence | Platform quota, authorization, symlink, snapshot, CLI, and dashboard regressions |
| Enforced site quotas had no safe user-facing reclamation path | Added owner/admin-only permanent deletion with dual confirmation, scoped-token denial, durable locking, path-safe storage cleanup, metadata cascades, token revocation, and surviving audit evidence | Platform RBAC, CSRF, path substitution, runtime, storage, orchestration, quota/slug/port/domain reuse, CLI, and dashboard regressions |
| Deleted-site audit evidence survived in SQLite but became unreachable through the project API | Added durable organization attribution, old-schema backfill, current-role filtering, project-token containment, stable cursor pagination, dashboard Activity, and structured CLI output | Deletion survival, migration, RBAC/scope, pagination/input, CLI, desktop/mobile, refresh, and browser-console regressions |
| Routine workspace access administration and invitation acceptance required CLI work, while replacement invitations remained valid | Added complete browser invite/accept/member administration, CLI parity, administrator-only pending metadata, developer audit redaction, atomic invitation replacement, active-invite bounds, revocation, and existing-member rejection | RBAC, replay, email binding/privacy, cap, role/removal, CLI, CSRF, desktop/mobile, and browser-console regressions |
| Creating an additional workspace still required CLI knowledge | Added quota-aware browser creation, normalized slug preview, immediate selection, and server-authoritative validation | CSRF/quota regression, authenticated browser creation, and responsive dialog verification |
| Default bootstrap made invitations unusable for collaborators who did not already have an account; simultaneous control planes lacked a durable first-account claim; and platform/auth path normalization differed | Added email-bound invitation-assisted registration with rollback cleanup, an expiring SQLite bootstrap claim with insertion-order fallback, and one normalized signup-policy operation | Disabled/bootstrap, repeated-slash/legacy path, origin, expiry, mismatch, replay, account-cleanup, and two-runtime concurrency regressions |
| Reusing the console after sign-out could retain the prior account's rendered People data and stale bootstrap button state | Reload the server-rendered console on sign-out or session expiry so identity, policy, and DOM state are rebuilt together | Same-browser owner-to-invitee onboarding, refresh, role/privacy, and console-error verification |
| Platform auth and device-start throttles reset per process, while successful login cleared a malformed key | Added one atomic HMAC-keyed SQLite sliding-window store shared by auth and device onboarding, corrected successful-login clearing, and bounded high-cardinality retention | Framework reset, cross-runtime accumulation, restart persistence, raw-identity absence, clock rollback, 429, and 20k-key pruning regressions |
| Generated auth pages rendered correctly but mapped `clank` while compiled browser modules imported the framework package, preventing hydration | Aligned both scaffold import maps with `@clank.run/framework` and added generated-source regression coverage | Fresh scaffold registration and two-browser live-sync verification |
| CLI help could execute a command, offline dry-runs required auth, unknown options were ignored, and async failures had inconsistent exit behavior | Added command-aware help, strict option validation and suggestions, reliable async error handling, offline dry-runs, readiness diagnostics, and structured agent output | CLI regression tests, local-checkout consumer, packaged conformance, and fresh-app browser review |
## Readability decisions
- New behavior is split into focused `forms.ts` and `ui.ts` modules.
- Public controllers are headless and return ordinary props.
- Runtime schemas remain the source for TypeScript and agent contracts.
- IDs are deterministic for SSR, hydration, accessibility, and agent operation.
- Unsafe or unknown form keys throw rather than fail silently.
- Examples use domain-specific names and semantic native HTML.
Large existing modules such as `backend.ts`, `auth.ts`, and `platform.ts` remain cohesive but substantial. Splitting them without changing their public boundaries is future maintainability work; a mechanical split was not treated as inherently safer than tested cohesive code.
## Security posture
The existing security boundaries remain:
- bounded request and artifact intake;
- executable URL and inline-handler rejection;
- safe SSR escaping and serialized state;
- scrypt password hashing, CSRF, secure cookies, rate limits, roles, and revocation;
- owned SQLite rows and auth-partitioned live queries;
- traversal/symlink defenses;
- encrypted deployment secrets;
- immutable migration history, backup, health-gated activation, and rollback.
The audit added semantic password/file redaction and stricter form-key handling. Client forms and hidden UI are never authorization boundaries.
The 0.7.0 rename also preserves existing accounts, sessions, application rows, migration history, projects, releases, secrets, logs, and audit records. Clank writes only the new names after migration. See [Renaming from Proact](renaming-from-proact.md).
## Application coverage
The current examples prove different mechanics:
- `hello`: reactive primitives and agent actions.
- `todo`: keyed client CRUD.
- `fullstack`: SSR, SQLite, RPC, and live synchronization.
- `auth-todo`: sessions, user-owned data, SSR, and multi-tab live updates.
- `commerce`: search/filter/sort, cart state, modal checkout, validation, and async confirmation.
- `dashboard`: tabs, metrics, responsive navigation, tables, filtering, pagination, invite dialog, and settings.
- `booking`: multi-step composition, cross-field dates, room selection, computed pricing, guest validation, and confirmation.
These examples demonstrate framework breadth. They do not replace domain-specific payment, tax, inventory, medical, legal, or regulatory integrations.
## Known limits
- Form paths are intentionally top-level. Compose controllers for nested editors and independent wizard steps.
- A capability-gated local file store and upload endpoint are included for trusted single-host deployments; production object storage, CDN delivery, and image transformation remain provider integrations.
- No virtualized list is included yet; large datasets should page server-side.
- Dialogs are rendered in place rather than through a portal.
- The built-in process supervisor remains single-leader even though durable distributed coordination primitives are available.
- The trusted process runner is not a sandbox; use the Docker runner for stronger isolation.
- Clank now provides verified domain eligibility for Caddy On-Demand TLS, but certificate/key custody, WAF/DDoS service, WebSocket ingress, remote worker integration, and globally distributed control storage remain external or future platform work.
- Tailwind's browser build is suitable for examples and zero-install prototyping; production applications should serve compiled CSS.
## Release gate
A release is acceptable only after:
1. the zero-dependency TS/TSX syntax-lowering build succeeds;
2. checked-in declarations match built declarations, package export targets resolve, and local documentation links remain valid;
3. all unit and end-to-end tests pass above the enforced 80% line, 65% branch, and 80% function coverage floors;
4. package contents contain no databases, environment files, credentials, or platform state;
5. fresh package consumers can scaffold and build;
6. representative applications pass browser interaction, console/error, accessibility-tree, and responsive-layout checks;
7. `npm run conformance` passes against a packed release through browser auth, CLI device authorization, live synchronization, user isolation, deployment, migration, failed activation, rollback, and data restoration;
8. `npm run security:audit` verifies dependency, package-content, current-tree and reachable-history credential patterns, governance, least-privilege, immutable-action, OIDC, and evidence requirements; and
9. deterministic chaos tests prove worker reclaim/fencing, corrupt-backup fail-closed behavior, and ingress recovery.
Clank deliberately does not install a TypeScript package. Its built-in compiler validates syntax lowering, while the checked-in declarations define the consumer contract. Run `tsc --noEmit` as an additional semantic type check when a separately provisioned, trusted TypeScript compiler is available; do not describe that optional external tool as part of the zero-dependency gate.
See `docs/security.md` and `docs/platform-security.md` for the separate security checklists.
---
# Chaos and failure testing
Canonical URL: https://docs.clank.run/docs/chaos-testing
Raw source: https://docs.clank.run/raw/chaos-testing.md
Clank tests failures as state-machine behavior, not only happy-path output. The deterministic chaos suite runs under Node's test runner as part of every `npm test` and `npm run check`.
## Automated scenarios
| Fault | Expected invariant | Evidence |
| --- | --- | --- |
| Deployment worker disappears after claiming work | Expired work is reclaimed with a higher fence; the stale worker cannot commit | `tests/chaos.test.mjs`, orchestration tests |
| Encrypted backup is corrupted | Authentication fails before replacement; the live database remains unchanged | Chaos and recovery tests |
| Application upstream becomes unreachable | Requests fail generically, the circuit opens, and a later probe recovers | Chaos and data-plane tests |
| Candidate startup/health fails | Prior data and active release are restored | Platform tests and packaged conformance |
| Migration or artifact is malformed | Intake fails before activation and does not escape its project boundary | Migration, deploy, platform, and conformance tests |
| Concurrent/stale state is written | Version/fence/idempotency checks reject the stale operation | Backend, jobs, orchestration, and platform tests |
| Credential payload is hostile | Body, base64url, CBOR collection, and nesting limits fail closed | Security, auth, and WebAuthn tests |
Run only the deterministic chaos file after building:
```sh
npm run build
node --test tests/chaos.test.mjs
```
Run the release-level lifecycle:
```sh
npm run conformance
```
## Staging drills
Before public beta and at least quarterly:
1. Kill a deployment worker after it claims an operation; verify another worker reclaims it and the old fence cannot commit.
2. Stop the active app during traffic; verify health alerts, bounded failures, restart policy, and ingress recovery.
3. Publish an artifact whose health check fails after a pending migration; verify data and code return to the previous release.
4. Restore an encrypted off-host backup into a clean directory with the original key; compare integrity, revision, migrations, and representative application queries.
5. Remove access to email, object storage, webhook targets, and external database APIs; verify timeouts, retries, idempotency, dead letters, and redacted logs.
6. Drain a node and expire its heartbeat; verify desired placement is reassigned without accepting a stale observation.
7. Rotate a project secret and scoped CLI token; verify the prior values stop working and audit records remain readable.
8. Simulate disk-full and read-only filesystem conditions in an isolated environment; verify no partial release is activated.
## Safety rules
- Never run destructive drills against the only copy of production data.
- Capture the exact version, topology, fault injection, expected invariant, observed recovery time, and follow-up owner.
- Use synthetic accounts and credentials.
- Treat an unexpected successful stale write, cross-tenant read, unauthenticated restore, plaintext backup, or secret log entry as a release blocker.
- Keep drills deterministic in CI; reserve network partitions, process kills, storage faults, and regional failures for staging.
---
# Packaged-release conformance
Canonical URL: https://docs.clank.run/docs/conformance
Raw source: https://docs.clank.run/raw/conformance.md
`npm run conformance` proves the complete Clank golden path using a clean temporary installation of the package produced by `npm pack`.
The runner does not import framework source from the repository. It:
1. packs Clank and installs the tarball into a clean tool consumer;
2. statically parses the packaged Todoist-style AI blueprint and generates an authenticated application using that installed CLI;
3. installs the same tarball into the generated application and builds it;
4. starts the packaged deployment platform;
5. creates a browser account and completes the real CLI device-authorization flow;
6. deploys the generated application through the packaged CLI;
7. creates two independent authenticated sessions and proves live SSE synchronization;
8. proves a separate account cannot read the first account's owned rows;
9. deploys an immutable second migration and verifies the resulting SQLite history;
10. forces a failed health activation and proves the prior application and data remain available;
11. rolls back code and restores the pre-migration snapshot; and
12. verifies both application rows and migration schema returned to the expected state.
The test uses loopback HTTP, temporary owner-only directories, isolated CLI credentials, a one-port application range, and no registry downloads beyond the local tarball.
`npm run check` runs this suite after the build, zero-dependency check, coverage-enforced unit/integration tests, and documentation/declaration audit, then finishes with the security audit. A release is not acceptable if any stage fails.
---
# Backup and disaster recovery
Canonical URL: https://docs.clank.run/docs/recovery
Raw source: https://docs.clank.run/raw/recovery.md
Clank distinguishes deployment rollback from database recovery:
- a release rollback changes application code and can optionally restore the immediately preceding migration snapshot;
- a recovery backup is an independently retained, encrypted, integrity-verified SQLite snapshot.
## Application API
```ts
import { openBackupManager } from "@clank.run/framework/recovery";
const backups = await openBackupManager({
databasePath: "app.sqlite",
repositoryDirectory: "/srv/clank/backups/orbit-tasks",
encryptionKey: process.env.BACKUP_KEY!,
maxBackups: 30,
maxAgeMs: 90 * 24 * 60 * 60 * 1_000,
verifyAfterCreate: true,
});
await backups.create({ reason: "scheduled" });
backups.start(6 * 60 * 60 * 1_000);
```
Each `clank-backup/1` record contains:
- a transactionally consistent SQLite snapshot;
- AES-256-GCM encrypted database bytes;
- authenticated manifest metadata;
- plaintext size and SHA-256 for restore verification;
- database revision and migration position;
- key ID, reason, and creation time; and
- retention metadata.
Logical manifests are HMAC authenticated and are also bound as AEAD additional data. Restore decrypts to a private temporary file, verifies the plaintext digest and byte count, runs SQLite `integrity_check`, and only then replaces the stopped database.
`restore` requires the exact confirmation `restore `.
## Platform workflow
Clank Deploy creates and verifies an encrypted backup for every deployed project every 24 hours by default. The schedule is durable: due work and expiring lease tokens live in the control database, so multiple control-plane processes cannot back up the same project concurrently. Existing databases are due when automation first starts; a newly deployed database is due after one interval. Manual backups reset the next scheduled time.
```sh
clank backup create --reason "before bulk import"
clank backup list
clank backup verify
clank backup restore \
--confirm="restore-backup "
```
Before a platform restore, Clank creates and verifies a safety backup of the current database, stops the application, restores the requested backup, and restarts the active release. If restore or restart fails, it attempts to restore the safety backup before reporting failure.
Backup creation, verification, and restore are audited. The `rollback` project permission is required for mutations; read access can list backup metadata.
The deployment console's **Backups** view shows the cadence, next run, active work, last failure, retained restore points, and manual create/verify actions. Host filesystem paths are deliberately omitted from browser and CLI responses. Unexpected scheduler errors go only to the operator error callback; users receive a fixed failure status without exception text.
Platform retention defaults to 30 backups and 90 days per project. Configure it with:
| Variable | Default | Purpose |
| --- | ---: | --- |
| `CLANK_BACKUP_INTERVAL_MS` | `86400000` | Automatic cadence; `0` disables scheduling |
| `CLANK_BACKUP_BATCH_SIZE` | `5` | Maximum projects claimed in one pass |
| `CLANK_BACKUP_MAX_COUNT` | `30` | Maximum retained backups per project |
| `CLANK_BACKUP_MAX_AGE_MS` | `7776000000` | Maximum retained age |
| `CLANK_BACKUP_MAX_DATABASE_BYTES` | `10737418240` | Maximum source database size |
Disabling automation does not disable manual backup, verification, or restore. It also does not delete existing restore points.
## Recovery objectives
Operators should set and test explicit objectives:
- **RPO**: backup interval plus replication delay;
- **RTO**: detection, backup selection, decrypt/verify time, and application restart time;
- **retention**: enough restore points to cover delayed discovery;
- **key recovery**: backup keys must be stored separately from the backup repository; and
- **failure domain**: copy encrypted backup directories to storage outside the application host and region.
Run `verify` automatically and perform recurring restore drills into a temporary environment. A backup that has never been decrypted and opened is not a proven recovery point.
The built-in repository is local. A remote repository can synchronize completed backup directories because a backup becomes visible only after its encrypted envelope and authenticated manifest are complete. Replicate `projects//recovery/` off-host; the local schedule alone does not protect against loss of the platform disk or master key.
---
# Maintenance and release certification
Canonical URL: https://docs.clank.run/docs/maintenance
Raw source: https://docs.clank.run/raw/maintenance.md
This is the repeatable maintainer checklist for substantial framework or platform updates. It keeps the zero-dependency promise, runtime behavior, security evidence, and documentation in one review path.
## Working checklist
- [ ] Preserve unrelated work and inspect the branch, reachable history, repository size, package metadata, and generated output.
- [ ] Review framework, auth, storage, live-sync, deployment, DNS/ingress, observability, recovery, and CLI trust boundaries.
- [ ] Run the dependency-free build and the complete unit/integration suite with enforced coverage floors.
- [ ] Validate documentation links, README guide indexing, declaration synchronization, and every package export.
- [ ] Exercise a packed consumer through scaffold, auth, multi-session live sync, user isolation, deploy, migration, failed activation, rollback, and data restore.
- [ ] Scan package contents, the current tree, and all reachable history for credential material.
- [ ] Verify shutdown, readiness, bounded I/O, private errors, file permissions, and atomic local-state replacement.
- [ ] Perform browser checks for the touched human flow, console errors, accessibility semantics, and a narrow viewport.
- [ ] Review `git diff --check`, generated files, package contents, and the final status before publishing a branch or release artifact.
- [ ] Update the changelog and audit evidence without publishing a registry release unless that release was explicitly requested.
`npm run check` covers the build, dependency contract, coverage floors, documentation/declaration audit, packed-release conformance, deterministic chaos tests, package/current-tree/history credential scans, and workflow-policy checks. Browser review remains intentionally explicit because visual and interaction regressions need a rendered application.
## 2026-07-25 review
- [x] Preserved the existing security-remediation branch and removed the unused logo artifact.
- [x] Confirmed zero runtime, development, peer, and optional dependencies.
- [x] Passed the original 125-test baseline, packaged conformance, security audit, and healthy GitHub CI/CodeQL review.
- [x] Added enforced 80% line, 65% branch, and 80% function coverage floors.
- [x] Added automated local-link, documentation-index, declaration-parity, and package-export checks.
- [x] Bounded and validated CLI credential/link state, made private writes atomic, and bounded/timed platform responses.
- [x] Replaced constant control-plane health with storage-backed readiness and separate liveness.
- [x] Made signal shutdown attempt both HTTP and platform cleanup under a fixed deadline.
- [x] Browser-verified refreshed auth state and removed narrow project-detail overflow and the implicit favicon request.
- [x] Corrected file-service and TypeScript-gate documentation so the published contract matches the implementation.
The final certification result and exact test counts belong in the pull request or release record after the complete gate and browser review finish.
## 2026-07-26 follow-up
- [x] Merged the complete 2026-07-25 security and release-certification pass into `main`.
- [x] Replaced manual-only custom-domain routing refresh with bounded scheduled reconciliation.
- [x] Added durable DNS leases so multiple control planes cannot check and commit the same domain concurrently.
- [x] Added lookup deadlines, stale-result fencing, shutdown coordination, API status, and dashboard cadence visibility.
- [x] Removed the remaining console letter mark so no logo treatment remains.
- [x] Added durable scheduled encrypted backups with cross-control-plane claims, retention, private failure reporting, and graceful draining.
- [x] Added backup cadence/status, manual creation, and verification to the deployment console without exposing host paths.
- [x] Added automatic-backup encryption, failure, retention, multi-control-plane, and shutdown regression coverage.
- [x] Bounded cumulative release files and pre-deploy snapshots with durable per-project count/byte quotas.
- [x] Added confirmation-gated artifact cleanup with active-release and immediate-rollback protections.
- [x] Exposed release storage usage and cleanup in both the deployment console and CLI.
- [x] Re-certified all 136 tests, coverage floors, 45-file documentation/declaration audit, packed conformance, and repository/history security scan.
- [x] Browser-verified cleanup, persistent authenticated refresh, console health, accessibility semantics, and desktop/mobile layout.
- [x] Recorded the final full-suite, security, conformance, and browser evidence in [PR #9](https://github.com/nearbycoder/clank.run/pull/9); Node 22.16, Node 24, packaged conformance, JavaScript/TypeScript analysis, and CodeQL passed.
- [x] Added owner/admin-only permanent site deletion across the API, deployment console, and CLI.
- [x] Made site deletion reclaim quota, slug, port, and domain assignments while revoking scoped tokens and preserving its audit event.
- [x] Added exact confirmation, explicit data-loss acknowledgement, CSRF/scope/RBAC checks, durable locking, and symlink-safe project-root removal.
- [x] Re-certified all 138 tests at 82.22% line, 69.13% branch, and 82.21% function coverage, plus packed conformance and repository/history security scans.
- [x] Browser-verified exact-confirmation deletion, quota refresh, persistent authentication, console health, and desktop/mobile layout without overflow.
- [x] Added a durable workspace activity feed that keeps deleted-site evidence reachable through the dashboard, API, and structured CLI output.
- [x] Added organization attribution and old-schema backfill without weakening role, membership, project-token, pagination, or input boundaries.
- [x] Re-certified all 140 tests at 82.38% line, 69.30% branch, and 82.26% function coverage, plus packed conformance and repository/history security scans.
- [x] Browser-verified deleted-site activity, expandable metadata, authenticated refresh, console health, and desktop/mobile layout without overflow.
- [x] Added complete workspace people administration in the browser and CLI: email-bound invitation creation/acceptance/replacement/revocation, member role changes, administrator removal, and member self-leave.
- [x] Preserved last-owner and owner-only role boundaries, capped active invitations at 100, rejected existing-member reinvites, and redacted invitation-recipient emails from developer activity.
- [x] Re-certified all 141 tests at 82.53% line, 69.52% branch, and 82.36% function coverage, plus 45-file documentation/declaration audit, packed conformance, and repository/history security scans.
- [x] Browser-verified two-account invitation acceptance, role promotion, removal/self-leave, replacement/revocation, persistent auth, console health, and desktop/mobile People layouts without overflow.
- [x] Added quota-aware browser workspace creation with safe slug preview, immediate selection, and a responsive limit state.
- [x] Re-certified all 141 tests at 82.53% line, 69.56% branch, and 82.36% function coverage, plus packed conformance and repository/history security scans.
- [x] Browser-verified authenticated workspace creation, automatic slugging, immediate selection, enforced quota disabling, and desktop/mobile layout without overflow.
- [x] Added invitation-assisted registration so a new collaborator can create only the email-bound invited account and join atomically while ordinary signup remains closed.
- [x] Serialized first-account bootstrap across control-plane runtimes with an expiring SQLite claim and stable losing-account cleanup, and aligned platform policy with normalized auth paths.
- [x] Reloaded the server-rendered console at sign-out and session expiry so prior-account DOM and stale signup policy cannot cross an identity transition.
- [x] Re-certified all 143 tests at 82.56% line, 69.57% branch, and 82.36% function coverage, plus 45-file documentation/declaration audit, packed conformance, and repository/history security scans.
- [x] Browser-verified bootstrap closure, same-browser owner-to-invitee onboarding, consumed-invitation privacy, persistent refresh authentication, zero console/page errors, and a 390px People layout without horizontal overflow.
- [x] Moved platform auth and CLI device-start throttles into one HMAC-keyed, bounded SQLite sliding-window store shared across control-plane runtimes and restarts.
- [x] Corrected successful-login rate-limit clearing and covered cross-runtime accumulation, restart persistence, clock rollback, raw-identity privacy, 429 behavior, and high-cardinality pruning.
- [x] Re-certified all 145 tests at 82.61% line, 69.72% branch, and 82.47% function coverage, plus 45-file documentation/declaration audit, packed conformance, and repository/history security scans.
- [x] Browser-verified generic wrong-password handling, successful retry/reset, authenticated refresh, hashed-only retained limiter state, and zero console/page errors.
- [x] Added focused human and JSON CLI help, typo-safe options, readiness diagnostics, local-checkout scaffolding, offline deploy checks, structured timing results, and retry-safe deployment idempotency.
- [x] Added human and agent operating guides to both scaffold paths, including commands, file ownership, security/migration invariants, and a definition of done.
- [x] Corrected the generated browser import map discovered during fresh-app review so server-rendered authentication hydrates and becomes interactive.
- [x] Re-certified all 148 tests at 82.90% line, 69.76% branch, and 82.67% function coverage, plus documentation/declaration audit, packed conformance, and package/current-tree/history security scans.
- [x] Browser-verified a newly created local-checkout consumer through auth-mode switching, registration, todo creation, a second login, cross-browser live completion, zero error overlays, and no horizontal overflow.
- [x] Hardened hydration ownership so abandoned listeners, directives, keyed rows, and component output are cleaned without disguising application errors as structural mismatches.
- [x] Added case-sensitive SVG and `foreignObject` namespace hydration, and aligned both bundled SSR examples with the browser's exact framework import-map specifier.
- [x] Re-certified all 166 tests, coverage floors, 46-file documentation audit, packaged conformance, and repository/history security scans.
- [x] Browser-verified node-preserving `attached` hydration, refresh, and post-hydration mutations in the full-stack example plus `attached` hydration in the authenticated example.
---
# Renaming from Proact
Canonical URL: https://docs.clank.run/docs/renaming-from-proact
Raw source: https://docs.clank.run/raw/renaming-from-proact.md
Proact was renamed to Clank in version 0.7.0. The framework, deployment platform, package, CLI, generated applications, and public protocol names now use Clank.
## New names
| Before | Now |
| --- | --- |
| `proact` package | `@clank.run/framework` |
| `proact` command | `clank` |
| `proact-platform` | `clank-platform` |
| `proact.deploy.json` | `clank.deploy.json` |
| `.proact/` | `.clank/` |
| `.proact-platform/` | `.clank-platform/` |
| `PROACT_*` environment variables | `CLANK_*` |
| `/__proact/auth/*` | `/__clank/auth/*` |
| `x-proact-*` headers | `x-clank-*` |
| `proact-deploy/1` | `clank-deploy/1` |
| `application/vnd.proact.deploy+gzip` | `application/vnd.clank.deploy+gzip` |
New projects should use only the Clank names.
## Automatic migration
Clank performs these migrations in place:
- the default platform launcher renames `.proact-platform/` to `.clank-platform/` when the new directory does not already exist;
- SQLite framework, authentication, migration-ledger, and deployment-platform tables are renamed transactionally while preserving rows;
- the CLI reads an existing `~/.proact/config.json` or `.proact/project.json`, then writes the equivalent Clank file;
- legacy session cookies remain valid, and the next authentication response uses the new cookie name;
- old deployment artifacts, headers, configuration filenames, token prefixes, and selected environment variables remain readable during the transition.
If both an old and a new database table exist, startup stops instead of guessing which copy is authoritative. Back up platform and application data before the first 0.7.0 startup.
## Source changes
Application source must update package imports and public Clank-specific names. For example:
```ts
import { signal } from "@clank.run/framework";
import { createAuth } from "@clank.run/framework/auth";
```
Rename custom integrations that reference `ClankContext`, `ClankClientOptions`, `ClankPlatformOptions`, `data-clank-*`, or `clank-*` SSR markers. These TypeScript and DOM names are not dual-exported because doing so would permanently expand the public API.
## Compatibility policy
Legacy compatibility is read-side and transitional. Clank always writes new names. Operators should update scripts, secrets, service definitions, proxy rules, and checked-in deployment configuration to use `CLANK_*`, `/__clank/*`, and `clank.deploy.json`.
---
# Clank
Canonical URL: https://docs.clank.run/docs/overview
Raw source: https://docs.clank.run/raw/overview.md
Clank is a dependency-free, AI-first full-stack TypeScript framework and open-source deployment platform. It combines fine-grained reactivity, direct DOM rendering, SSR with node-preserving hydration, inference-first server functions, built-in auth, user-owned SQLite documents, live queries, deterministic artifacts, database migrations, and atomic deployment.
“AI-first” does not mean putting a chat box on every page. In Clank, the same application has two first-class interfaces:
- Humans receive accessible HTML, responsive state, forms, navigation, and Tailwind styling.
- Agents receive named actions with JSON Schema, explicit side-effect and confirmation metadata, and a compact semantic view of the interactive UI.
The framework itself has no dependencies, dev dependencies, peer dependencies, virtual DOM, runtime compiler, or bundler. Clank's build-time TSX compiler is included; Node 22.16+ supplies the final TypeScript-to-JavaScript transform and consistent built-in SQLite backups.
## What works
| Layer | Features |
| --- | --- |
| Reactivity | Signals, lazy computed values, effects with cleanup, batching, rollback transactions, untracked reads, owned roots, deep proxy stores, snapshots, async resources, stream reduction |
| UI | Typed compiler-powered TSX, automatic reactive expressions and props, keyed lists, stable text nodes, lifecycle/context, forms, dialogs, tabs, disclosures, pagination, directives, `Show`, `For`, `Switch`, lazy components |
| Forms | Schema validation, typed fields, accessible control/error props, touched/dirty state, cross-field rules, cancellation, server errors, invalid-focus behavior, reset, agent-readable manifests |
| AI | Web-focused runtime schemas, JSON Schema output, typed actions, authorization, side-effect/confirmation policy, action runners, semantic views, native-label-aware inspect/activate/input surface with secret-value redaction |
| Routing | Parameters, optional segments, wildcards, repeated query values, async loaders, aborts, guards, redirects, titles, links, history navigation |
| Full stack | Inferred schemas/documents/arguments/results, branded IDs, query and mutation functions, zero-codegen typed API references |
| Auth | Email/password sessions, scrypt hashing, secure cookies, CSRF, roles, revocation, default auth UI, SSR boot state |
| Data | Node's built-in SQLite, JSON documents, declared expression indexes, owned tables, atomic mutations, persisted revisions, dependency-tracked query cache |
| Live sync | Auth-partitioned Fetch RPC and cache, EventSource streams, session revocation, automatic invalidation, SSR seeding, multi-tab synchronization |
| SSR | Async string rendering, full-document templates, safe state serialization, CSP nonces, context and keyed lists, marker-based DOM-preserving hydration |
| Server | Fetch router, security headers, safe CORS, bounded Node HTTP adapter, Host checks, symlink-aware static files, response helpers |
| Styling | Native `class`, reactive `classList`, style objects, CSS custom properties, Tailwind Play CDN and compiled Tailwind CSS compatibility |
| Deploy | Full browser console, workspace people/role/invitation administration, activity feed, ingress performance metrics, enforced account/organization/site/domain/release-storage quotas, custom DNS/TLS onboarding, browser-approved CLI auth, deterministic artifacts, encrypted secrets, immutable migrations, SQLite backups, storage-backed readiness, health-gated releases, confirmation-gated release and site deletion, logs, audit, rollback |
## Install
Install the framework and its `clank` and `clank-platform` commands from the organization scope:
```sh
npm install --global @clank.run/framework
clank --version
```
Generated applications declare `@clank.run/framework` as their only dependency. The framework has no transitive packages.
## Run it
```sh
npm run check
npm run dev
```
Open `http://127.0.0.1:4173`. `npm` only runs scripts here; `package.json` installs nothing.
The development server also exposes several complete application shapes:
- `/examples/commerce/index.html`: catalog, filtering, cart dialog, and checkout form.
- `/examples/dashboard/index.html`: responsive SaaS admin, tabs, tables, pagination, invitations, and settings.
- `/examples/booking/index.html`: multi-step dates, room selection, guest details, pricing, and confirmation.
- `/examples/todo/index.html`: the smallest focused keyed CRUD example.
Run the SSR + SQLite + live-sync example separately:
```sh
npm run dev:fullstack
```
Open `http://127.0.0.1:4180` in two tabs. A committed mutation in either tab streams a new query snapshot to both. The first response already contains the todo HTML; the client hydrates those exact nodes.
Run the auth-first reference app:
```sh
npm run dev:auth
```
Open `http://127.0.0.1:4181`, create an account, and sign into that account from a second browser. Registration, secure sessions, a synchronized profile, create/rename/complete/remove todo operations, per-user data isolation, SSR, Tailwind, and live synchronization are already wired. The complete source and Tailscale instructions are in [`examples/auth-todo`](examples/auth-todo).
Create and deploy a new authenticated app:
```sh
npm run dev:platform
# Create an account at http://127.0.0.1:4200
clank login --server=http://127.0.0.1:4200
clank create my-app
cd my-app
npm install
clank doctor
clank deploy
```
The generated app installs only Clank, which has no transitive dependencies, and includes `README.md` plus an agent-ready `AGENTS.md`. Before publishing Clank to npm, use `--framework=local` to install from the current checkout. The first deploy creates and links the site automatically. The CLI builds locally without a shell, vendors the exact Clank runtime, verifies every path and SHA-256 digest, applies ordered SQLite migrations behind a backup, health-checks the candidate, and restores the previous release automatically on failure. `clank deploy --dry-run` performs the complete local artifact check without login or network access.
The smallest application is:
```tsx
import { computed, render, signal } from "./dist/index.js";
const count = signal(0);
const label = computed(() => `Count: ${count.value}`);
function App() {
return (
);
}
render(document.querySelector("#app")!, );
```
## Documentation
- [Documentation website](https://docs.clank.run) — searchable, responsive, and available as Markdown, JSON, `llms.txt`, and a complete agent corpus
- [Documentation site source](docs-site/README.md)
- [Getting started](docs/getting-started.md)
- [Reactivity](docs/reactivity.md)
- [Rendering and components](docs/rendering.md)
- [Forms](docs/forms.md)
- [Headless UI behavior](docs/ui.md)
- [Performance model](docs/performance.md)
- [AI-first contracts](docs/ai-first.md)
- [AI application blueprints](docs/blueprints.md)
- [Authentication, MFA, passkeys, and recovery](docs/authentication.md)
- [Organizations, RBAC, invitations, and scoped tokens](docs/organizations.md)
- [Service drivers for files, email, jobs, and webhooks](docs/services.md)
- [Structured logs, traces, metrics, and health](docs/observability.md)
- [Encrypted backups and disaster recovery](docs/recovery.md)
- [Durable distributed deployment and agent fencing](docs/distributed-deployment.md)
- [Managed ingress, custom domains, and external PostgreSQL](docs/data-plane.md)
- [ASVS-oriented security verification](docs/security-asvs.md)
- [Threat model](docs/threat-model.md)
- [Chaos and failure testing](docs/chaos-testing.md)
- [Public beta readiness](docs/public-beta.md)
- [Application recipes for humans and agents](docs/application-recipes.md)
- [Packaged-release conformance](docs/conformance.md)
- [Code and product audit](docs/code-audit.md)
- [Maintenance and release certification](docs/maintenance.md)
- [Release process](docs/releases.md)
- [Authentication](docs/auth.md)
- [Security and deployment](docs/security.md)
- [Deployment platform](docs/deployment-platform.md)
- [Deployment dashboard, quotas, metrics, and custom domains](docs/platform-dashboard.md)
- [Deployment CLI](docs/cli.md)
- [Renaming from Proact](docs/renaming-from-proact.md)
- [SQLite migrations](docs/migrations.md)
- [Database revisions and correctness](docs/database.md)
- [Platform security](docs/platform-security.md)
- [Self-hosting](docs/self-hosting.md)
- [Railway production deployment](docs/railway.md)
- [Routing](docs/routing.md)
- [Server primitives](docs/server.md)
- [Full-stack SSR, SQLite, and live sync](docs/full-stack.md)
- [Tailwind CSS](docs/tailwind.md)
- [Architecture](docs/architecture.md)
- [API reference](docs/api-reference.md)
## Design principles
1. **Inference should flow from runtime contracts.** Declare a validator once; TypeScript derives documents, IDs, function inputs, outputs, and clients while agents receive the equivalent JSON Schema.
2. **Fine-grained updates beat tree rerenders.** Components establish bindings once; signals update only the dependent region or property.
3. **The platform is the dependency.** Clank uses DOM, Fetch, URL, AbortController, Proxy, Web History, and Node primitives directly.
4. **Simple code should stay simple.** A component is a function, state is an object with `.value`, and UI is ordinary HTML structure.
5. **Dangerous behavior must be legible.** Actions declare side effects, confirmation expectations, validation, and authorization at their boundary.
6. **Private data should be private by construction.** Auth-required functions and owned tables make the safe path the short path.
7. **Deployment should be a verifiable transaction.** Build inputs, artifact contents, migration history, activation, failure recovery, and rollback are explicit and auditable.
Clank is MIT licensed.
The official npm package is `@clank.run/framework`; the installed executables are `clank` and `clank-platform`. The npm organization provides a verified namespace for future Clank packages, while the unrelated unscoped `clank` package remains a different project.
---
# Changelog
Canonical URL: https://docs.clank.run/docs/changelog
Raw source: https://docs.clank.run/raw/changelog.md
Clank follows semantic versioning. Entries describe user-visible framework, CLI, protocol, storage, security, and deployment changes.
## Unreleased
### Added
- The framework now publishes from the `@clank.run` npm organization as `@clank.run/framework`, with explicit public-registry, provenance, repository, documentation, and package-export metadata.
- A fully authenticated deployment dashboard with site status, 1-hour through 30-day ingress charts, releases, logs, and guided custom-domain setup.
- Transactionally enforced per-organization site and per-project custom-domain limits, with operator-configurable metric retention.
- Minute-level fixed-histogram ingress metrics plus DNS routing inspection and a Caddy On-Demand TLS permission endpoint for deployed built-in and verified custom hosts.
- Packaged-release conformance covering scaffold, browser and CLI auth, live synchronization, isolation, deployment, migration, failed health activation, rollback, and data restoration.
- GitHub CI and OIDC trusted-publishing release workflows.
- Security reporting, contribution, conduct, ownership, and release-governance documentation.
- Deterministic AI blueprints with plan, explain, and generated authenticated application files.
- Email verification, password recovery, email-code MFA, WebAuthn passkeys, organizations, RBAC, invitations, and project-scoped CLI tokens.
- Typed file, email, job, webhook, observability, encrypted backup, orchestration, ingress, custom-domain, external PostgreSQL, and database-provisioning drivers.
- ASVS-oriented evidence, threat modeling, chaos tests, CodeQL, immutable GitHub Actions, package/credential auditing, and a public-beta gate.
- Enforced line, branch, and function coverage floors plus a documentation audit that verifies local links, guide indexing, declaration synchronization, and package export targets.
- Storage-backed `/healthz` and `/readyz` probes alongside the process-only `/livez` endpoint.
- Bounded automatic custom-domain routing reconciliation with durable cross-control-plane leases, lookup deadlines, operator configuration, and dashboard status.
- Automatic verified encrypted database backups with durable cross-control-plane leases, configurable cadence and retention, private failure reporting, and dashboard controls.
- Enforced per-project release count/byte quotas plus rollback-scoped dashboard and CLI cleanup for inactive runtime artifacts and pre-deploy snapshots.
- Owner/admin-only permanent site deletion in the dashboard, API, and CLI with exact confirmation, explicit data-loss acknowledgement, scoped-token denial, path-safe storage removal, token revocation, and retained audit evidence.
- A role-filtered, cursor-paginated workspace activity feed in the dashboard, API, and CLI that retains deleted-site history and upgrades existing audit rows with organization attribution.
- Workspace people administration in the dashboard and CLI, including browser acceptance, member role changes, immediate removal, safe pending-invitation listing, one-time token copy, replacement, and revocation.
- Quota-aware workspace creation in the People console with safe slug preview and immediate selection.
- Durable SQLite-backed deployment-platform authentication and CLI device-start rate limits shared across control-plane runtimes and restarts.
- Command-aware human and JSON CLI help, agent-readable readiness diagnostics, generated `README.md`/`AGENTS.md` guides, and local-checkout scaffolding that does not require an npm release.
- Offline deterministic deployment dry-runs, structured deployment results with timing, one-command first-project naming/workspace selection, and retry-safe persisted idempotency attempts.
### Fixed
- Workspace invitations can now create their email-bound account and accept membership in one browser flow even when public registration is disabled or the one-time bootstrap is complete.
- First-account bootstrap is now protected by an expiring SQLite claim across control-plane runtimes, preventing concurrent processes from creating multiple initial accounts.
- Platform signup policy now evaluates the same normalized auth operation as the low-level router, closing repeated-slash registration bypasses.
- Sign-out and expired-session transitions now reload the deployment console at the identity boundary, clearing prior-account dashboard DOM and recomputing bootstrap availability before another account signs in.
- Successful password login now clears the exact failed-attempt rate-limit key instead of leaving the account throttled.
- CLI profile and project-link state is now bounded, structurally validated, URL-canonicalized, privately and atomically replaced, and never reflected in parse errors.
- CLI control-plane responses are streaming-bounded, decoded as strict UTF-8/JSON, and protected by finite request and deployment timeouts.
- Platform signal handling now closes both the HTTP listener and control-plane state, reports shutdown failures, and enforces a 30-second termination deadline.
- Project navigation now resets scroll position and closes the mobile drawer; the detail tab row stays within narrow viewports without a page-level horizontal scrollbar or clipped breadcrumb, and the console explicitly serves no favicon asset.
- The deployment console no longer renders or requests a logo asset or letter-mark treatment.
- Backup API responses no longer expose private host database paths.
- Repeated deployments can no longer grow retained release storage without an installation-defined ceiling.
- Site quotas can now be reclaimed without operator-side SQLite or filesystem edits.
- Managed ingress now fixes the upstream origin before applying a request path and streaming-bounds bodies without `Content-Length`, closing scheme-relative SSRF and unbounded-buffer paths.
- Pending custom-domain assignments can no longer be moved between projects; platform-owned DNS namespaces are reserved.
- Reissuing an invitation now revokes every older active token for that workspace/email, existing members cannot be reinvited, and active invitations are capped at 100 per workspace.
- Refreshed dashboard sessions now render one consistent authentication state and return to sign-in when a session expires.
- Failed distributed-lease acquisition now releases the local project queue.
- Managed ingress strips `Connection`-nominated headers and retries safe requests after transient upstream 5xx responses.
- WebAuthn CBOR parsing now bounds collection size and nesting, and passkey counter advancement is atomic.
- Blueprint suffix parsing and local-file endpoint normalization are linear on adversarial input.
- Unexpected deployment-platform failures are logged privately and never reflected to HTTP clients.
- Every HTTP/runtime adapter now keeps unexpected exception text private, and adversarial prefix, identifier, URL, and numeric-literal parsing is linear.
- CLI device login prints its browser approval URL without launching an operating-system command.
- The minimum runtime is Node 22.16, the first Node 22 release with the built-in SQLite backup API used by migrations and recovery.
- The official package name is `@clank.run/framework`, avoiding collision with unrelated unscoped packages while leaving the binaries as `clank` and `clank-platform`.
- Command-specific `--help` no longer authenticates or executes the command, dry-run deploys no longer require login, asynchronous CLI failures now reliably exit non-zero without stack traces, and unknown long options fail with spelling guidance instead of being ignored.
- Generated authenticated apps and bundled SSR examples now map the browser's actual `@clank.run/framework` module specifier, allowing their server-rendered screens to hydrate and become interactive.
- Hydration now cleans partially attached listeners, directives, keyed rows, and component ownership before fallback; only structural mismatches remount, lifecycle/application errors remain visible, and case-sensitive SVG plus `foreignObject` namespaces preserve their server nodes.
- Local deployment artifacts are now written through a private atomic replacement, preventing a pre-existing output symlink from redirecting CLI writes.
## 0.7.0 - 2026-07-16
### Added
- AI-first runtime schemas, actions, semantic UI, forms, headless UI primitives, SSR, hydration, routing, SQLite backend, authentication, migrations, and the Clank deployment platform.
- Deterministic deployment artifacts, encrypted platform secrets, device authorization, health-gated activation, logs, audit history, and rollback.
### Changed
- Renamed the complete framework, CLI, storage, protocol, documentation, and deployment UI from Proact to Clank while preserving legacy data through compatibility readers and in-place migration.
---
# Contributing to Clank
Canonical URL: https://docs.clank.run/docs/contributing
Raw source: https://docs.clank.run/raw/contributing.md
Clank favors small public contracts, explicit security boundaries, deterministic behavior, and platform primitives over hidden dependency machinery.
## Before changing code
Open an issue for a new public API, wire protocol, persistent schema, authentication behavior, deployment state transition, or compatibility break. A short proposal should contain:
1. the human and agent workflow;
2. the smallest proposed contract;
3. authorization and data boundaries;
4. migration and rollback behavior;
5. alternatives considered; and
6. the evidence that will prove the change works.
Typographical fixes, tests for existing behavior, and narrow bug fixes do not require a proposal.
## Development
Requirements:
- Node 22.16 or newer;
- no runtime or development NPM dependencies; and
- a filesystem that supports normal SQLite locking and atomic rename semantics.
Run:
```sh
npm run check
```
That command builds the framework, enforces the zero-dependency and coverage contracts, audits documentation/declaration/package-export integrity, runs unit/integration tests, installs and exercises the packed release through the complete conformance journey, and finishes with package/tree/history security checks.
## Pull requests
- Add tests that would fail without the change.
- Update every relevant public guide and API reference.
- Preserve the zero-dependency contract.
- Keep generated files, databases, credentials, `.env` files, platform state, and local artifacts out of commits.
- Explain compatibility, migration, failure recovery, and rollback.
- Treat model output, browser input, uploaded artifacts, migrations, URLs, and persisted application data as untrusted.
Large modules may be split when the split improves a concrete change. Moving code without improving a public boundary, testability, or security property is not itself a goal.
## Commit and release policy
Maintainers use reviewed pull requests for `main`. Releases follow [the release process](docs/releases.md). By contributing, you agree that your contribution is licensed under the repository's MIT license.
Git commit author and committer addresses are public repository metadata. Contributors who want email privacy should configure GitHub's account-specific `ID+username@users.noreply.github.com` address locally and enable GitHub's command-line push protection for personal email addresses before committing. Never rewrite published shared history only to change an address; coordinate any history rewrite as a separate incident response when actual credentials or regulated data were committed.
---
# Security policy
Canonical URL: https://docs.clank.run/docs/security-policy
Raw source: https://docs.clank.run/raw/security-policy.md
## Supported versions
Security fixes are made for the latest published minor release. A fix may require upgrading when preserving an older contract would leave users exposed.
## Reporting a vulnerability
Do not open a public issue for an undisclosed vulnerability.
Use GitHub's private vulnerability-reporting flow for this repository. Include:
- the affected Clank version and Node version;
- the smallest reproducible request, artifact, migration, application, or configuration;
- the security boundary crossed and realistic impact;
- whether the issue has been exploited or disclosed elsewhere; and
- any suggested mitigation.
Do not send real credentials, access tokens, session cookies, master keys, customer data, or production databases. Replace sensitive values with a minimal synthetic reproduction.
Maintainers should acknowledge a complete report within three business days, establish an initial severity and response owner within seven days, and coordinate disclosure after a fix or mitigation is available. Those targets are operational goals, not a guarantee or bug-bounty offer.
## Scope
High-value boundaries include:
- authentication, session, CSRF, role, ownership, and CLI-token enforcement;
- path traversal, symlink substitution, static-file access, and artifact extraction;
- migration validation, backup, restore, release activation, and rollback;
- secret encryption, key handling, logging, and process/container isolation;
- SSR/DOM injection, executable URLs, agent action authorization, and confirmation; and
- cross-project or cross-user data access.
The trusted process runner is not a public-code sandbox. Reports that only demonstrate that trusted process-mode application code can access its own Unix user's authority are out of scope unless they cross a documented isolation boundary. Docker and stronger isolation still depend on correct host configuration.
## Disclosure
Security releases will describe affected versions, impact, mitigation, and upgrade instructions without publishing exploit details that would unnecessarily endanger users. Credit is offered when requested and safe.
---
# Community conduct
Canonical URL: https://docs.clank.run/docs/code-of-conduct
Raw source: https://docs.clank.run/raw/code-of-conduct.md
Clank contributors and maintainers are expected to make participation respectful, safe, and productive.
Be considerate, critique technical decisions rather than people, avoid harassment or discriminatory conduct, respect privacy, and stop behavior when someone identifies it as harmful. Maintainers may edit or remove contributions, comments, or participation that violate these expectations.
Report conduct concerns privately through the repository owner's GitHub profile contact channel. Do not use a public issue when doing so would expose private or sensitive information. Maintainers should disclose conflicts of interest and handle reports as confidentially as practical.
This policy applies in repository discussions, reviews, community spaces, and public interactions undertaken while representing Clank.