---
title: "Start here: what Ursprung is, and how it gets decided"
date: 2026-08-06
summary: >-
  A standalone primer rather than a daily update, for somebody who has read none
  of the log: what Ursprung is, how the wayfinder map plans it, and where the
  whole thing stands after five days. It covers the settled axioms, the
  five-step build, the rules an app author follows, and what is still open. Two
  warnings first — almost none of it exists as code yet, and the plan keeps
  getting smaller.
---

# Start here: what Ursprung is, and how it gets decided

_This is not a daily update. The day-one through day-five posts are a running
log, and each one assumes you read the last. This one assumes nothing. It
describes the whole project as it stands on 2026-08-06, in plain words, for
somebody who has never seen it before. The daily log resumes tomorrow._

Two warnings before the tour.

First, **almost none of this exists as code yet.** The published `ursprung`
package ships one thing: the TypeScript compiler settings that every Ursprung app
extends. Everything else below is a plan. That is deliberate.

Second, the plan mostly gets **smaller**. Five days of work removed a transpiler,
a bundler, a parser, a wasm toolchain, a CSS pipeline, a dev server and a command
line. Read what follows as a series of subtractions, because that is what it has
been.

## How the work runs

Ursprung is planned with a method called **wayfinder**. It works like this.

A large idea arrives. It is too large for one agent session, and the way to the
end is not visible. So you write one **map** file, and one **ticket** file for
each open decision. Every ticket asks a question and settles a decision. A ticket
never builds a feature.

One session resolves one ticket. The session writes the answer into the ticket,
closes it, and adds one line to the map. Each answer clears some fog, and new
tickets become possible. You repeat until nothing is left to decide.

Tickets come in four types. **Research** tickets send an agent off to read
sources. **Prototype** tickets build a throwaway spike to react to. **Grilling**
tickets are conversations, and they are the common case. **Task** tickets do
manual work that a decision waits on. Work that turns out to sit past the goal is
ruled **out of scope** and never comes back to this map.

For Ursprung the files are all in the repository. The map is
`.scratch/ursprung-v0/map.md`. The tickets are in `issues/` beside it. Ten
throwaway spikes and nine research notes sit in `spikes/` and `research/`.

Today the count is **41 tickets: 21 resolved, 5 out of scope, 12 open.**

## Where it is going

The goal is one document — a written v0 architecture specification. It has to
lock the module boundaries, the build stages, the resumability format, the module
resolution policy and the public API. It has to be complete enough to implement
without reopening any architecture.

Spikes are part of that goal, not a side activity. Ursprung's combination of
choices has no prior art, and the nearest reference points changed their minds
only after building. Qwik shipped without a virtual DOM and then added one, and
rewrote its serialization format twice in three years. So some questions must be
answered by a rough artifact rather than by argument.

## What Ursprung is

Ursprung is a full-stack web framework and a **manifest generator**. It supplies
JSX, server components, client components, server-side rendering, and resumable
client-side rendering.

It is easier to say what it is not.

- It is **not a transpiler**. It does not strip types and it does not transform
  JSX.
- It is **not a bundler**. It never bundles anything.
- It is **not a checker**. There is no `ursprung check`.
- It has **no command line**. It has no commands at all.

An app installs `ursprung`. If it wants styling, it also installs
`@tailwindcss/cli`. The producer of the app then runs three tools that Ursprung
neither owns nor wraps: `tsc`, `@tailwindcss/cli` and `wrangler`.

## Agents are the audience

This is the strongest rule on the map, and it explains most of the odd-looking
ones. **Coding agents write Ursprung apps. People do not.**

That inverts several usual trade-offs. Conventions that save typing are worth
little, because an agent does not get bored. Rules are cheap, provided you state
each rule exactly and the error message names the fix. Magic and inference are
liabilities, because an agent cannot reason its way out of them. Types are
valuable, because they are the one feedback channel an agent can act on before it
runs anything.

The sharp form is called **formerly unreasonable expectations**. A rule that a
human author would have refused — repetitive, verbose, easy to forget — is
available now. Ask three questions of it. Is the rule stated exactly? Can
something detect a violation? Does the error message name the fix? If all three
hold, take the rule, however cumbersome it would once have been.

The payoff is that **every such rule deletes machinery from the build**, and
machinery is where the failure modes live.

There is a live problem with the second question, and it is an open ticket. The
test was written when the build had a full parser. The build now analyses no
language at all, so several rules on the map are enforced by nobody. Ticket 41
goes through them one at a time.

