Decisions
Architecture decision records from a trip-planning app we're building. It has a Rust and GraphQL backend hosted on a small NixOS cluster, and a SwiftUI app for iPhone and Mac that shares a Rust core with the server. Each record is one choice, the alternative we turned down, and why.
AUG 2026
Run infrastructure probes as Rust integration tests in the CI suite
Infrastructure checks are tests in the suite CI runs, reading the configuration that's actually deployed.
Check the GraphQL schema against a committed compatibility floor
The schema only grows, and the build checks it against a committed floor of the oldest supported schema.
Bound GraphQL request cost at input size instead of per-field query complexity
Limits go on input size where data enters, and superlinear reads are bugs to fix.
Grant tailnet membership reachability only, and make every service authenticate
Being on the tailnet grants reachability and no authority, so every service authenticates on its own.
JUL 2026
Converge concurrent edits by id-addressed replay and escalate non-commuting pairs
Concurrent edits converge by replay, and combinations nobody intended are flagged to a person.
Address destinations by id and batch-local handles in the effect vocabulary
Effects name destinations by id, with batch-local handles for things created in the same batch.
Replay the effect log on-device through a shared Rust core
The app replays the trip locally through the same Rust core the server runs, and syncs changes it has already applied.
JUN 2026
Build native macOS from the iOS SwiftUI target with an adaptive NavigationSplitView root
One target serves both platforms: presentation adapts to the platform and logic never forks.
Replace the pre-merge click-through with Maestro flows run locally on maestro-runner
Black-box Maestro flows against a seeded backend replace the manual check before merging, run locally.
Deploy NixOS nodes with Colmena, building closures on an ephemeral x86 Hetzner builder
Nodes run NixOS and deploy with Colmena, with closures built on a throwaway x86 machine that holds no cluster credentials.
MAY 2026
Replace upsert effects with create, update and remove, and embed bookings in creates
Separate create, update and remove effects, bookings embedded in their create effect, and ids derived per kind.
APR 2026
Serve MCP over stateless Streamable HTTP with JSON responses
Every MCP request stands alone, so any replica can serve it.
Route every modification write through one apply-and-rebuild service method
One service method stores, rebuilds, commits and publishes every modification for every transport.
Test MCP tool selection with a multi-model Promptfoo eval suite
Promptfoo evals run against the real server instructions and gate every change to tool descriptions.
Hand-roll a minimal OAuth 2.1 authorization server for MCP
A small in-house OAuth 2.1 server issues our own tokens, where oxide-auth didn't fit.
Serve the planning engine as deterministic MCP tools from the GraphQL binary
MCP tools wrap the same domain services as GraphQL, and the user's own model is the agent.
Diff versions on the server as fork point, branch history and end state
The server returns a typed diff of fork point, each branch's history and the end states, so agents never compare trips themselves.
MAR 2026
Accept every booking and derive discrepancies from the rebuilt trip
Every booking is accepted, and anything that doesn't fit the plan becomes a dismissible notice.
Deploy each ready pull request as its own Nomad job and Postgres database
Each pull request gets its own Nomad deploy, database and Redis namespace on the production cluster.
Order a trip's modifications under a FOR UPDATE lock on the trip row
Writes lock the trip row, and replay orders by the position assigned under that lock, never by timestamp.
Model trip versions as named refs into a modification tree
Modifications form a tree and versions are pointers into it, which replaced copy-on-branch and a what-if mode.
Wrap every domain identifier in its own UUID newtype
Every identifier is its own type, so a swapped ID fails to compile instead of corrupting a trip.
Use generated types directly in the iOS app instead of a hand-written mirror model
The app uses generated and shared types directly and keys the cache by type and id, with no hand-written mirror model.
FEB 2026
Publish trip-id signals over GraphQL subscriptions and filter each device's own echo on the server
Subscriptions carry only a trip id, the server drops each device's own echo, and clients pull the change.
JAN 2026
Trace from the app through to the backend via a per-node Grafana Alloy collector that holds every credential
Traces run from the app into the backend, and only the per-node collector holds telemetry credentials.
Detect intrusions from Traefik and sshd logs with CrowdSec
CrowdSec bans attackers from Traefik and SSH logs, since packet inspection can't see inside TLS.
Back up Postgres with pgBackRest in weekly fulls and daily differentials
Weekly fulls kept for a year, daily differentials and incrementals, stored in R2 for free egress on restore.
DEC 2025
Generate each domain's error types with a declarative macro
A macro generates each domain's error codes, types and GraphQL extensions, replacing anyhow and hand-written thiserror glue.
OCT 2025
Generate neighbourhood guides with Exa's answer endpoint and keep the output display-only
Exa generates neighbourhood overviews, and the output is only ever displayed, which bounds what a prompt injection can do.
Reference accommodations by Apple Maps Place ID with a name and coordinate snapshot
An accommodation stores a Place ID plus a small snapshot, and never a copy of details that go stale.
Geocode destinations on-device with MapKit and record the result as an effect
The phone geocodes locations with Apple's geocoder, so the backend needs no paid geocoding API.
SEPT 2025
Make discoverable WebAuthn passkeys the primary sign-in
Passkeys are the main sign-in, with email and password as the fallback.
Test external API calls against recorded reqwest-vcr cassettes
Tests replay recorded model responses, re-recorded in the same change that alters a prompt.
Event-source trips as an append-only log of effects
A trip is rebuilt from an append-only log of resolved effects, and removing a destination keeps its bookings.
Generate effects directly with GPT-5 as S-expressions under a Lark grammar
The model writes effects directly as S-expressions, constrained by a Lark grammar that tests hold to the effect enum.
AUG 2025
Write component-specific GraphQL queries instead of reusable ones
Each view writes its own query for exactly what it shows, instead of sharing queries that fetch everything.
JUL 2025
Serve the API as GraphQL with async-graphql on Axum
The API is GraphQL on async-graphql, because the app is built around Apollo's cache.
Use device-bound PASETO tokens as the only session state
The PASETO token is the whole session: user and device only, device-bound, 90 days sliding, refreshed through a response header.
Serialize PASETO key rotation across nodes with pg_try_advisory_xact_lock
A non-blocking, transaction-scoped Postgres lock makes sure one node rotates keys, with no extra coordination service.
Persist Apollo's normalized cache in SQLite and clear it on sign-out
The GraphQL cache lives in SQLite so it survives a restart and works offline, and it's cleared on sign-out.
Organise the backend into domain modules that query Postgres through sqlx directly
Code is grouped by business domain, with no technical layers and no repository abstraction.
Return GraphQL error codes in extensions and localize them on iOS
The API returns machine-readable error codes and the app turns them into localized messages, so the server never writes English for the user.
Test and preview iOS services over a mocked Apollo network transport
Unit tests and SwiftUI previews both run real services over a mocked Apollo network transport.
Serve SwiftUI views from @Observable environment services over Apollo's normalized cache
Thin SwiftUI views call observable services from the environment, with Apollo's cache as the source of truth for fetched data, in place of MVVM or a global store.