| 1 | # đŸ¤– todo-swift |
| 2 | |
| 3 | TodoMVC for the CARLOS bakeoff, written in Swift. It's live at |
| 4 | [https://todo-swift.bfj.oncarlos.com](https://todo-swift.bfj.oncarlos.com). |
| 5 | |
| 6 | The server renders every page, and every action is an ordinary HTML form, |
| 7 | so the app works with JavaScript off. With JavaScript on, a small ES module |
| 8 | sends the same forms with `fetch` and swaps in the new list, so it behaves |
| 9 | like the TodoMVC reference apps. `AGENTS.md` has the rule the app lives by |
| 10 | and its tenancy declaration; `CONTRACT.md` at the repo root has the routes |
| 11 | and markup it shares with the other entries. |
| 12 | |
| 13 | ## The stack, and why |
| 14 | |
| 15 | | Piece | Choice | Why | |
| 16 | |---|---|---| |
| 17 | | Language | Swift 6.2, strict concurrency | The setup under test. The Static Linux SDK links musl and the Swift runtime into one static binary | |
| 18 | | 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 | |
| 19 | | 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 | |
| 20 | | Header types | [swift-http-types](https://github.com/apple/swift-http-types) | Already under Hummingbird; listed because the app names `HTTPField` itself | |
| 21 | | 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` | |
| 22 | | 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 | |
| 23 | | Tests | Swift Testing, with `HummingbirdTesting` driving the router | No server or port needed for the unit tests | |
| 24 | | Arguments | Parsed by hand | Three flags didn't justify swift-argument-parser | |
| 25 | |
| 26 | Two size decisions: Hummingbird's default `FullFoundation` trait is |
| 27 | switched off and the app only imports `FoundationEssentials`, so the ICU |
| 28 | data that full Foundation carries stays out. That took the arm64 binary |
| 29 | from 66 MB to 22 MB. The shipped binary is also stripped. |
| 30 | |
| 31 | ### How it fits together |
| 32 | |
| 33 | - `Sources/todo/main.swift` reads the flags and starts the server. |
| 34 | - `Server.swift` parses `--socket`/`--listen`/`--db`, works out the app's |
| 35 | own origin (`https://$CARLOS_HOST` on a socket, `http://127.0.0.1:<port>` |
| 36 | with `--listen`) and runs Hummingbird until SIGTERM. |
| 37 | - `Routes.swift` holds every route, including the `GET /` handler. |
| 38 | - `Visitors.swift` gives each visitor a list through a `todo_list` cookie |
| 39 | (128 random bits, `HttpOnly; SameSite=Lax; Path=/`, plus `Secure` on a |
| 40 | socket, where the edge serves https). It also holds the `Origin` check |
| 41 | that answers 403 to cross-site posts. |
| 42 | - `Store.swift` is the SQLite layer: WAL mode, append-only migrations |
| 43 | tracked by `PRAGMA user_version`, and one SQL statement per change. An |
| 44 | action on an item that isn't in the caller's list changes no row and |
| 45 | answers 404. |
| 46 | - `Page.swift` renders the page from `shared/reference.html`'s markup. |
| 47 | - `static/app.js` is the JavaScript; `static/app.css` places the |
| 48 | no-JavaScript controls over the checkboxes they stand in for and hides |
| 49 | them when JavaScript runs. |
| 50 | |
| 51 | With JavaScript, an action sends the header `X-Todo-Partial: 1` and gets |
| 52 | back the new `section.main` and `footer.footer` (200) instead of a 303. |
| 53 | Without that header nothing changes. |
| 54 | |
| 55 | ## Build, run, test, deploy |
| 56 | |
| 57 | Put the toolchain on `PATH` first: `source ../../scripts/env.sh`. The |
| 58 | Makefile does that itself. |
| 59 | |
| 60 | | Command | Does | |
| 61 | |---|---| |
| 62 | | `make build` | Builds for this machine into `dist/app` (dynamically linked against the local Swift runtime) | |
| 63 | | `make build-arm64` | Builds the shipped static binary, `dist/app-linux-arm64` | |
| 64 | | `make test` | Runs the unit tests | |
| 65 | | `make run` | Runs `dist/app` on `127.0.0.1:8080` (`PORT=` to change) | |
| 66 | | `make clean` | Removes `dist/` | |
| 67 | | `make clean-deps` | Also removes every compiled artifact under `.cache/build`, keeping the downloaded sources | |
| 68 | | `make bump` / `make unbump` | Changes a string constant in `Routes.swift`, forcing that file to recompile | |
| 69 | | `make deploy` | Deploys `dist/app-linux-arm64` to Carloku | |
| 70 | |
| 71 | SwiftPM's checkouts, repository cache and build output all live in |
| 72 | `.cache/` (`--scratch-path` and `--cache-path`), so nothing is shared with |
| 73 | other projects. |
| 74 | |
| 75 | The build version is the commit's short SHA. `make` writes it into |
| 76 | `Sources/TodoApp/Version.swift` (git-ignored), with `-dirty` added when |
| 77 | `apps/swift` has uncommitted changes. `make deploy` refuses a dirty build, |
| 78 | or a binary built at another commit. |
| 79 | |
| 80 | The shared behaviour suite: |
| 81 | |
| 82 | ``` |
| 83 | make build |
| 84 | dist/app --listen 127.0.0.1:8080 --db $TMPDIR/swift.db & |
| 85 | ../../scripts/test-app.sh http://127.0.0.1:8080 $TMPDIR/swift-suite.json |
| 86 | ``` |
| 87 | |
| 88 | Before a deploy, `../../scripts/boot-check.sh dist/app-linux-arm64` runs |
| 89 | the arm64 binary under qemu on a unix socket and checks `/healthz`, |
| 90 | `/api/version` and a clean exit on SIGTERM. Then: |
| 91 | |
| 92 | ``` |
| 93 | git commit ... |
| 94 | make build-arm64 deploy MSG="what changed" |
| 95 | ``` |
| 96 | |
| 97 | The app was created on Carloku once with: |
| 98 | |
| 99 | ``` |
| 100 | carlos apps create --account bfj --app todo-swift --build-probe /api/version |
| 101 | carlos instances enable --account bfj --app todo-swift |
| 102 | carlos instances create --account bfj --app todo-swift --host todo-swift.bfj.oncarlos.com --channel edge |
| 103 | ``` |
| 104 | |
| 105 | ## Trade-offs |
| 106 | |
| 107 | - Trust class C: titles are stored as plain text, and anyone holding the |
| 108 | database or a replica can read every list. See `AGENTS.md`. |
| 109 | - A list belongs to a cookie. Clear it and the list is gone for good. |
| 110 | Abandoned lists are never erased. |
| 111 | - Every visit without a valid cookie creates a list row, crawlers |
| 112 | included. The rows are tiny, and there's no cleanup yet. |
| 113 | - The one SQLite connection serialises all queries. That's plenty for a |
| 114 | todo list. A busier app would add a read pool. |
| 115 | |