# `blog` — a sample writeonce app A complete blogging website in **~200 lines of `.wo`** that creates a database, exposes REST + live-subscription endpoints, renders HTML pages, enforces row-level policies, and emits typed client SDKs. > This project is a **docs artifact** — it illustrates the shape of a real `wo init`'d project. The `wo` toolchain referenced here is the one specified in [`../../runtime/wo-language.md`](../../runtime/wo-language.md); the engine is at prototype stage in [`../../../prototypes/wo-db/`](../../../prototypes/wo-db/). ## What it does | Thing | How | | --- | --- | | Persists articles, authors, tags, comments | `type` declarations compiled to relational rows + embedded documents + graph edges | | Serves 24 REST endpoints (CRUD + subscribe × 4 types) | `service rest` blocks on each type | | Serves 4 web pages (list, detail, tag, admin) | `##ui` screens + route table in `app.wo` | | Pushes live updates on every commit | `live: true` on screens + `LIVE` queries under the hood | | Enforces "drafts hidden from anonymous readers" | `policy read anyone when published == true` | | Bumps `published_at` automatically | `on update` trigger inside the transaction | | Generates a typed Go client | `wo gen sdk --lang go` | ## Project layout ``` blog/ ├── wo.toml # project manifest (like go.mod) ├── app.wo # routes, theme, startup hooks ├── types/ │ ├── author.wo # Author type + per-type service/policy │ ├── article.wo # Article — all three paradigms in one type │ ├── tag.wo # Tag taxonomy │ └── comment.wo # Reader comments ├── ui/ │ ├── article_list.wo # home page list view (live) │ └── article_detail.wo # per-article page with comments + related └── tests/ └── article_test.wo # `wo test` picks this up ``` No `main.wo` is needed — a pure type+service app auto-generates its entry point. Add `main.wo` if you need CLI args, background workers, or custom startup logic beyond the `on startup` hook in `app.wo`. ## Run it ```bash $ cd docs/examples/blog $ wo run [wo] parsing: 7 files, 4 types, 2 ui screens [wo] compiling schema: 4 sql tables, 1 doc collection, 3 graph edge types [wo] starting runtime (engine: in-memory, data_dir: ./data) [wo] on startup: seed_admin() — inserted admin@example.com [wo] HTTP 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 GET /api/authors list GET /api/authors/me me WS /api/authors/live subscribe GET /api/tags list GET /api/comments list POST /api/comments create WS /api/comments/live subscribe ... (and the rest) GET / ui.article-list GET /article/:slug ui.article-detail GET /tag/:slug ui.article-list (filtered) GET /admin ui.article-list (role: Admin) ``` ## Exercise the REST API ```bash # Create an author (requires admin session — see auth docs; stub'd here for brevity) $ curl -X POST localhost:8080/api/authors \ -H "Content-Type: application/json" \ -H "Authorization: Bearer $ADMIN_TOKEN" \ -d '{"email":"alice@example.com","handle":"alice","display":"Alice","role":"Author"}' {"id":2,"email":"alice@example.com","handle":"alice",...} # Create an article as that author $ curl -X POST localhost:8080/api/articles \ -H "Authorization: Bearer $ALICE_TOKEN" \ -d '{ "slug": "hello", "title": "Hello, writeonce", "author": 2, "meta": {"excerpt":"First post","body_md":"# Hi\n\nHello."}, "published": true }' {"id":1,"slug":"hello","title":"Hello, writeonce","published_at":"2026-04-17T12:00:00Z",...} # List published articles (public — no token) $ curl localhost:8080/api/articles [{"id":1,"slug":"hello","title":"Hello, writeonce",...}] # Filter by tag (via the query layer) $ curl 'localhost:8080/api/articles?tags.slug=rust' [...] ``` ## Subscribe to live updates ```bash $ websocat ws://localhost:8080/api/articles/live?published=eq.true {"kind":"snapshot","rows":[{"id":1,"slug":"hello",...}]} # Now in another terminal, update article 1. The open socket receives: {"kind":"update","id":1,"old":{"title":"Hello, writeonce"},"new":{"title":"Hello!"}} ``` No polling. The subscription predicate was registered at connect time; the engine's commit path emits the delta directly. ## Generate a Go client ```bash $ wo gen sdk --lang go --out ./client [wo] reading types from ./types/ [wo] writing ./client/sdk.go (4 types, 16 endpoints, 4 subscriptions) ``` Use it: ```go import "github.com/you/blog/client" c, _ := client.Connect(ctx, "wo://localhost:8080", client.WithToken(token)) // Typed query articles, _ := c.Articles.List(ctx, client.Where{Published: ptr(true)}) // Typed subscription — deltas arrive on a channel sub, _ := c.Articles.Subscribe(ctx, client.Where{Published: ptr(true)}) for d := range sub.C { switch d.Kind { case client.Insert: fmt.Printf("new article: %s\n", d.Row.Title) case client.Update: fmt.Printf("updated: %s\n", d.Row.Slug) } } ``` ## Run the tests ```bash $ wo test === tests/article_test.wo === create and fetch by slug OK (3ms) policy blocks public read of unpublished drafts OK (4ms) graph traversal: related articles OK (7ms) live subscription receives delta on commit OK (12ms) PASS 4/4 tests, 0 failures (26ms) ``` Each `test` block runs against an isolated engine snapshot that's rolled back at the end — no setup/teardown code needed. ## Build a production binary ```bash $ wo build --target linux-amd64 --out bin/blog [wo] static binary: bin/blog (14 MB, database + HTTP + subscription engine embedded) $ ./bin/blog [wo] HTTP listening on :8080 ``` One binary, no dependencies. Copy it to a server, run it, done. The database file lives in `./data/` relative to the binary; the WAL ensures crash safety ([Phase 3](../../runtime/database/03-inmemory-engine.md)). ## What to read next - [`../../runtime/wo-language.md`](../../runtime/wo-language.md) — the user-facing language overview this project builds on - [`../../runtime/database/02-wo-language.md`](../../runtime/database/02-wo-language.md) — the two-layer language spec (schema + query layers) - [`../../runtime/database/06-lowcode-fullstack.md`](../../runtime/database/06-lowcode-fullstack.md) — the `##ui`/`##policy`/`##service`/`##app` block spec - [`../../../prototypes/wo-db/`](../../../prototypes/wo-db/) — the C++ prototype that runs the query-layer subset today