CELL · V2 / FIELD GUIDE

CELL

The hook is the program.
The database keeps it alive.

THIRD EDITION · V2

Push a root into the database, declare named state where it is used, and expose only the artifacts and capabilities clients need.

MOUNT TRACEAPP / 01

ROOT.
HOOKS.
ARTIFACTS.

USESTATEpersistent signal

type and value live together

FRONTENDJSX artifact

holes are reactive edges

HELLO.CELL
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>
)
RUNS AThttp://localhost:8080
RUN THE APP

11 chaptersOne file to the engine underneath

00START HERE

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.

REQUIRESNode.js 23+
BUILD STEPNone
RUNTIME DEPSNone
TERMINAL · SERVE
node src/cli.ts push examples/11-graph/hello.cell --db hello.db
node src/cli.ts serve --db hello.db --port 8080
OPENhttp://localhost:8080
TERMINAL · INSPECT
node src/cli.ts state --db hello.db
REPORTSschema: mounted cell storage
value: root source, modules, and cells
seq: committed write count

Trace what just happened

  1. Push. Cell stores the file source as the database's root program—an ordinary data write.
  2. Invoke. serve parses, checks, and mounts that root in hook mode; call invokes an exported root action.
  3. Wire. useState declares a typed cell; a name such as "title" keeps its identity stable when code moves.
  4. Serve. frontend(JSX) selects a frontend artifact for LiveView or hydration while the port stays deployment configuration.
RETURN 01 · RECALL

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.

01LANGUAGE

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.

IMMUTABLEconst answer = 42
LOCAL MUTABLElet total = 0
RECORD{ name: "Ada" }
COLLECTION[1, 2, 3]
FUNCTION(x: number) => x + 1
JSX VALUE<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.

MODES.CELL
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)
  }
}
FormPurposeImportant behavior
if (c) a else bConditional expressionProduces a value; either branch may be a block
match (x) { ... }Enum eliminationAnnotated enums must be handled exhaustively
for (const x of xs)Imperative collection loopUseful with a local let; persistent writes still use actions
throw valueUnrecoverable failureNo try; model expected failure with an enum
CONTROL.CELL
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"
  }
02TYPES

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.

recordstructural product{ name: string }
enumtagged sumenum { some: T, none }
hidenominal sealtype Id = hide number
TYPES.CELL
type 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 }
PRIMITIVESnumber string booleanscalar values
DATAdataserializable values
COLLECTIONT[] · T{}same identity-bearing container
FUNCTIONSfn(A) => Radd action for effects
SINGLETONjust(value)exactly one inhabitant

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

DEFINITION-TIME CHECKER

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.

03MODULES

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 region
→
cell call / serveinvoke root
→
{ values, hooks, artifacts }ordinary exports

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

MODULES.CELL
import tools from "./tools.cell"

const offset = 2
const double = (n: number) => n * offset

return {
  answer: double(21),
  transform: double,
  tools
}
RETURN 02 · CODE IS DATA

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.

04HOOKS

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.

ENTRYmodule body
CLI mounts
FRAMEstable tree path
use* declares
CELLSpersistent signals
hook returns
ARTIFACTfrontend + capabilities
NESTED-HOOKS.CELL
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>
)
01 · DECLARATIVENo effects in the body

Hook bodies wire cells and signals. Put mutation, I/O, and event work in action callbacks.

hook () => wiring
02 · COMPOSABLECalls become child frames

Two invocations of the same hook receive different hierarchical paths, so identically named child cells remain independent.

/0.count · /1.count
03 · DYNAMICKeys can own frames

signal.each(hook (...)) mounts one persistent hook frame per key and tears down only its live wiring when the key leaves.

base@key.count
05PERSISTENT STATE

TYPE · 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.

STATE-HANDLE.CELL
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 action
cell.signalthe underlying reactive value
cell.map(fn, equals?)derive another signal
cell.set(value)replace after type coercion
cell.update(fn)pure current-value transition
cell.refstorable authority to read or rewrite this cell

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

06FRONTEND

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.

APP MODULEJSX + signals
server renders
HTMLholes + capability ids
browser event
ACTIONserver mutation
signal update
PATCHhole, attribute, or list op
HELLO-WEB.CELL
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>
)
ARTIFACT

frontend(vnode) is the "frontend" convention over the general artifact primitive.

HOLES

Static values render once; signals subscribe and receive fine-grained updates.

PUBLIC ACTION

Ordinary event actions run locally when hydrated; useAction deliberately stays authoritative.

CLIENT

Choose a tiny event/patch client or hydrate the root for optimistic public-cell rendering.

CHOOSE THE CLIENT
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.

VISITS.CELL · READ SOVEREIGNTY
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.

07REACTIVITY

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.

