# 🤖 todo-swift

TodoMVC for the CARLOS bakeoff, written in Swift. It's live at
[https://todo-swift.bfj.oncarlos.com](https://todo-swift.bfj.oncarlos.com).

The server renders every page, and every action is an ordinary HTML form,
so the app works with JavaScript off. With JavaScript on, a small ES module
sends the same forms with `fetch` and swaps in the new list, so it behaves
like the TodoMVC reference apps. `AGENTS.md` has the rule the app lives by
and its tenancy declaration; `CONTRACT.md` at the repo root has the routes
and markup it shares with the other entries.

## The stack, and why

| Piece | Choice | Why |
|---|---|---|
| Language | Swift 6.2, strict concurrency | The setup under test. The Static Linux SDK links musl and the Swift runtime into one static binary |
| HTTP server | [Hummingbird 2](https://github.com/hummingbird-project/hummingbird) | The lightweight server framework on SwiftNIO, async/await throughout. It binds a unix socket directly (`BindAddress.unixDomainSocket`), and its `runService()` handles SIGTERM with a graceful shutdown, which is exactly the platform's process contract. Vapor would also work, but it brings far more than a nine-route app needs |
| SQLite | [SQLiteNIO](https://github.com/vapor/sqlite-nio) | It compiles SQLite (3.54) from source as part of the package, so nothing depends on a system `libsqlite3`, which the Static Linux SDK doesn't have. GRDB is the nicer API, but it expects a system SQLite on Linux |
| Header types | [swift-http-types](https://github.com/apple/swift-http-types) | Already under Hummingbird; listed because the app names `HTTPField` itself |
| HTML | String building in `Page.swift`, one `escape` function | Three small templates don't need a template engine. Every visitor-supplied string goes through `escape` |
| CSS and JS | Embedded in the binary by `tools/embed.sh` | One file to ship. A test checks the embedded copies match `shared/` and `static/` byte for byte |
| Tests | Swift Testing, with `HummingbirdTesting` driving the router | No server or port needed for the unit tests |
| Arguments | Parsed by hand | Three flags didn't justify swift-argument-parser |

Two size decisions: Hummingbird's default `FullFoundation` trait is
switched off and the app only imports `FoundationEssentials`, so the ICU
data that full Foundation carries stays out. That took the arm64 binary
from 66 MB to 22 MB. The shipped binary is also stripped.

### How it fits together

- `Sources/todo/main.swift` reads the flags and starts the server.
- `Server.swift` parses `--socket`/`--listen`/`--db`, works out the app's
  own origin (`https://$CARLOS_HOST` on a socket, `http://127.0.0.1:<port>`
  with `--listen`) and runs Hummingbird until SIGTERM.
- `Routes.swift` holds every route, including the `GET /` handler.
- `Visitors.swift` gives each visitor a list through a `todo_list` cookie
  (128 random bits, `HttpOnly; SameSite=Lax; Path=/`, plus `Secure` on a
  socket, where the edge serves https). It also holds the `Origin` check
  that answers 403 to cross-site posts.
- `Store.swift` is the SQLite layer: WAL mode, append-only migrations
  tracked by `PRAGMA user_version`, and one SQL statement per change. An
  action on an item that isn't in the caller's list changes no row and
  answers 404.
- `Page.swift` renders the page from `shared/reference.html`'s markup.
- `static/app.js` is the JavaScript; `static/app.css` places the
  no-JavaScript controls over the checkboxes they stand in for and hides
  them when JavaScript runs.

With JavaScript, an action sends the header `X-Todo-Partial: 1` and gets
back the new `section.main` and `footer.footer` (200) instead of a 303.
Without that header nothing changes.

## Build, run, test, deploy

Put the toolchain on `PATH` first: `source ../../scripts/env.sh`. The
Makefile does that itself.

| Command | Does |
|---|---|
| `make build` | Builds for this machine into `dist/app` (dynamically linked against the local Swift runtime) |
| `make build-arm64` | Builds the shipped static binary, `dist/app-linux-arm64` |
| `make test` | Runs the unit tests |
| `make run` | Runs `dist/app` on `127.0.0.1:8080` (`PORT=` to change) |
| `make clean` | Removes `dist/` |
| `make clean-deps` | Also removes every compiled artifact under `.cache/build`, keeping the downloaded sources |
| `make bump` / `make unbump` | Changes a string constant in `Routes.swift`, forcing that file to recompile |
| `make deploy` | Deploys `dist/app-linux-arm64` to Carloku |

SwiftPM's checkouts, repository cache and build output all live in
`.cache/` (`--scratch-path` and `--cache-path`), so nothing is shared with
other projects.

The build version is the commit's short SHA. `make` writes it into
`Sources/TodoApp/Version.swift` (git-ignored), with `-dirty` added when
`apps/swift` has uncommitted changes. `make deploy` refuses a dirty build,
or a binary built at another commit.

The shared behaviour suite:

```
make build
dist/app --listen 127.0.0.1:8080 --db $TMPDIR/swift.db &
../../scripts/test-app.sh http://127.0.0.1:8080 $TMPDIR/swift-suite.json
```

Before a deploy, `../../scripts/boot-check.sh dist/app-linux-arm64` runs
the arm64 binary under qemu on a unix socket and checks `/healthz`,
`/api/version` and a clean exit on SIGTERM. Then:

```
git commit ...
make build-arm64 deploy MSG="what changed"
```

The app was created on Carloku once with:

```
carlos apps create --account bfj --app todo-swift --build-probe /api/version
carlos instances enable --account bfj --app todo-swift
carlos instances create --account bfj --app todo-swift --host todo-swift.bfj.oncarlos.com --channel edge
```

## Trade-offs

- Trust class C: titles are stored as plain text, and anyone holding the
  database or a replica can read every list. See `AGENTS.md`.
- A list belongs to a cookie. Clear it and the list is gone for good.
  Abandoned lists are never erased.
- Every visit without a valid cookie creates a list row, crawlers
  included. The rows are tiny, and there's no cleanup yet.
- The one SQLite connection serialises all queries. That's plenty for a
  todo list. A busier app would add a read pool.