## The settled rules

These are not open questions. Every session works inside them.

**Server target.** Cloudflare Workers and workerd only. No adapter layer, no
portability shim.

**No legacy.** ES modules only. No CommonJS. No lowering for old browsers. Use
the newest platform APIs rather than the intersection of what everything
supports. When reach fights modernity, modernity wins.

**Build host.** Whatever work is left for the build must be able to run inside a
worker. This is a capability bar, not a location, and it buys exactly one code
path by construction.

**Dependency budget.** Ursprung depends on almost nothing, and every exception is
named. Inside the build there is now **one package**: `es-module-lexer` 2.3.1,
its pure-JavaScript entry, 27,504 bytes, no dependencies of its own. `capnweb`
handles browser-to-server calls. `wrangler` is a types-only peer dependency. The
signals polyfill is **vendored** — copied in and owned outright — so it is
Ursprung source rather than a dependency at all.

**Rendering runtime.** Signals, no virtual DOM, no React. The public words are
`signal()`, `computed()` and `effect()`.

**Server components.** Resumability, not hydration. The server writes the state
of each signal and each handler reference into the document. The client resumes
from that. It does not run the component code again on load.

**No splicing.** **Ursprung never edits your source.** It reads, refuses, hashes
and emits the bytes unchanged. Every output byte is an input byte. There is no
Ursprung-authored text in the middle, and no vendor-authored text either.

**Styling.** Tailwind only, and Tailwind now sits outside Ursprung entirely. The
producer runs the Tailwind command line. The compiled stylesheet is an **input**
to Ursprung's build, never a product of it. Ursprung treats it as opaque bytes,
content-hashes it, records it in the manifest, and links it from the document
head. Ursprung never parses CSS.

**Browser-to-server calls.** capnweb, pinned to an exact version. Ursprung does
not design a wire protocol. The delegation stops at the wire: capnweb's
serializer cannot express what resumability needs, so Ursprung ships a second
serializer of its own, deliberately.

## How an app gets built and shipped

The whole build is five steps: **lex, walk, refuse, hash, manifest.**

The input is JavaScript. An agent either writes JavaScript directly, or runs
`tsc` in the sandbox it already has. Ursprung lexes each module to get a list of
import specifiers, walks the graph, refuses what breaks the rules, hashes the
files and writes the manifest.

Measured, inside a real workerd isolate: **28 milliseconds cold and 8 warm, for
201 modules.** workerd costs nothing over Node for this work.

There are **two ways an app reaches Cloudflare, and one library serves both.**

A **dynamic app** is built in memory. An agent hands raw source to a worker, that
worker calls the Worker Loader, and the build runs about 3 milliseconds before
the app serves. No deploy cycle. This path is the reason the build-host rule
exists at all — and finding that out was the single largest change to the plan so
far.

A **deployed app** is built on the producer's machine, in plain Node or Bun,
before `wrangler deploy` uploads it. A deployed worker cannot read its own module
source text, so it cannot build itself. That was measured, not assumed.

Both callers import the same library and get the same output shape. Since there
is no command to hide it behind, `ursprung/build` is public API.

The free Cloudflare plan is enough. Work at module scope is charged against a
startup gate of about one second, measured on production: 936 milliseconds of
startup work deploys and serves, and about 1.16 seconds is refused.

## What an app author has to do

These are the rules with teeth. Most of them exist because they let the build
delete something.

**Filenames carry the realm.** A module named `*.server.js` runs on the server, a
module named `*.client.js` runs in the browser, and everything else is universal.
The build refuses a bad import at resolution time, before it even reads the
server module. There is no per-export control — split the file instead. **A
universal module may not import server code.**

**Imports name the real file, with its extension.** The build does not probe for
files. Bare package names work anywhere for `ursprung` itself, and only inside a
server module for anything else. Third-party code is refused in the client graph,
because a framework with no bundler cannot absorb a 200-module library.

**Routes live in one explicit table.** It is a flat list of `route()` calls with
absolute string paths and a lazy page import. File-based routing was rejected.
Ursprung executes that table at build time.

**Handlers are declared at module scope and exported.** A handler's identity is
its module URL plus its export name — written by the author, stable across
builds. **Inline arrow functions as JSX handlers do not exist.** This is the most
visible break from React habits, and it is accepted knowingly. In exchange, the
build needs no extraction step and no scope analysis whatsoever.

**A signal on the server must be created inside a request.** Cloudflare runs
module scope once at startup, so a violation fails at deploy time rather than at
some user's request.

