proto-sudoku

System Architecture

Classic sudoku on Cloudflare: static files serve the page, and a small Rust program compiled to WebAssembly generates every puzzle. One Worker, no database, no server-side state, no accounts.

worker — wrangler.jsonc

name
→ rust-sudoku-api
main
→ build/index.js + index_bg.wasm
assets
→ ./public (served first)
routes
→ sudoku.eswat2.app (custom domain)
dev port
→ :8787 (wrangler dev)
L1 · CLIENT

Web Component — proto-sudoku-wc on npm

<proto-sudoku>

the whole game

A Stencil and Tailwind component in its own Shadow DOM: the board, the digit buttons, New Puzzle, Check ? and the status line. platform="cloudflare" points it at the relative path /api.

store

state in memory

Holds the 81 cells, the picked cell and its peers (row, column, box), and the legal digits for it. Decodes ref once, so Check compares every entry locally — no second request.

localStorage

survives a reload

The puzzle, the picked cell, and the entries that passed a check. Unchecked entries stay in memory only, so a reload drops them.

The page loads the component from jsDelivr at an exact version (proto-sudoku-wc@0.1.340). Stencil lazy-loads hashed chunks through the same URL, so @latest could mix files from two releases; .wc-loader bumps the pin instead.

GET /api/puzzle
L2 · EDGE

Cloudflare Workers — wrangler.jsonc

Static assets

public/ · no Worker invocation

Any path that matches a file: the game page, /about, this page, /llms.txt, images and favicons. Most requests never run the Rust code.

Worker

build/index.js · ~300 KB wasm

Everything without a matching file. A small JS shim from worker-build loads the wasm module and hands it each request.

Local

wrangler dev · :8787

Runs worker-build --release, then the same Worker in a local workerd. No other services to start.

routes
L3 · API

Router — src/lib.rs

Game

One route does the work

GET /api/puzzleOPTIONS *

Leftover demo — returns {"hello": name}

GET /:nameGET /api/:name

Static

Pages

//about/architecture

Discovery — for LLMs & agents, not the app itself

/llms.txt

Every Worker response — 404s and OPTIONS preflights included — gets permissive CORS headers in one place after routing (Access-Control-Allow-Origin: *, methods GET,OPTIONS), so any page can call the API. There is no key and no rate limit.

calls
L4 · CORE

Rust → WebAssembly — workers-rs 0.8 & the sudoku crate

Handlers

  • fetch() — the router, then CORS
  • puzzle() — builds the JSON
  • greet() — the demo route
  • with_cors()

Puzzle — plain Rust, no Worker types

  • new_puzzle()
  • Sudoku::generate() — unique, minimal, symmetric
  • solution() — solved straight away
  • base64 — encodes the answer

Build

  • worker-build --release
  • opt-level = "s" — plus LTO
  • getrandom — js feature, crypto.getRandomValues

Bench — benches/puzzle.rs

  • cargo bench — Criterion, native
  • generate ~80 µs · solve ~2 µs
  • wasm runs ~1.5–3× slower
  • free plan allows 10 ms CPU

Because new_puzzle() has no Worker types, the same function is benchmarked natively. The response carries no timings: Workers freezes the clock while code runs, so a timer would always read 0.

JSON response
L5 · DATA

No server storage — the puzzle lives in the browser

/api/puzzle — response

puzzle81 chars · dots blank
refbase64 solution
metricsblanks + clues
tag"cloudflare"
→ saved by
the component

localStorage — proto-sudoku::

datathe response
inputschecked entries
pickselected cell

The solution ships with the puzzle on purpose: it's a casual game, and checking in the browser beats a round trip. Base64 hides the answer from a glance; it doesn't protect it.

deploy
L6 · SHIP

Deploys & hosting — scripts/deploy.sh

Local deploys

10–30 s · not Workers Builds

Workers Builds had no Rust cache and took 6–7 minutes a push. The script builds locally, ships only a clean main that matches the remote, and tags the deploy with the commit.

Daily updates

scripts/update.sh · never deploys

cargo update, and only if Cargo.lock changed: check the wasm and native builds, then commit and push. Major-version bumps are reported, not applied. Deploys stay a separate, manual step.

Two hostnames

custom domain + workers.dev

sudoku.eswat2.app is a Workers Custom Domain, so Cloudflare manages its DNS and certificate. rust-sudoku-api.eswat2.workers.dev stays on too.

Wasm, not Containers

free plan · no cold start

Containers could have run the old Actix binary unchanged, but need the paid plan. Before the port it lived on Heroku until the free tier ended in 2022. A serverless copy still runs on Vercel.