# 🤖 todo-nim

TodoMVC in Nim for the CARLOS bakeoff: server-rendered, works with
JavaScript off, and keeps each visitor's list in SQLite. It runs at
[https://todo-nim.bfj.oncarlos.com](https://todo-nim.bfj.oncarlos.com).

## The stack, and why

- **Nim 2.2, compiled to C.** Nim builds a small native binary, and its C
  backend makes the static linux-arm64 build easy: `zig cc` targeting
  musl acts as the C compiler and linker, and the result is a 1.7 MB
  static executable that starts in milliseconds.
- **HTTP: the standard library's `asynchttpserver`**, on `asyncdispatch`.
  A todo list doesn't need more than a single-threaded event loop, and the
  standard library means no framework to pin. The popular alternatives
  don't fit this platform: Mummy and httpbeast (Jester's server) only
  listen on TCP, and Carloku hands the app a unix socket.
  `asynchttpserver` doesn't take a unix socket either, but it's a small
  gap: `unixServer` in `src/app.nim` binds the socket itself and gives it
  to the server through `std/importutils.privateAccess`. The server is a
  copy of the standard library's (`src/httpserver.nim`) with one fix:
  upstream doesn't limit chunked request bodies, so one POST could
  exhaust the instance's memory. The copy holds chunked bodies to the same
  1 MiB limit as `Content-Length` ones. The standard library's docs call
  this server basic; it has no request timeouts or keep-alive limits.
  Carloku's edge sits in front of it, and that's an acceptable trade for
  an app this size.
- **Routing: a `case` on the path.** Six actions and four pages don't need
  a router.
- **Templates: Nim's built-in `stdtmpl` source filter** (`src/page.nimf`).
  It compiles the template into an ordinary Nim proc, so the markup is
  type-checked and there's nothing to load at run time. Titles go through
  `xmltree.escape`.
- **SQLite: `db_connector`'s `sqlite3` binding** (the database module that
  moved out of Nim's standard library) over the **SQLite 3.54.1
  amalgamation, compiled into the binary.** The binding normally loads
  `libsqlite3.so`; `--dynlibOverride:sqlite3` turns that off so it links
  against the compiled-in copy. `src/store.nim` prepares every statement
  once and binds parameters, so no user input is ever spliced into SQL.
  WAL mode, `synchronous = NORMAL`, and numbered migrations that only add
  (`PRAGMA user_version` records how far the file has got).
- **JavaScript: one plain ES module, no framework** (`static/app.js`). It
  sends the same form POSTs with an `X-Fragment: 1` header, and the server
  answers with the re-rendered list section instead of a 303. Rendering
  stays in one place, the server.
- **The CSS** is `shared/base.css` and `shared/index.css`, embedded at
  compile time with `staticRead` and served byte for byte.
  `static/app.css` adds the no-JavaScript controls and hides them once
  JavaScript is running.

Lists are anonymous: the first visit gets a cookie holding 128 random bits
from the OS (`std/sysrand`), and every query is scoped to it. See
`AGENTS.md` for the tenancy declaration.

## Build, run, test, deploy

Put the toolchains on `PATH` first: `source scripts/env.sh` from the repo
root. The first build fetches `db_connector` into `.cache/nimble` and the
SQLite amalgamation into `.cache/dl`, checking it against a pinned SHA-256.

```
make build          # dist/app for this machine
make build-arm64    # dist/app-linux-arm64, static (zig cc, musl)
make test           # unit tests: the store, and the routes with fake requests
make clean          # removes dist/
make clean-deps     # also removes compiled C: .cache/nimcache-* and zig's caches
```

Run it locally and drive the shared behaviour suite against it:

```
dist/app --listen 127.0.0.1:8080 --db $TMPDIR/nim.db
scripts/test-app.sh http://127.0.0.1:8080 $TMPDIR/nim-suite.json
```

On Carloku it runs as `app --socket <path> --db <path>`, reads
`CARLOS_HOST` for its origin, and sets `Secure` on its cookie. It exits
cleanly on SIGTERM.

`/api/version` reports the git short SHA the binary was built from, so
commit before building what you deploy. Then:

```
scripts/boot-check.sh dist/app-linux-arm64
make deploy MESSAGE="what changed"
```

## Layout

| Path | What |
|---|---|
| `src/app.nim` | Flags, the server, routes and cookies |
| `src/httpserver.nim` | The standard library's HTTP server, with chunked bodies limited |
| `src/store.nim` | SQLite: schema, migrations, every query |
| `src/views.nim`, `src/page.nimf` | The page and the list fragment |
| `static/app.js`, `static/app.css` | The JavaScript layer and the no-JS controls |
| `tests/test_app.nim` | Unit tests |
