CELL / FIELD GUIDE

CELL

A database and a language,
under one roof.

SYSTEM OBSERVATION

The database stores one typed value. Code is data. Everything else is a program.

STATE EXPERIMENT01 / 01

ONE VALUE.
ONE TYPE.
ONE WRITE.

SCHEMAtype

defines the shape

VALUEschema

inhabits that type

FIRST.CELL
const Greeting = struct { message: string }

const main = action () => {
  db.evolve((s) => ({
    schema: Greeting,
    value: { message: "hello, cell" }
  }))
  io.log(db.get().value.message)
}

return { main }
OUTPUThello, cell
RUN THE FIRST PROGRAM

11 chaptersFrom first value to full-stack app

00START HERE

15 MINUTES · FIRST CONTACT

Run Cell, then inspect what persisted.

Cell programs are modules. The module loads purely, exports an effectful main action, and the CLI calls it. Your database persists beside the program unless you choose another path.

REQUIRESNode.js 23+
BUILD STEPNone
RUNTIME DEPSNone
TERMINAL · RUN
node src/cli.ts run examples/01-basics.cell
OUTPUTalice is an adult
bob is a minor for 3 more years
point: { first: 3, second: 4 }
TERMINAL · INSPECT
node src/cli.ts state \
  --db examples/02-todos.cell.db
REPORTSschema: the stored type
value: the stored value
seq: committed write count

The execution model

  1. Load. The file is parsed, checked, and evaluated as a pure module.
  2. Export. The module's return value becomes its exports.
  3. Run. If the exports contain main, it must be an action and the CLI calls it.
  4. Stay alive when needed. An inbox, listener, or peer subscription keeps a program running like a server.
RETURN 01 · RECALL

A fresh Cell database already contains something. What?

Reveal

The unit value {} at type struct {}, with sequence 0. Initialization is simply the first migration away from that state.

01LANGUAGE BASICS

VALUES · EXPRESSIONS · CONTROL FLOW

Small syntax. Explicit effects.

Cell is expression-oriented and immutable. Newlines and semicolons both separate statements. Blocks return through an explicit return; there is no reassignment and no exception recovery construct.

NUMBER42 · 3.14 · -7
STRING"cell\nline"
BOOLEANtrue · false
ARRAY[1, 2, 3]
STRUCT{ name: "Ada", active: true }
UNIT{}

Bindings and annotations

const creates an immutable lexical binding. An annotation checks and coerces the value; at nominal boundaries, that is also where raw values become sealed brands.

const answer: number = 42
const person = { name: "Ada", year: 1815 }
const renamed = { ...person, name: "Augusta" }

Functions come in two kinds

A plain arrow is pure. An action may perform effects. Pure code can call only pure code; actions can call both. The rule is checked at definition time when possible and enforced at runtime through every call depth.

FUNCTIONS.CELL
const square = (n: number): number => n * n

const report = action (n: number) => {
  io.log("square:", square(n))
  return {}
}

// Effectful function types say action fn
const useReporter = (f: action fn(number) => data) => f

return { square, report }
LevelOperatorsNotes
Unary! -Boolean not; numeric negation
Multiply* / %Numeric operations
Add+ -+ also concatenates strings
Compare< > <= >=Numbers, or strings for ordering
Equality== !=Deep structural equality; nominal brands matter
Logic&& ||Boolean logic, lowest precedence
CONTROL.CELL
const abs = (n: number): number =>
  if (n < 0) -n else n

const fact = (n: number): number =>
  if (n <= 1) 1 else n * fact(n - 1)

const requireName = (name: string): string => {
  if (name == "") throw "name is required"
  return name
}

Control flow is made of expressions

if (condition) a else b produces a value. Either branch may be a block. Functions may recurse.

throw is for unrecoverable failure. Cell deliberately has no try; expected failure belongs in an enum such as Option(T) or your own Result.

Comments and strings

Use // line comments or /* block comments */. Strings support \n, \t, \", and \\ escapes.

02TYPES & PROOFS

STRUCTURAL CORE · NOMINAL CAPABILITIES

Types are values you can compute.

The primitives are number, string, boolean, data, and type. Types can be bound, compared, passed to functions, returned, and stored as the database schema.

structintersectionextra fields are fine
enumunionone exhaustive variant
hidenominal sealcapability-controlled

Structs: width-subtyped records

A value may have more fields than the struct type requires, but not fewer. Struct literals support shorthand fields and spread.

Enums: tagged alternatives

A variant may carry one payload. Construct it through the type value, then destructure it with match. Annotated matches are checked for exhaustiveness; _ is the wildcard.

