carlos / todomvc-bakeoff Public
| Sources/ | ||
| Tests/ | ||
| static/ | ||
| tools/ | ||
| .gitignore | ||
| AGENTS.md | ||
| DEPS | ||
| Makefile | ||
| Package.resolved | ||
| Package.swift | ||
| README.md | ||
| STATUS.md |
đŸ¤– todo-swift
TodoMVC for the CARLOS bakeoff, written in Swift. It's live at 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 | 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 | 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 | 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.swiftreads the flags and starts the server.Server.swiftparses--socket/--listen/--db, works out the app's own origin (https://$CARLOS_HOSTon a socket,http://127.0.0.1:<port>with--listen) and runs Hummingbird until SIGTERM.Routes.swiftholds every route, including theGET /handler.Visitors.swiftgives each visitor a list through atodo_listcookie (128 random bits,HttpOnly; SameSite=Lax; Path=/, plusSecureon a socket, where the edge serves https). It also holds theOrigincheck that answers 403 to cross-site posts.Store.swiftis the SQLite layer: WAL mode, append-only migrations tracked byPRAGMA 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.swiftrenders the page fromshared/reference.html's markup.static/app.jsis the JavaScript;static/app.cssplaces 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.jsonBefore 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 edgeTrade-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.