Decisions
Architecture Decision Records from projects I work on. These are working documents from real codebases.
MAR 2026
Accept all bookings, surface conflicts as notices
Early mutations like and rejected bookings whose dates fell outside the destination's date range. This felt safe but was wrong — real trip planning is messy. You book a...
Deploy full-stack preview environments for device testing
Testing feature branches means either deploying to production (risky) or testing locally (doesn't catch infra issues). Preview environments give each feature branch its own...
Use pessimistic row locking for modification ordering
The git-like modification tree (ADR-0046) requires ordered appending — each new modification must point to the current HEAD, and HEAD must advance atomically. With multiple...
Use SwiftUI previews with mock services
The MV + Environment Services architecture (ADR-0003) means views trigger fetches on and observe service properties. This is correct for the running app but breaks Xcode...
Use git-like modification tree for versioning
ADR-0033 established that trips are derived state, generated by replaying modifications (event sourcing). The modification history already supports going back in time — you...
Use newtype IDs to prevent identifier confusion
The codebase uses UUIDs as identifiers for many domain entities: trips, users, snapshots, modifications, invitations, accommodations, reservations, documents, journey legs,...
Use Apollo-generated types directly in iOS
Apollo iOS generates strongly-typed Swift structs from GraphQL fragments and operations. Early in the project, we maintained a parallel layer of domain model types (, , ,...
Use parse-don't-validate for type safety
Without a consistent validation approach, validation logic scatters throughout the codebase: Functions repeatedly validate the same inputs Validation failures occur deep in...
FEB 2026
Use device ID filtering for subscription signals
With real-time subscriptions (ADR-0041), a mutation on Device A publishes a signal via Redis pub/sub. All subscribers to that trip receive the signal — including Device A...
Adopt signal-not-payload for real-time subscriptions
Issue #111 introduces GraphQL subscriptions for real-time trip updates across a user's devices. The fundamental design question is how much data the subscription carries....
JAN 2026
Run local Grafana and Tempo for development observability
Production traces go to Grafana Cloud via Alloy (ADR-0053). But iterating on tracing instrumentation against production has a slow feedback loop and costs money per trace....
Use Alloy as credential boundary for telemetry
The backend and iOS app export OpenTelemetry traces and metrics to Grafana Cloud and Langfuse. The recommended OpenTelemetry approach is to export to a local collector rather...
Adopt CrowdSec for intrusion detection
The application runs on a public-facing Hetzner cluster with SSH and Traefik exposed to the internet. Any public endpoint receives constant automated scanning from bots...
Adopt pgBackRest GFS backup strategy
The application uses PostgreSQL as its primary data store with pgBackRest for backup and point-in-time recovery (PITR) to Cloudflare R2. The initial configuration used daily...
DEC 2025
Use custom error macros over anyhow/thiserror
The backend needs structured error codes for GraphQL (ADR-0006), error source chains for debugging, and automatic tracing on error creation. Standard Rust error crates don't...
OCT 2025
Use Tailscale for developer and CI access to the cluster
Developers and CI runners need to reach cluster services — deploying via Colmena, running database migrations, accessing preview environments (ADR-0055). This was done over...
Use Exa.AI for neighborhood discovery
Users need to understand destination neighborhoods before booking accommodations. When planning a trip to Lisbon, you want to know that Bairro Alto is nightlife, Alfama is...
Use MapKit Place ID for accommodation references
The app captures accommodation bookings as part of destinations. The initial approach considered storing full location data (coordinates, address, name) for each hotel. This...
Use iOS CLGeocoder for location enrichment
Locations need to be more than strings — coordinates, timezone, country code, and locality enable map visualizations, timezone-aware scheduling, and distance calculations....
SEPT 2025
Adopt passkey-first authentication with WebAuthn
The app needs user authentication. Passwords are the default but they're a known-weak pattern — reuse, phishing, credential stuffing. Passkeys (WebAuthn/FIDO2) provide...
Use S-expressions for GPT-5 effect generation
The trip planning system uses GPT-5 with CFG constraints to generate effects (ADR-0032). The original approach used a custom Lark grammar that output a domain-specific...
Model trips as destinations with effect-based modifications
Traditional trip planning follows a direct manipulation model: start with an empty itinerary, add each element through forms, the trip IS the data you entered. This doesn't...
Use GPT-5 with CFG for effect generation
The original architecture planned three layers: natural language → intents (via Apple Foundation Models) → effects (via rule interpreter) → trip modifications. This failed...
AUG 2025
Write component-specific GraphQL queries
A common anti-pattern is treating GraphQL queries like REST endpoints — creating "reusable" queries that fetch all possible fields to serve multiple components. This negates...
JUL 2025
Use NixOS and Colmena for infrastructure management
The application runs on a multi-node Hetzner cluster. Initial deployments used a custom script, but coordinating deploys across the cluster — ordering, error handling,...
Inject services via async-graphql Data context
Resolvers need access to services (database pools, external API clients, domain services). Options: a DI framework, a global singleton, or async-graphql's built-in container.
Use async-graphql for the API layer
The app needs an API layer between the iOS client and Rust backend. REST is the default, but Apollo iOS provides a normalized cache that acts as a source of truth for the UI...
Adopt OpenTelemetry tracing for iOS
The backend already has OpenTelemetry tracing exported to Grafana Cloud Tempo via an Alloy collector. But mobile requests appear as orphaned traces — the backend shows a 67ms...
Adopt stateless PASETO authentication
The authentication stack used PASETO tokens as transport for session IDs, with server-side sessions in Redis via tower-sessions and axum-login. ADR-0020 attempted to run...
Use response headers for token refresh
Key rotation (ADR-0026) means tokens can end up signed by deprecated keys. Mobile clients need updated tokens without explicit refresh requests or authentication interruptions.
Use tokio-cron for PASETO key rotation
The application runs as a long-lived Rust/Axum server on a 3-node cluster. PASETO key rotation needs to happen automatically — generate new keys, deprecate old ones, revoke...
Extend session duration for mobile
Users who book trips months in advance (booking a June trip in January) should remain logged in without re-authentication. Mobile devices are personal devices with a...
Use pasetors crate for PASETO
After choosing PASETO v4.local for session transport (ADR-0019), we needed a Rust crate that could handle the full lifecycle: token creation, validation, key generation, and...
Use PASERK identifiers for key identification
During key rotation, the server needs to know which key was used to create a given token without exposing key material.
Use PostgreSQL advisory locks for coordination
The application runs on a 3-node cluster. PASETO key rotation needs to happen on exactly one node at a time.
Support token and session auth in parallel
After adopting PASETO tokens for transport (ADR-0019), we had token generation working but no token validation middleware. The existing auth stack (tower-sessions,...
Adopt PASETO for session transport
The app started with HTTP session cookies via tower-sessions and axum-login. This worked on web but caused persistent lost-session bugs on iOS — cookies in native apps...
Use environment-based auth manager with Apollo interceptors
The iOS app makes authenticated GraphQL requests via Apollo Client. Authentication needs to be transparent to views — they shouldn't manage tokens, handle 401s, or know about...
Adopt three-tier design tokens in Swift
SwiftUI apps accumulate hardcoded colors, spacing values, and font sizes across components. Without a system, design changes mean hunting through multiple files, dark mode...
Adopt SnapshotPreviews for iOS visual testing
The iOS app needed visual regression testing — something like Chromatic or Storybook but for SwiftUI. The ecosystem in 2024 doesn't have a good middle ground: open source...
Use @Observable over @ObservableObject in iOS
SwiftUI historically used with properties for state observation. The problem is it triggers view redraws for any property change, even if the view doesn't use that...
Use Result types for specific scenarios
The LLM introduced Result types into the iOS authentication service. While Result types force explicit success/failure handling, async throws is the idiomatic Swift pattern...
Adopt domain-driven design for the backend
The backend will have complex business logic spanning users, trips, accommodations, reservations, journeys, memberships, and more. The traditional alternative is layered...
Use GraphQL schema evolution for API compatibility
Unlike web applications where frontend and backend deploy together and every client immediately gets the new bundle, native iOS apps update on the device's schedule. Users...
Use cfg!() runtime checks over #[cfg] conditionals
Rust provides two ways to handle conditional compilation based on build configuration: Compile-time conditionals (): Code is included/excluded at compile time Runtime checks...
Use structured error codes in GraphQL
We need to decide how to handle error messages across the GraphQL API and iOS client. The backend can either return user-facing error messages in English, or return...
Develop using thin vertical slices
We want short feedback loops when building features. If an idea takes days of backend and frontend work before you can see it running, that's too long to find out it doesn't...
Adopt two-layer iOS testing
We want good coverage so we can move fast, but not tests that overlap and create maintenance work. LLMs make generating tests trivial now, so the problem isn't writing enough...
Adopt MV + Environment Services for iOS
Having used React Query for server state management, the goal was to find a similar pattern for SwiftUI — something where views declare data needs and a cache layer handles...
Record architecture decisions
We need to record the architectural decisions made on this project. Traditional documentation claims to always be current, creating maintenance burdens and unrealistic...