SHAPES.CELL
const Named = struct { name: string }
const ada: Named = { name: "Ada", role: "mathematician" }

const Shape = enum { circle: number, square: number, dot }

const area = (s: Shape): number =>
  match (s) {
    circle(r) => 3 * r * r,
    square(w) => w * w,
    dot => 0
  }
ARRAYT[]ordered values
COLLECTIONT{}stable identities
PURE FUNCTIONfn(A) => Rrepeatable
ACTIONaction fn(A) => Reffectful
REFERENCESRef · DbRefpersistent identity
TYPE-LEVEL COMPUTATION

Generics are ordinary functions.

Module-load time is compile time. A function can accept values of type type and return a new type. No angle-bracket syntax or separate generic system is needed.

PAIR.CELL
const Pair = (A: type, B: type) =>
  struct { first: A, second: B }

const point: Pair(number, number) =
  { first: 3, second: 4 }
INSIDE MODULE
numbertransparentUserId
ANNOTATION SEALS → OUTSIDE MODULE
UserIdopaquecreate / elim

hide makes a nominal capability

hide UserId = number binds a plain struct containing type, create, and elim. Inside the defining module, raw numbers and UserId are transparent to one another. An annotation seals values at the boundary.

Export { UserId: UserId.type } to reveal only the opaque type. Export { UserId } to grant creation and elimination capabilities.

NOMINAL.CELL
hide UserId = number

const issue = (n: number): UserId => n

// Only the type crosses the module boundary.
return {
  UserId: UserId.type,
  issue
}
PROOF.CELL
const claim = { claim: "positive" }
const p = verify((n: number) => n > 0, claim, 5)
const q = axiom(claim)

const grant = (proof: Proof(claim)): string =>
  "granted"

// p == q: proof irrelevance
return { result: grant(p), same: p == q }

Proofs are branded, storable evidence

verify(predicate, proposition, subject) runs a pure decidable check. axiom(proposition) asserts the same evidence without checking. Proof(proposition) is a singleton nominal type, so evidence for another proposition is rejected and any two proofs of the same proposition compare equal.

DEFINITION-TIME CHECKER

At function creation, Cell reports definite field typos, operand and arity mistakes, non-exhaustive matches, purity violations, and opaque-brand access. Unannotated dynamic code remains allowed and is checked when values cross runtime boundaries.

03MODULES & CODE

PURE AT LOAD · SOURCE AS DATA

A module is a pure function.

Top-level evaluation is pure, and the module's return is its exports. That makes importing conceptually identical to calling a function—and makes source text safe to store, version, query, and reload.

import x from "./x.cell"statement sugar
use("./x.cell")file + cache
load(source)text from anywhere
{ exports }ordinary value
MODULES.CELL
import std from "./lib/std.cell"

const source =
  "return { double: (n: number) => n * 2, seed: params.seed }"

const generated = load(source, { seed: 7 })
const configured = use("./worker.cell", { mode: "fast" })

return {
  answer: generated.double(21),
  seed: generated.seed,
  transact: std.transact
}

use and import

use(path, params?) resolves relative to the current module. Without params, file modules are cached by absolute path and circular imports are rejected. import name from "path" is sugar for the one-argument form.

load

load(source, params?) evaluates source text without filesystem access. Loaded modules receive params, just like CLI parameters, and mint nominal brands from their content identity.

Effects enter through actions

io.log(...) and io.read(path) are actions. Reading source from disk therefore happens before passing that string into the pure load operation.

RETURN 02 · CODE IS DATA

Why does Cell store source text instead of closures?

Reveal

Closures capture runtime environments and do not serialize honestly. Source is readable, diffable data; load revives it into a module. The database rejects stored functions and tells you to store source instead.

04THE DATABASE

STATE · EVOLVE · ATOMIC COERCION

The database is one dependent pair.

Every store holds { schema: type, value: schema }. There is one write primitive: db.evolve. The transition is pure, the returned value is checked against the returned schema, and the pair commits atomically or not at all.

BEFORE · SEQ 8{ schema: V1,
value: { n: 1 } }
db.evolvepure State → State
AFTER · SEQ 9{ schema: V2,
value: { n: 2, s: "hi" } }

schema check + data check + atomic persist + signal propagation

EVOLVE.CELL
const V1 = struct { n: number }
const V2 = struct { n: number, note: string }

