writeonce/docs/runtime/wo-language.md

12 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 .wo files 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:

  1. The language — .wo source files. Declarative by default (type, 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.
  2. 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.
  3. The toolchain — the wo command: 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 service blocks
  • transactional triggers from on <event> blocks
  • row-level policies from policy blocks
  • 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, tokio async 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. .wo has its own lexer, parser, analyzer, and bytecode. The prototypes/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 ##ui and the application-level blocks.
  • Phase 7 migrates writeonce-the-blog from wo-seg onto 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, LIVE subscriptions 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.