**A signal may hold JSON values, `bigint` and `Date`. Nothing else.** This closed
value domain is what deletes most of the resumability machinery: with no cycles
and no shared identity, the back-reference table, the two-pass inflate and the
delta-encoded ids all disappear.

**One TypeScript program, strict.** Ursprung generates no code. Your Worker
bindings are inferred from your own `cloudflare.config.ts` rather than generated
into a file that can drift. The JSX element types are hand-written and use real
HTML attribute names, so `class`, not `className`.

## What has already been decided

One line each, and each one is a closed ticket with the argument written out.

**No bundler for either output.** One output module per input module. Client
modules are content-hashed and resolved through an import map, with
`modulepreload` covering a route's graph.

**A component boundary is a pair of HTML comments** — `<!--u7-->` and
`<!--/u7-->`. Qwik's premise reproduced exactly: the DOM's element model cannot
express a component boundary. But the conclusion does not follow, because comment
nodes are not part of that model. A resumed document finds a boundary and
replaces its subtree **with no component code on the page at all.** Cost: 21.9
raw bytes per component.

**The resumption payload is JSON in an inert script tag**, read by a 221-byte
reviver. Executable JavaScript was refused: it earns its keep on cycles and
identity, both now ruled out, and it would make the document uncacheable.
capnweb's encoding shape is copied as a specification rather than installed as a
dependency, on arithmetic — 14,418 bytes gzipped against 221.

**Nothing in Ursprung emits.** The question of which tool should transpile
dissolved once somebody asked who the transpile was for. The answer was nobody.

**A lexer, not a parser.** The build performs no language analysis whatever.

**There is no command line.** Every command collapsed, each for its own separate
reason.

**Ursprung does not write a dev server.** `wrangler` drives everything through
its own build command and watch loop, so the development loop and the deploy path
read the same configuration with the same tool. Fidelity becomes unbreakable
rather than maintained.

Underneath those sit the surveys that made them possible: oxc, the TC39 signals
proposal, resumability prior art, the workerd platform, capnweb, workerd-hosted
builds, wasm on production Cloudflare, and Tailwind's programmatic surface.

## What is still open

Seven tickets can be worked today.

The most uncomfortable is **ticket 35**: the server graph's import specifiers may
not resolve at all under an unbundled upload, and no ticket had noticed. Source
names `./foo.ts`, the emit produces `.js`, and the no-splicing rule forbids the
rewrite. It is a hole in two closed tickets, and three other tickets wait behind
it.

**Ticket 37** is the resume runtime: how a signal finds the text node it drives,
what code runs first, and how a click that arrives too early is captured.
**Ticket 41** asks which stated rules anything still enforces. **Ticket 23**
settles route matching and its error messages. **Ticket 33** asks whether layouts
are ordinary composition or a routing feature. **Ticket 39** asks whether v0
states its browser assumption as plainly as it states its server one. **Ticket
14** decides where the one compiled stylesheet is served from.

Five more are blocked behind those: handler addressability, a measurement of
whether server module count costs latency, the capnweb integration, keyed list
reordering without a diffing structure, and the exact shape of `ursprung/build`.

Beyond the tickets lies the fog — real work that is not yet sharp enough to
ticket. Client-side navigation payloads. The event capture layer. Asset and
response precedence. Which `node:` builtins server code may use. The semantics of
effects. Streaming rendering. Diagnostics as an agent interface, which has
collected five specimens of confident, wrong advice from other tools and one
structural problem: Ursprung's build now runs as a child of wrangler, so it does
not even own the output stream it writes into.

## What has been ruled out

Transpilation in every form. A CSS pipeline of Ursprung's own. Any styling path
other than Tailwind. Running Tailwind. Minifying the app's JavaScript. Non-Workers
server targets. Legacy support of every kind. React compatibility. Writing a
TypeScript parser. Native app targets. A data layer with schemas, sync and
loading.

Each returns only if somebody redraws the goal — and then as a fresh effort with
a fresh map, not as a resumption of this one.

## The shape of it

Strip the detail away and Ursprung is a small claim: if the author is an agent
rather than a person, a web framework can ask for things a framework has
historically had to infer — and every one of those asks buys back a piece of
machinery it no longer has to own.

Five days in, that has cashed out as a build of five steps, one 27-kilobyte
dependency, no parser, no bundler, no transpiler, no command line, and a rule
that Ursprung never edits a byte you wrote.

The open question is how much of that survives contact with the parts nobody has
built yet.