const main = action () => {
  // initialize
  db.evolve((s) => ({ schema: V1, value: { n: 1 } }))

  // transaction: preserve schema
  db.evolve((s) => ({
    schema: s.schema,
    value: { n: s.value.n + 1 }
  }))

  // migration: change both
  db.evolve((s) => ({
    schema: V2,
    value: { ...s.value, note: "ready" }
  }))
}

Initialization is a migration

A fresh database begins at { schema: struct {}, value: {} }. Moving to your first application schema is the same operation as every later migration.

Transactions are a two-line library

const transact = action (f: fn(data) => data) =>
  db.evolve((s) => ({ schema: s.schema, value: f(s.value) }))

db.get() is effectful

Reading the current store state is an action because the value changes over time. Inside an evolve transition, use the supplied state s; calling db.get() there would violate purity.

STATE.JSON

Atomic-rename snapshot containing sequence, serialized schema, and serialized value.

LOG.JSONL

An operation ledger records each committed sequence and timestamp.

REVIVAL

Types, nominal brands, refs, database refs, and collection identities survive restart.

GUARANTEE

An invalid next value throws before assignment, so the old pair remains unchanged.

05COLLECTIONS & REFS

TABLES WHEN YOU WANT THEM

Collections add stable identity to values.

An array is positional. A collection assigns every element a stable internal id and returns a Ref when you add a value. This is enough to recover tables and foreign keys without making them the database foundation.

#1{ title: "read", done: true }
#2{ title: "write", done: false }Ref(c1,#2)
#3{ title: "ship", done: false }

map changes values in place, so the reference still points to element #2 after a migration.

COLLECTION.CELL
const empty = collection([])
const added = empty.add({ title: "learn", done: false })
const todos = added.collection
const ref = added.ref

const done = todos.update(ref, (t) => ({ ...t, done: true }))
const found = done.get(ref)          // some(todo)
const entries = done.entries()       // [{ ref, value }]
const values = done.values()         // [todo]
const migrated = done.map((t) => ({ ...t, priority: 1 }))
const removed = migrated.remove(ref)

The collection API

.size
number of live elements
.add(value)
{ collection, ref }
.update(ref, pureFn)
new collection, same id
.remove(ref)
new collection without that id
.get(ref)
Option(T)
.values()
id-ordered T[]
.entries()
{ ref, value }[]
.map(pureFn)
identity-preserving transform

deref(container, ref) walks nested data to locate the referenced collection and returns an Option. A ref never grants mutation.

RETURN 03 · ONE VALUE

Is a collection a separate table outside the database value?

Reveal

No. A collection is an ordinary immutable data value nested anywhere in the one stored value. Its stable ids make table-like relationships possible without changing the database model.

06SIGNALS

PURE QUERIES · CHANGE PROPAGATION

The database is reactive by construction.

A source signal is a pure query over the state pair. It records a baseline when created and fires only after a commit changes its result. Observers are actions; the graph therefore pushes effects safely downstream from pure computation.

STATE{ todos, page }
query
SOURCEdone.length
map
DERIVED"2 complete"
out
EFFECTio.log(...)
SIGNAL.CELL
const doneCount = db.signal((s) =>
  s.value.todos.values().filter((t) => t.done).length)

const label = doneCount.map(
  (n) => str(n) + " complete",
  (a, b) => a == b
)

label.out(action (text) => io.log(text))

// Later:
label.stop()
01

No initial fire. Creation computes a baseline; do initial rendering explicitly.

02

Source suppression. db.signal(query, equals?) fires only when the query result changes.

03

Unforgetful map. map(f) forwards every upstream fire, even if its own output repeats.

04

Forgetful map. map(f, equals) suppresses equivalent derived values.

05

Custom equivalence. Equality can be any pure two-argument relation—even a domain-specific notion such as equal string length.

06

Stop downstream. stop() detaches a node and prevents its observers from running.

DYNAMIC GRAPH

sig.each(build) gives every key its own subgraph.

When a key appears, Cell runs an action that builds signals for that key. Every node created during the build is captured in its scope. When the key disappears, the entire scope is torn down. Duplicate keys create only one scope.

KEYS["lab", "attic"]
labtemp signalobserver
attictemp signalobserver
garagescope stopped

Cascades may write again

An out observer may call db.evolve, producing another propagation pass. Convergent loops settle because unchanged results are suppressed. A depth guard stops genuinely self-feeding cycles with a clear error after 64 nested fires.

07MIGRATIONS

EXPAND · MIGRATE · CONTRACT

A migration is an evolve that changes type and value.

Because readers are signals and collection refs survive map, the familiar expand–migrate–contract rollout is a composition of ordinary primitives—not a special migration subsystem.

