13 KiB
writeonce — the .wo Language and Runtime
A declarative programming language with database and subscription-native HTTP in its standard runtime. Like
go run, you write.wofiles and execute them — but your program is a full-stack application.
What writeonce is
writeonce is a programming language, a standard runtime, and a toolchain. Three layers of one product:
- The language —
.wosource files. Declarative by default (type,class,service,policy,on <event>) with a hybrid SQL+Cypher query sublanguage for the imperative parts. Types, queries, transactions, subscriptions, policies, triggers, HTTP endpoints, and UI screens are all first-class language constructs. Aclassis atypeplusfnmethods (selfreceiver, transactional) — state and behavior, no inheritance (plan 13). - The runtime — an ACID multi-paradigm database (relational + document + graph), an HTTP server, a subscription engine, and a scheduler. All of it links into a single binary with your program. No external Postgres, no external Redis, no separate Node process.
- The toolchain — the
wocommand:wo run,wo build,wo test,wo fmt,wo mod,wo gen. Modelled directly on the Go toolchain. One binary per project; no runtime to install on the target host.
The one-line pitch: Go + Postgres + net/http + Phoenix LiveView, folded into one language and one binary.
Hello, world
Full example projects:
docs/examples/blog/— a blog (~200 lines): articles, authors, tags, comments, live subscriptions, row-level policies, typed Go client.docs/examples/ecommerce/— an e-commerce store (~300 lines): cross-paradigm ACID checkout, link types with properties, tagged unions, a live order-ops table that delta-updates in place.
A complete .wo program that creates a database table, exposes six REST endpoints with live subscriptions, and emits a typed Go client:
-- article.wo
type Article {
id: Id
title: Text
body: Markdown
author: Text
created_at: Timestamp = now()
service rest "/api/articles"
expose list, get, create, update, delete, subscribe
}
Run it:
$ wo run
[wo] compiling ./article.wo
[wo] schema: 1 type, 0 migrations needed
[wo] listening on :8080
GET /api/articles list
GET /api/articles/:id get
POST /api/articles create
PATCH /api/articles/:id update
DELETE /api/articles/:id delete
WS /api/articles/live subscribe
Use it:
$ curl -X POST localhost:8080/api/articles \
-H "Content-Type: application/json" \
-d '{"title":"Hello","body":"# First post","author":"me"}'
{"id":1,"title":"Hello","body":"# First post","author":"me","created_at":"2026-04-17T..."}
$ curl localhost:8080/api/articles
[{"id":1,"title":"Hello","...":"..."}]
Generate a typed client:
$ wo gen sdk --lang go --out ./client
# produces ./client/sdk.go with typed Article struct and Subscribe helper
Subscribe from the client — deltas push on every commit, no polling:
import "myapp.example.com/client"
c, _ := client.Connect("wo://localhost:8080")
sub, _ := c.Articles.Subscribe(ctx, client.Where{Author: "me"})
for delta := range sub.C {
fmt.Printf("%s: %+v\n", delta.Kind, delta.Row)
}
Three files, five commands, zero infrastructure. Compare the same thing in Go+Postgres+React: one SQL schema, one migration tool, one ORM, one HTTP router, one subscription layer (polling or Redis pub-sub), one hand-written client, one React hook — roughly 2000 lines before you write any business logic.
The toolchain
Go-literal. Every command maps to a Go equivalent so the mental model transfers:
| Command | Go equivalent | Purpose |
|---|---|---|
wo init <name> |
go mod init |
scaffold a new project |
wo run |
go run ./... |
compile and execute |
wo build |
go build |
emit a static binary |
wo test |
go test |
run .wo tests |
wo fmt |
gofmt |
canonical formatter |
wo vet |
go vet |
lint + type-check without running |
wo mod <cmd> |
go mod |
dependencies |
wo doc <sym> |
go doc |
render docs for a type |
wo gen sdk --lang <L> |
go generate (codegen) |
emit a client SDK |
wo migrate [--plan|--apply] |
no direct equivalent | schema evolution |
wo dev |
no direct equivalent | hot-reload dev server |
wo run vs wo build. Same as Go: wo run compiles to a temp binary and executes it; wo build writes a named binary. No interpreter mode — .wo is compiled, always.
wo dev is the one non-Go addition. Edit a .wo file, the runtime hot-swaps the affected module without restarting. Live subscriptions survive the reload. This is the Phoenix LiveView influence.
Program structure
myapp/
├── wo.toml # like go.mod — name, version, dependencies
├── main.wo # optional entry point
├── types/ # `type` declarations (one file per domain concept)
│ ├── article.wo
│ └── user.wo
├── ui/ # ##ui screens (optional)
├── tests/ # *_test.wo files
└── wo.lock # locked dependency graph (like go.sum)
Minimum project is one .wo file with one type declaration. The compiler generates:
- the database schema (relational row, document structures, graph edges) from the type's fields
- HTTP handlers from type-attached
serviceblocks - transactional triggers from
on <event>blocks - row-level policies from
policyblocks - typed client SDKs from the same type, on demand
No main() is required for a pure type-and-service app. The runtime starts the HTTP server, loads the database, and dispatches. If you need procedural entry logic (CLI args, graceful shutdown hooks, cron jobs), add main.wo with a main { ... } block.
The runtime — what's in the standard library
Every wo build links these in. They're not external packages you import — they're the language.
| Component | Responsibility | Mapped to phase |
|---|---|---|
| Database | In-RAM ACID multi-paradigm (relational + doc + graph) with WAL durability | Phase 2 + Phase 3 |
| Transaction coordinator | MVCC, snapshot isolation, cross-paradigm RETURNING alias table |
Phase 2 |
| HTTP server | REST + GraphQL dispatch generated from service blocks |
Phase 4 + crates/http |
| Subscription engine | LIVE queries push deltas on commit, zero polling |
Phase 4 |
| Wire protocol | Native binary codec for typed clients | Phase 4 |
| Codegen | wo gen sdk — Go, TypeScript, Rust, Python clients from type declarations |
Phase 5 |
| UI renderer | ##ui screens → SSR HTML + client runtime |
Phase 6 |
| Authorization | policy blocks compiled into planner rewrite rules |
Phase 6 |
| Scheduler | Single-threaded event loop over io_uring; one core per process (shard to scale) | async.md + Phase 2 concurrency |
Comparison to Go's stdlib:
| Need | Go | writeonce |
|---|---|---|
| HTTP server | net/http |
built-in service rest |
| Database | none (use database/sql + driver + Postgres) |
built-in |
| Template rendering | html/template |
##ui blocks |
| Concurrency | goroutines + channels | single-threaded event loop (Redis-style); shard to scale past one core |
| Testing | testing |
wo test + .wo test syntax |
| Formatting | gofmt |
wo fmt |
| Modules | go.mod + go.sum |
wo.toml + wo.lock |
Clients — who consumes your program
The same .wo type declarations that define the database also define the wire format. wo gen sdk emits:
- Go — typed structs,
*Client,TypedSubscription[T]generics over a channel - TypeScript / browser — types +
fetch+ WebSocket subscriptions - Rust — structs,
tokioasync client,impl Stream<Item = Delta> - Python — dataclasses,
async for delta in sub - curl / raw REST — documented via auto-generated OpenAPI spec at
/openapi.json - GraphQL clients — SDL auto-generated at
/graphql/schema.graphql
Raw .wo DML is a first-class escape hatch. Every client SDK exposes a single method — client.Wo(ctx, src, params) in Go, equivalents in TypeScript/Rust/Python — that accepts any .wo source the server would accept: mixed SQL + Cypher, BEGIN … COMMIT blocks with RETURNING aliases threading across statements, ad-hoc MATCH-then-SELECT queries that cross multiple generated types. The typed methods are sugar; the engine speaks .wo on the wire. A Go program can send a cross-paradigm transaction as a single string and the server parses + executes it exactly like wo run would — see Phase 5: Go Client SDK for the full API.
One schema, every protocol. A browser app, a mobile client, and a background worker can all subscribe to the same live query and receive the same delta stream.
What this is, and isn't
Is. A declarative, full-stack, single-binary language for building CRUD apps with live data. A replacement for the "Go backend + Postgres + Redis + React + Prisma + GraphQL server" stack.
Isn't.
- Not a general-purpose language like Rust or Go. You can't write a kernel module or a video codec in
.wo. The scope is data-shaped applications. - Not a JavaScript meta-framework. No Node, no React. The UI layer (
##ui) is declarative and compiles to SSR HTML with a small vanilla-JS client. - Not a DSL that transpiles to another language.
.wohas its own lexer, parser, analyzer, and bytecode. Theprototypes/db/C++ prototype and the planned Rust crates implement the runtime natively. - Not a hosted service. Your binary owns its own DB file. No managed cloud offering is required.
How this maps to the design series
This overview is the user-facing frame. The underlying engineering plan is the 7-phase series linked from database.md:
- Phase 2 designs the language and the transaction coordinator.
- Phase 3 builds the storage engine.
- Phase 4 builds the wire protocol and subscription engine.
- Phase 5 builds the first typed client (Go) and
wo gen. - Phase 6 adds
##uiand the application-level blocks. - Phase 7 migrates writeonce-the-blog from
wo-segonto this runtime.
Phase 1 (evaluation) and the case studies in surreal-case-study.md argue why the language exists at all. Read those first if you're skeptical; read the phase docs if you're implementing; read this page if you want to know what it feels like to use.
Reference points
The design absorbs lessons from several systems. In order of influence:
- Go — toolchain shape, single-binary deployment, "the language is the build system"
- Phoenix LiveView — subscription-native UI, hot-reloading dev server
- SAP CDS — declarative entity/service language, admin UI generation
- SurrealDB — multi-paradigm query language,
LIVEsubscriptions over wire - PocketBase — single-binary CRUD backend (the proof of concept that this is shippable)
- EdgeDB — unified type system above storage paradigms
- Elixir / Erlang / OTP — hot code loading, supervision, subscription semantics
- Django — admin UI as a built-in, not a bolt-on
None of these give you all of: a language, a database, a subscription engine, a UI toolkit, a client codegen, and a single-binary output. writeonce is the attempt to fuse the best of each into one thing.
Minimal "hello, world" as a full program
If you want a pure procedural test, without the server:
-- hello.wo
main {
print("hello, world")
}
$ wo run hello.wo
hello, world
If you want the database without HTTP:
type Counter {
name: Text @unique
value: Int = 0
}
main {
insert Counter { name: "visits" };
update Counter{ name == "visits" }.value += 1;
let c = select Counter{ name == "visits" };
print(c.value);
}
If you want the full app — database, HTTP, subscriptions, clients — it's the article example at the top of this page.
Three progressive shapes, one language, one command to run each.