An Odin + WebAssembly UI example with a minimal virtual DOM, a small static server powered by odin-http, and JWT-backed endpoints. The UI runs in the browser (WASM), while the native server serves static files and a tiny JSON API.
- Chat UI rendered from Odin to the DOM via JS bridges
- Fixed-height, scrollable message list with auto-scroll and focus preservation
- Send messages from the UI; server stores them in memory (max 200)
- JWT (HS256) demo endpoints with constant-time signature verification
- “Who Am I” button to validate the JWT from the browser
- Odin toolchain (tested with
odin version dev-2025-08) - just (optional but recommended):
just --version
- Build and run everything:
just serve- Open
http://localhost:8080/
The serve recipe will:
- Fetch
vendor/odin-httpif missing (just vendor) - Build the WASM UI (
just build→ui.wasm) - Build the native server (
just build-server→./server_bin) - Launch the server on port 8080
- Build WASM only:
just build - Build server only:
just build-server - Fetch/update vendor:
just vendor(clone odin-http)just vendor-update(pull latest)
Without just:
- WASM:
odin build . -target:js_wasm32 -out:./ui.wasm - Server:
odin build ./server -collection:local=./vendor -out:./server_bin && ./server_bin
- One-off:
LOG_LEVEL=debug just serve - Or use the recipe:
just serve-debug - Levels:
debug,info(default),warn,errorviaLOG_LEVEL.
Enable “Sign in with Bluesky” by setting these environment variables (e.g., in .env):
BSKY_OAUTH_REDIRECT_URI: Usehttp://127.0.0.1:8080/(or your host) and ensure it’s registered.BSKY_OAUTH_AUTHORIZATION_ENDPOINT: e.g.https://bsky.social/oauth/authorize.BSKY_OAUTH_TOKEN_ENDPOINT: e.g.https://bsky.social/oauth/token.BSKY_OAUTH_SCOPE(optional): Defaults toprofile offline_access.
Click “Sign in with Bluesky” in the UI to run a PKCE flow. On success, the server exchanges the code and issues a local JWT; the UI stores it and updates the current user.
GET /api/messages→[{ user, text, at }]POST /api/messages→ body{ user, text }GET /api/auth/token?sub=NAME→{ token }(HS256 JWT)GET /api/auth/whoami(requiresAuthorization: Bearer <token>) →{ payload }GET /api/health→{ ok: true }GET /api/time→{ now: "YYYY-MM-DD HH:MM:SS" }
index.html— Boots the WASM, provides DOM/JS bridge and loading spinnerodin.js— Odin’s JS runtime/loader required byindex.html(tracked in repo)main.odin— UI state + components; fetch handlers, JWT wiringvdom.odin— Minimal virtual DOM node definitionsrenderer.odin— DOM builder + JS foreign imports (events, fetch, focus)events.odin— Event dispatch from JS → Odin and rerenderserver/main.odin— Static server, JSON API, rate limiting, loggingserver/jwt.odin— HS256 sign/verify and base64url helpersjustfile— Build/serve tasks; vendor management
- Messages are stored in Turso (libSQL over HTTP). Set environment variables before starting the server:
TURSO_DATABASE_URL(orLIBSQL_URL)TURSO_AUTH_TOKEN(orLIBSQL_AUTH_TOKEN) The server auto-creates themessagestable if needed. Without these env vars, storage is disabled and message endpoints return 500.
- JWT secret defaults to
dev-secret-change-me(seeserver/main.odin). Swap for production. - The app shows a spinner until the first render; on failure, it replaces the spinner with a friendly error.
- If the server can’t find
odin-http, runjust vendor. - If the browser fetches fail, check DevTools Network and the terminal logs. The UI logs status and up to 200 chars of body on parse failures.
- If you see a 404 for
odin.js, ensure it’s present in the project root (it’s tracked in this example); the page loads it before startingui.wasm.
This is an educational example; harden and review before production use.