V1nameold reader live
EXPAND
V2name · firstName · lastNameboth readers live
MIGRATE
V2new writes fill both formsstop old reader
CONTRACT
V3firstName · lastNamenew reader unchanged
MIGRATE.CELL · CORE OF EXAMPLE 03
// EXPAND: keep old field, add new fields
std.migrate(V2, (v) => ({
  people: v.people.map((p) => ({
    name: p.name,
    firstName: part(p.name.split(" "), 0),
    lastName: part(p.name.split(" "), 1)
  }))
}))

const newReader = db.signal((s) =>
  s.value.people.values().map((p) => p.firstName).join(" & "))

oldReader.stop()

// CONTRACT: refs survive; newReader's result does not change
std.migrate(V3, (v) => ({
  people: v.people.map((p) => ({ firstName: p.firstName, lastName: p.lastName }))
}))
RETURN 04 · ATOMICITY

Can new code observe an old schema inside one deployment?

Reveal

Not when code, schema, and data live in the same state pair and one evolve migrates all three. Across separate sovereign deployments, version skew is normal and can be handled with expand–migrate–contract or by inspecting a peer's first-class schema.

08PEERS & DEPLOYS

SOVEREIGN ENVIRONMENTS · THREE RELATIONS

No database gives up its writes.

Every store is sovereign. Environments relate through ownership, read-only subscription, or durable messages. “Upstream” is a per-query relationship, not a global hierarchy.

OWNERSHIPspawn / open

Parent → child. The parent created and controls the child database.

const eu = db.spawn("eu")
db.open(eu).evolve(...)
SUBSCRIPTIONconnect / signal

Subscriber pulls a read-only view from a peer.

const peer = db.connect(addr)
peer.signal(query)
MESSAGEsend / inbox

Sender pushes data; the owner's own action decides what it means.

peer.send(message)
db.inbox(action (msg) => ...)
YOUR LAPTOPdev.dbowns local writes
PLATFORMplatform.dbowns production writes
message: push source →
← subscription: watch status

Local directory or HTTP URL

db.connect(addr) accepts a database directory or an http(s):// URL. In-process peers deliver synchronously. Other processes are watched through the filesystem; remote subscriptions use HTTP long-poll.

Remote views are eventually consistent

peer.get() returns the newest state this process has seen. Starting a signal begins live listening; a send-only program holds no subscription and can exit.

Messages are durable, at least once

A file-peer message to an offline owner lands in its inbox directory and is consumed when an inbox handler starts. Handlers should tolerate retry. Authorization is ordinary owner policy in the handler.

REMOTE.CELL
const main = action () => {
  const platform = db.connect(params.platform)

  platform.signal((s) => s.value.status)
    .out(action (status) => io.log("live:", status))

  platform.send({
    kind: "deploy",
    source: io.read(params.source),
    key: params.secret
  })
}

return { main }
LISTENdb.listen(7070)

serves exactly

GET /statesnapshot
GET /waitlong-poll
POST /inboxmessages
Deliberately absent: distributed consensus, a built-in merge algorithm, and exactly-once delivery. Those are policies to express in Cell, not frozen substrate.
09THE BROWSER

THE TAB IS ANOTHER PEER

Full stack means the same language on both sides.

Pass a page query to db.listen(port, page). Cell serves an HTML shell, the dependency-free interpreter, and the page module returned by that query. The browser receives server and dom as its effect boundaries.

SERVER STATEdata + page source
page query
BROWSER CELLpure view + wiring
dom.render
DOMvisible interface
server.send
INBOXowner handles intent
SERVER · APP.CELL
const main = action () => {
  if (db.get().schema == struct {}) {
    const page = io.read("./page.cell")
    db.evolve((s) => ({
      schema: struct { todos: data, page: string },
      value: { todos: collection([]), page: page }
    }))
  }

  db.inbox(action (msg) => io.log("message:", msg))
  db.listen(num(params.port), (s) => s.value.page)
}

return { main }
BROWSER · PAGE.CELL
const item = (t: data): string => "<li>" + t.title + "</li>"

const view = (s: data): string =>
  "<h1>todos</h1>" +
  s.value.todos.values().map(item).join("")

const main = action () => {
  dom.render(view(server.get()))
  server.signal(view).out(
    action (html) => dom.render(html))

  dom.on("click", "#add", action (arg) =>
    server.send({ kind: "add", title: dom.value("#title") }))
}