SOURCEtodos
map
DERIVEDtodos.map(...)
hole
KEYED COLLECTION<li key={...}>
diff
WIRErm · set · ord
TODOS.CELL
type 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.

map

Derive a signal. Supply an equality to suppress equivalent results.

out

Attach an action observer; used directly in lower-level hooks.

each

With a hook builder, mount one persistent frame per key and stop its live wiring when it leaves.

stop

Detach a node; stopping an each also tears down its scopes.

DYNAMIC-HOOKS.CELL
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.

08MIGRATIONS

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.

V1stringstored payload
MIGRATE
FORTHstring → Nameclosure stored
STORE
V2{ first: string }latest native shape
BACK
OLD CODEstringtranslated view
APP + MIGRATION
// 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.

09ENGINE & PEERS

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.

STATE{ schema: type,
value: schema }
db.evolvepure State → State
COMMITtype check
atomic persist
signal propagation

V2 root programs and useState are the mounted-hook interface to this persistent engine.

OWNERSHIPspawn / open

A database creates and controls a child. Use this only for environments genuinely owned by the parent.

parent → child
SUBSCRIPTIONconnect / signal

A peer reads another sovereign environment without gaining write access.

subscriber ← state
MESSAGEsend / inbox

Data enters the owner's durable mailbox. Only the owner's own action decides its meaning.

sender → owner policy
SurfaceUse it forStatus
push + call / serveSet or invoke a database-resident root programPreferred host path
hook + use* + artifactCompose mounted programs, state, authority boundaries, and outputsPreferred language path
db.listen(port, page, props)V1 browser interpreter with projected props, local tab DB, and sessionsCompatibility path
db.listen(port)Sovereign database state, long-poll subscriptions, durable inbox transportInfrastructure API
LIVE DEPLOY · SAME PORT
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.

Deliberately out of the v2 surface: the experimental proof helpers were removed from current globals. Distributed consensus, a merge algorithm, and exactly-once delivery also remain policy rather than substrate.
10REFERENCE

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
FormMeaning
number string booleanPrimitive scalar types
dataSerializable data, including closures whose captures are data and first-class references; excludes live signals and host built-ins
typeThe 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) => RPure function type; prefix with action for effects
type Pair<A, B> = ...Generic type alias; type-level computation
type Id = hide numberNominal type with controlled create/eliminate capabilities
just(value)Singleton type inhabited by exactly that value
BINDINGS & CONTROL08
FormBehavior
const x: T = valueImmutable binding; annotation checks and seals
let x = value / x = nextMutable local binding
for (const x of xs) { ... }Iterate a collection in insertion order
(x: T): R => valuePure 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 valueAbort for unrecoverable errors; no catch construct
HOOKS & ARTIFACTS11
NameBehavior
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
NameBehavior
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.signalUnderlying signal node
cell.set(value)Replace the cell after coercion to T
cell.update(pureFn)Pure current-value transition
cell.refStorable, runtime-minted CellRef for this cell
ref.read(value) / .write(value, next)Pure lens operations over database value data
cell.map/out/each/stopDelegated signal members
SIGNAL MEMBERS05
MemberBehavior
.map(pureFn, equals?)Derived signal; equality suppresses equivalent values
map source => expressionSugar 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
MemberBehavior
[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 / .sizeElement 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
NameRole
Option(T), some(v), noneStandard 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
paramsCLI/load/use parameters as a struct of strings or supplied data
io.log(...values) / io.read(path)Effectful output / file read
Ref / DbRef / CellRefCollection element, child database, and persistent-cell reference types
dbCurrent sovereign database handle
DATABASE & PEERS · LOWER LEVEL11
MemberBehavior
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
COMMANDS
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 lsp

push 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
FeatureBehavior
Semantic highlightingClassifies Cell keywords, types, functions, variables, parameters, strings, numbers, comments, and operators
Error highlightingPublishes parser and static-checker diagnostics on open and every full-document change
HoverShows inferred types and API documentation for local bindings, built-ins, records, state, signals, and collections
CompletionsSuggests 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.

01Language, types, nominal valuesexamples/01-basics.cell
02Persistent todos on v1examples/02-todos.cell
03Whole-state migrationexamples/03-migrate.cell
04VCS + hot reloadexamples/04-vcs-hmr.cell
05Owned deploymentsexamples/05-deployments.cell
06Sovereign deploy platformexamples/06-platform/
07Network peer + auth policyexamples/07-remote/
08Legacy browser peerexamples/08-web/
09Projected props + capabilitiesexamples/09-notes/
10Sessions + live presenceexamples/10-presence/
11V2 hooks, secrets, artifacts, keyed collectionsexamples/11-graph/
TRACE COMPLETE · THIRD EDITION

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.

RETRACE FROM THE START