CELL
The hook is the program.
The database keeps it alive.
Push a root into the database, declare named state where it is used, and expose only the artifacts and capabilities clients need.
ROOT.
HOOKS.
ARTIFACTS.
persistent signal
type and value live together
JSX artifact
holes are reactive edges
type Title = string
const title = useState(Title, "hello, hook world", "title")
const shout = map title => title + "!"
return frontend(
<html><body>
<h1>{title}</h1>
<p>{shout}</p>
<input value={title} onInput={action (value: string) =>
title.set(value)}/>
</body></html>
)
11 chaptersOne file to the engine underneath
10 MINUTES · PUSH THEN SERVE
Run a complete persistent web app.
A v2 database stores a root program. push sets that root; serve invokes it as the root hook, revives its cells, and serves the artifact it returns.
node src/cli.ts push examples/11-graph/hello.cell --db hello.db
node src/cli.ts serve --db hello.db --port 8080
node src/cli.ts state --db hello.db
value: root source, modules, and cells
seq: committed write count
Trace what just happened
- Push. Cell stores the file source as the database's root program—an ordinary data write.
- Invoke.
serveparses, checks, and mounts that root in hook mode;callinvokes an exported root action. - Wire.
useStatedeclares a typed cell; a name such as"title"keeps its identity stable when code moves. - Serve.
frontend(JSX)selects a frontend artifact for LiveView or hydration while the port stays deployment configuration.
Where is the schema for title?
Reveal
At useState(Title, ..., "title"). The declaration carries its type and initial value; the name gives it a stable address in the mounted root.
TS-FLAVORED · EXPRESSION-ORIENTED
Familiar syntax, three execution modes.
Cell resembles TypeScript without trying to be TypeScript. Values and types are checked by Cell; functions are explicitly pure, effectful, or declarative.
const answer = 42let total = 0{ name: "Ada" }[1, 2, 3](x: number) => x + 1<p>{answer}</p>Pure, action, hook
A plain arrow is pure. An action may perform effects and mutate persistent cells. A hook declares wiring: it may call state hooks and compose child hooks, but its body may not perform effects directly.
Purity is statically checked when definite and enforced at runtime through every call depth.
const square = (n: number): number => n * n
const report = action (n: number) => {
io.log("square:", square(n))
}
const counter = hook () => {
const count = useState(number, 0, "count")
return {
count,
increment: action () =>
count.update((n: number) => n + 1)
}
}
| Form | Purpose | Important behavior |
|---|---|---|
if (c) a else b | Conditional expression | Produces a value; either branch may be a block |
match (x) { ... } | Enum elimination | Annotated enums must be handled exhaustively |
for (const x of xs) | Imperative collection loop | Useful with a local let; persistent writes still use actions |
throw value | Unrecoverable failure | No try; model expected failure with an enum |
const sum = (values: number[]): number => {
let total = 0
for (const value of values) {
total = total + value
}
return total
}
const Status = enum { adult, minor: number }
const describe = (status: Status): string =>
match (status) {
adult => "adult",
minor(years) => "minor for " + str(years) + " more years"
}
STRUCTURAL CORE · COMPUTED TYPES · NOMINAL SEALS
Types are values, now with TypeScript-shaped declarations.
Use type Name = ... for named types and type Pair<A, B> = ... for generic aliases. The older expression forms remain valid because type-level computation is still ordinary evaluation.
{ name: string }enum { some: T, none }type Id = hide numbertype User = { name: string, age: number }
type Pair<A, B> = { first: A, second: B }
type Meters = hide number
const walk = (distance: Meters): Meters => distance + 5
const point: Pair<number, string> = { first: 1, second: "one" }
const user: User = { name: "Ada", age: 36, role: "mathematician" }
const distance = Meters.elim(walk(10))
return { point, user, distance }
number string booleanscalar valuesdataserializable valuesT[] · T{}same identity-bearing containerfn(A) => Radd action for effectsjust(value)exactly one inhabitantStructural until identity earns a name
Record types accept extra fields. Enums are tagged alternatives. hide creates a nominal type with .type, .create, and .elim capabilities; annotations seal raw values inside the defining module.
just(v) is the singleton type inhabited only by v. It remains available for indexed type-level programs, while the earlier public proof helpers are intentionally parked and are no longer language globals.
Cell reports definite field, operand, arity, exhaustiveness, purity, and opaque-brand errors when a function is defined. Dynamic unknowns remain legal and meet runtime coercion at boundaries.
ROOT PROGRAM · DATABASE IMPORTS · CLOSURES AS DATA
The database owns the program it invokes.
push stores root source in the database. call or serve invokes it in hook mode; imports from a database root resolve only from that database's module region.
cell push app.cellset root source{ root, modules }database code regioncell call / serveinvoke root{ values, hooks, artifacts }ordinary exportsThe root chooses its host surface
A root may expose named actions for call, declare cells directly, or return an artifact for serve. The host sets or invokes the root; installation policy and every other operation remain Cell code.
Imports follow authority
File-run compatibility resolves imports from files. Once code runs as a database root, imports come from that database alone. Missing modules fail with the exact path and must be installed by root logic.
import tools from "./tools.cell"
const offset = 2
const double = (n: number) => n * offset
return {
answer: double(21),
transform: double,
tools
}
What makes a closure persistable?
Reveal
Cell records the lambda source plus the data values it captures. That pair serializes and revives as a callable closure; mutable captures and live wiring captures are refused because they cannot be persisted honestly.
WIRE ONCE · REACT FOREVER
A hook is mounted, not imperatively called.
A hook declares cells, derives signals, composes child hooks, and returns capabilities or artifacts. Invoking one while wiring creates a child mount frame.
const counter = hook () => {
const count = useState(number, 0, "count")
return {
count,
increment: action () =>
count.update((n: number) => n + 1)
}
}
const left = counter()
const right = counter()
return frontend(
<html><body>
<button onClick={left.increment}>left: {left.count}</button>
<button onClick={right.increment}>right: {right.count}</button>
</body></html>
)
Hook bodies wire cells and signals. Put mutation, I/O, and event work in action callbacks.
hook () => wiringTwo invocations of the same hook receive different hierarchical paths, so identically named child cells remain independent.
/0.count · /1.countsignal.each(hook (...)) mounts one persistent hook frame per key and tears down only its live wiring when the key leaves.
base@key.countTYPE · INITIAL VALUE · OPTIONAL NAME · HANDLE
Declare durable state exactly where you use it.
useState(T, initial, name?) creates or revives one persistent cell. Name long-lived cells so identity survives hook reordering; the handle exposes reactive methods and a first-class reference.
type Profile = { name: string, visits: number }
const profile = useState(Profile, {
name: "Ada",
visits: 0
}, "profile")
const greeting = map profile =>
"hello, " + profile.name
const visit = action () =>
profile.update((p: Profile) => ({
...p,
visits: p.visits + 1
}))
return { profile, greeting, visit }
cell.get()read now while wiring or in an actioncell.signalthe underlying reactive valuecell.map(fn, equals?)derive another signalcell.set(value)replace after type coercioncell.update(fn)pure current-value transitioncell.refstorable authority to read or rewrite this cellNames survive movement
At the root, "profile" becomes .profile; the same name in a child is scoped by that child's path. Named cells do not consume positional indices, so adding or reordering them does not shift unnamed neighbors.
cell.ref is a storable lens: ref.read(state) returns an option and ref.write(state, value) returns updated database data. There is no string-to-ref escape hatch.
ARTIFACT · LIVEVIEW · HYDRATION · AUTHORITY
The frontend is one artifact a hook may return.
artifact(kind, payload) is the native program-splitting mechanism; frontend(JSX) is its JSX convention. LiveView keeps execution on the server, while hydration mounts the root against optimistic public mirrors.
type Title = string
const title = useState(Title, "hello, hook world", "title")
const shout = map title => title + "!"
return frontend(
<html>
<body>
<h1>{title}</h1>
<p>shouted: {shout}</p>
<input value={title}
onInput={action (value: string) => title.set(value)}/>
<button onClick={action () => title.set("hello, hook world")}>
reset
</button>
</body>
</html>
)
frontend(vnode) is the "frontend" convention over the general artifact primitive.
Static values render once; signals subscribe and receive fine-grained updates.
Ordinary event actions run locally when hydrated; useAction deliberately stays authoritative.
Choose a tiny event/patch client or hydrate the root for optimistic public-cell rendering.
node src/cli.ts serve --db hello.db --port 8080
node src/cli.ts serve --db hello.db --port 8080 --hydrate
The first command server-renders and patches precise holes, attributes, and keyed collections. The second ships the database root and module region; public writes update local mirrors immediately, are type-checked by the server, and reconcile from the authoritative stream.
type Visit = { note: string }
const visitLog = hook () => {
const events = useSecret(Visit[], [], "events")
const count = useState(number, 0, "count")
const total = map events => events.length
total.out(action (n: number) => count.set(n))
const record = useAction(action (note: string) =>
events.update((items: Visit[]) => items.append({ note: note })))
return { count, record }
}
const log = visitLog()
return frontend(
<main>visits: {log.count}
<input onChange={action (note: string) => log.record(note)}/>
</main>
)
A hook is a hide
useSecret creates authority-only state: its value is absent from hydrated bundles and streams, and a remote write receives 404 as if the cell did not exist. Publishing count is an explicit signal edge.
SIGNAL EDGES · KEYED COLLECTIONS · DYNAMIC HOOKS
Change travels only where the hook says it can.
Signals compute a baseline when wired and fire on change. Every list is now an identity-bearing collection; JSX reconciles keyed elements individually, and each(hook (...)) can give every key its own persistent hook frame.
todostodos.map(...)<li key={...}>rm · set · ordtype Todo = { text: string, done: boolean }
const todos = useState(Todo[], [
{ text: "try cell v2", done: false }
], "todos")
const item = (todo: Todo) =>
<li key={todo.text}>
<button onClick={action () =>
todos.update((current: Todo[]) =>
current.map((candidate: Todo) =>
if (candidate.text == todo.text)
({ ...candidate, done: !candidate.done })
else candidate))}>
{if (todo.done) "[x]" else "[ ]"}
</button>
{todo.text}
</li>
const list = map todos => todos.map(item)
const left = map todos =>
todos.filter((todo: Todo) => !todo.done).length
return frontend(
<html><body>
<h1>todos</h1>
<p>{left} left</p>
<ul>{list}</ul>
</body></html>
)
Identity is the default container rule
[...] now creates a collection; T[] and T{} name the same container. map, filter, update, and append preserve identity; concat deliberately creates a fresh container.
For JSX reconciliation, an explicit key remains the stable DOM identity; position is the fallback.
Derive a signal. Supply an equality to suppress equivalent results.
Attach an action observer; used directly in lower-level hooks.
With a hook builder, mount one persistent frame per key and stop its live wiring when it leaves.
Detach a node; stopping an each also tears down its scopes.
const rooms = useState(string[], ["alpha", "beta"], "rooms")
const pulse = useState(number, 0, "pulse")
rooms.each(hook (room: string) => {
const count = useState(number, 0, "count")
pulse.out(action (_pulse: number) =>
count.update((n: number) => n + 1))
})
Every key owns a mount frame
The frame path combines the surrounding hook position with the key's identity. Named cells, nested hooks, useAction, and duplicate-name checks work inside it exactly as they do at the root.
VERSIONED CELL · STORED LENSES · OLD CODE LIVES
Migration is a library hook, not a runtime privilege.
versioned.use(T, initial) composes two named cells: payload and lineage. Its handle carries first-class cell refs, while the migration library stores real forward and backward closures in one atomic update.
stringstored payloadstring → Nameclosure stored{ first: string }latest native shapestringtranslated view// app.cell
import versioned from "./examples/lib/versioned.cell"
type Name = { first: string }
const name = versioned.use(Name, { first: "Ada" })
const label = map name => name.first
return { ...frontend(<p>{label}</p>), nameRef: name.ref }
// migrate.cell — receives the exported or stored ref
import migrations from "./examples/lib/migrations.cell"
return {
migrate: action (ref: { value: CellRef, lineage: CellRef }) =>
migrations.update(
ref,
Name,
(old) => ({ first: old }),
(now) => now.first
)
}
The type is a version claim
Current code asks for one type in the lineage. Latest code reads the native payload; older code is translated backward on read and forward on write through stored closures.
The migration accepts name.ref, not a mount-path string. Whoever holds that storable pair has the authority to migrate exactly those payload and lineage cells.
THE V1 ENGINE STILL RUNS UNDERNEATH
Hooks simplify the surface; sovereignty stays.
The mature v1 APIs remain implemented for existing programs and advanced infrastructure: the database is one typed value, writes are atomic, collections keep identity, and peers never gain another store's write capability.
{ schema: type,
value: schema }type check
atomic persist
signal propagationV2 root programs and useState are the mounted-hook interface to this persistent engine.
spawn / openA database creates and controls a child. Use this only for environments genuinely owned by the parent.
parent → childconnect / signalA peer reads another sovereign environment without gaining write access.
subscriber ← statesend / inboxData enters the owner's durable mailbox. Only the owner's own action decides its meaning.
sender → owner policy| Surface | Use it for | Status |
|---|---|---|
push + call / serve | Set or invoke a database-resident root program | Preferred host path |
hook + use* + artifact | Compose mounted programs, state, authority boundaries, and outputs | Preferred language path |
db.listen(port, page, props) | V1 browser interpreter with projected props, local tab DB, and sessions | Compatibility path |
db.listen(port) | Sovereign database state, long-poll subscriptions, durable inbox transport | Infrastructure API |
node src/cli.ts push app-v2.cell --to http://host:8080
The serving database writes the new root, tears down the old mount wiring, and remounts on the existing port. Named cells survive; connected browsers reload into the new artifact.
CURRENT SURFACE · V2 FIRST
Keep this beside your editor.
Database roots, hooks, authority boundaries, and the merged collection surface come first. Lower-level APIs remain because they implement the same model underneath.
TYPE FORMS11
| Form | Meaning |
|---|---|
number string boolean | Primitive scalar types |
data | Serializable data, including closures whose captures are data and first-class references; excludes live signals and host built-ins |
type | The type of type values |
type User = { name: string } | V2 named structural record type |
struct { name: string } | Equivalent expression-form record type |
enum { some: T, none } | Tagged union |
T[] / T{} | Two type spellings for the same identity-bearing collection |
fn(A) => R | Pure function type; prefix with action for effects |
type Pair<A, B> = ... | Generic type alias; type-level computation |
type Id = hide number | Nominal type with controlled create/eliminate capabilities |
just(value) | Singleton type inhabited by exactly that value |
BINDINGS & CONTROL08
| Form | Behavior |
|---|---|
const x: T = value | Immutable binding; annotation checks and seals |
let x = value / x = next | Mutable local binding |
for (const x of xs) { ... } | Iterate a collection in insertion order |
(x: T): R => value | Pure function |
action (x: T) => ... | Effectful callback |
hook () => ... | Declarative wiring closure, valid while mounted |
if (c) a else b / match (x) { ... } | Conditional / exhaustive enum elimination |
throw value | Abort for unrecoverable errors; no catch construct |
HOOKS & ARTIFACTS11
| Name | Behavior |
|---|---|
return frontend(jsx) | The database root mounts in hook mode and returns its served artifact |
hook (...) => ... | Declare reusable mounted wiring; invoking it creates a child frame |
artifact(kind, payload) | Native mechanism for returning a named executable artifact |
frontend(jsx) | Convention for artifact("frontend", jsx) |
<tag attr="text">...</tag> | Native JSX evaluates to vnode data |
{signal} | Reactive content hole |
value={signal} | Reactive element attribute |
onClick={action} | Event action; hydrated roots may run it optimistically |
key={data} | Stable identity for collection-element reconciliation |
childHook() | Wire a child frame inside a mounted hook |
items.each(hook (...)) | Mount one persistent dynamic frame per distinct key |
STATE & AUTHORITY10
| Name | Behavior |
|---|---|
useState(T, initial?, name?) | Create or revive a public typed cell; a name survives hook reordering |
useSecret(T, initial?, name?) | Create authority-only state excluded from hydration and remote writes |
useAction(action) | Keep an action on the authority and give hydrated clients a typed proxy |
cell.get() | Read the current typed value while wiring or inside an action |
cell.signal | Underlying signal node |
cell.set(value) | Replace the cell after coercion to T |
cell.update(pureFn) | Pure current-value transition |
cell.ref | Storable, runtime-minted CellRef for this cell |
ref.read(value) / .write(value, next) | Pure lens operations over database value data |
cell.map/out/each/stop | Delegated signal members |
SIGNAL MEMBERS05
| Member | Behavior |
|---|---|
.map(pureFn, equals?) | Derived signal; equality suppresses equivalent values |
map source => expression | Sugar for mapping while rebinding the source name to its current value |
.out(action) | Effect observer; returns a stoppable node |
.each(action | hook) | One live scope—or one persistent mount frame—per distinct collection key |
.stop() | Detach this node and any owned each scopes |
COLLECTIONS & STRINGS16
| Member | Behavior |
|---|---|
[a, b] / T[] / T{} | Literal and type forms for the single identity-bearing container |
Collection(T) / collection(values?) | Expression-form type constructor / compatibility value constructor |
.length / .size | Element count |
.map(fn) / .filter(pred) | Transform or select while preserving surviving identities |
.append(value) | Extend the same container identity with one fresh element id |
.concat(other) | Create a fresh container with fresh element identities |
.reduce(fn, initial) / .join(separator) | Fold or display values in insertion order |
.find(pred) / .at(index) | Return Option(T) |
.each(fn) | Call for each value in insertion order |
.add(value) | Return { collection, ref } |
.update(ref, fn) | Replace one referenced value without changing identity |
.remove(ref) | Delete one referenced element |
.get(ref) | Return Option(T) |
.values() | Compatibility identity operation; returns the collection itself |
.entries() | Collection of ordered { ref, value } pairs |
string.length / .split(separator) | Character count / identity-bearing string collection |
GLOBALS & MODULES12
| Name | Role |
|---|---|
Option(T), some(v), none | Standard optional-value enum |
Collection(T) | Expression-form collection type constructor |
str(value) / num(value) | Display conversion / numeric conversion |
assert(bool, message?) | Return unit or throw an assertion failure |
deref(container, ref) | Find a referenced value in nested data |
load(source, params?) | Purely evaluate source text |
use(path, params?) | Evaluate a module in the current file or database resolution space |
import x from "path" | Sugar for one-argument use |
params | CLI/load/use parameters as a struct of strings or supplied data |
io.log(...values) / io.read(path) | Effectful output / file read |
Ref / DbRef / CellRef | Collection element, child database, and persistent-cell reference types |
db | Current sovereign database handle |
DATABASE & PEERS · LOWER LEVEL11
| Member | Behavior |
|---|---|
db.get() | Current { schema, value } |
db.evolve(pureTransition) | Atomic schema-checked write |
db.signal(query, equals?) | Reactive pure state query |
db.spawn(name) / db.open(ref) | Create/open an owned child database |
db.connect(address) | Read/send peer for a directory or URL |
db.inbox(action) | Handle durable pending and future messages |
db.sessions(query, equals?) | Reactive signal over v1 browser sessions |
db.listen(port, page?, props?) | V1 peer protocol and optional browser interpreter |
peer.addr / peer.get() | Resolved address / latest state seen locally |
peer.signal(query, equals?) | Subscribe; URLs long-poll after first subscription |
peer.send(data) | Fire-and-forget durable message |
CLI08
node src/cli.ts push app.cell --db app.db
node src/cli.ts serve --db app.db --port 8080
node src/cli.ts serve --db app.db --port 8080 --hydrate
node src/cli.ts call actionName arg --db app.db
node src/cli.ts push app-v2.cell --to http://host:8080
node src/cli.ts run file.cell --db app.db
node src/cli.ts state --db app.db
pnpm lsppush sets a database root; call and serve invoke it. A remote push remounts a serving root on the same port. run remains the direct file compatibility workflow.
EDITOR / LSP04
| Feature | Behavior |
|---|---|
| Semantic highlighting | Classifies Cell keywords, types, functions, variables, parameters, strings, numbers, comments, and operators |
| Error highlighting | Publishes parser and static-checker diagnostics on open and every full-document change |
| Hover | Shows inferred types and API documentation for local bindings, built-ins, records, state, signals, and collections |
| Completions | Suggests keywords and in-scope names, plus context-aware members after . and JSX tags after < |
Run the dependency-free stdio server with pnpm lsp and configure your editor for .cell files. It uses LSP 3.17 full-document sync and UTF-16 positions; rename and go-to-definition are deliberately out of scope.
examples/01-basics.cellexamples/02-todos.cellexamples/03-migrate.cellexamples/04-vcs-hmr.cellexamples/05-deployments.cellexamples/06-platform/examples/07-remote/examples/08-web/examples/09-notes/examples/10-presence/examples/11-graph/Now push a new root.
Name the cells in examples/11-graph/hello.cell, add a secret behind useAction, then push it into a database and serve with --hydrate. Change the artifact and deploy it live with push --to; the port and named state stay put.