return { main }
dom.render(html)replace the root HTML
dom.on(event, selector, action)delegate an event; data-arg becomes the action argument
dom.value(selector)read an input value
dom.list(selector, keys)create keyed list slots
dom.patch(key, html)replace exactly one keyed slot

Deploying UI is a data write

When the stored page source changes, connected browsers fetch the new module and hot-reload it without replacing server state. Combine signal.each, dom.list, and dom.patch to give every item its own subgraph: toggling one todo patches one slot with no virtual DOM.

RUN THE EXAMPLE
node src/cli.ts run examples/08-web/app.cell --param port=8080

Then open localhost:8080. Push another stored UI with examples/08-web/push-ui.cell.

10REFERENCE

EVERY IMPLEMENTED SURFACE

Keep this beside your editor.

This appendix covers the complete current built-in surface. Cell is a prototype; source and tests remain authoritative when it evolves.

TYPE FORMS08
FormMeaning
number string booleanPrimitive scalar types
dataAny serializable Cell data; excludes closures, builtins, signals
typeThe type of type values
struct { a: A }Structural record; extra fields accepted
enum { some: T, none }Tagged union
T[] / Array(T)Array type
T{} / Collection(T)Identity-bearing collection type
fn(A) => R / action fn(A) => RPure or effectful function type
GLOBALS & BUILT-INS15
NameResult / role
Option(T), some(v), noneStandard optional-value enum
Proof(p), verify(pred,p,x), axiom(p)Exact branded evidence
collection(array?)New collection; empty when omitted
str(value)Display conversion to string
num(stringOrNumber)Numeric conversion; throws for invalid text
assert(bool, message?)Returns unit or throws assertion failure
deref(container, ref)Find referenced value anywhere in nested data
load(source, params?)Purely evaluate source text
use(path, params?)Evaluate a file module relative to current module
io.log(...values)Effectful stdout logging
io.read(path)Effectful file read, available in file modules
paramsStruct passed by CLI, load, or use; empty unit by default
dbThe current sovereign database handle
RefCollection element reference type
DbRefChild database reference type; values expose .name
ARRAY & STRING MEMBERS11
MemberBehavior
array.lengthElement count
.map(pureFn)Transform each element
.filter(purePredicate)Keep matching elements
.reduce(pureFn, initial)Fold left
.find(purePredicate)First match as Option(T)
.each(fn)Call for every element; an action callback may have effects
.at(index)Element as Option(T)
.concat(other)Combine arrays
.append(value)New array with one value added
.join(separator)Display elements and join as string
string.length / string.split(separator)Character count / string array
COLLECTION MEMBERS08
MemberBehavior
.sizeLive element count
.add(value){ collection, ref }
.update(ref, pureFn)Replace one value while keeping identity
.remove(ref)Delete one element
.get(ref)Value as Option(T)
.values()Values ordered by element id
.entries(){ ref, value }[]
.map(pureFn)Transform all values, preserving ids and refs
DATABASE HANDLE08
MemberBehavior
db.get()Current { schema, value }
db.evolve(pureTransition)Atomic checked write
db.signal(query, equals?)Reactive pure query
db.spawn(name)Create/open owned child and return DbRef
db.open(dbRef)Child database handle
db.connect(address)Read/send peer handle for directory or URL
db.inbox(actionHandler)Handle pending and future messages
db.listen(port, pageQuery?)Serve peer protocol; optionally browser runtime
SIGNAL MEMBERS04
MemberBehavior
.map(pureFn, equals?)Derived signal; without equality it forwards every upstream fire
.out(action)Effect observer, returns a stoppable signal node
.each(actionBuilder)One scoped subgraph per distinct key in an array signal
.stop()Detach this node; an each also tears down its scopes
PEER HANDLE04
MemberBehavior
peer.addrResolved directory or normalized URL
peer.get()Latest state seen locally
peer.signal(query, equals?)Subscribe; URL long-poll starts on first call
peer.send(data)Fire-and-forget message; data values only
CLI03
COMMANDS
node src/cli.ts run file.cell
node src/cli.ts run file.cell --db path/to/store --param key=value --param other=value
node src/cli.ts state --db path/to/store

All CLI parameters arrive as strings in params; convert numbers explicitly with num(params.port).

01Language, types, proofsexamples/01-basics.cell
02Persistent todosexamples/02-todos.cell
03Live 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/
08Browser runtime + keyed DOMexamples/08-web/
TRACE COMPLETE

Now read the examples as one program.

Each advanced example recombines the same few ideas: values, types, one atomic evolve, pure queries, effectful observers, and source as data.

RETRACE FROM THE START