Architecture
Apple Clients
iOS 18 · Swift 5.9
- 100% SwiftUI, XcodeGen-managed project
- Widget extension (WidgetKit)
- VoiceOver + Dynamic Type throughout
watchOS 10 Companion
- Standalone watch app + watch widgets
- WatchConnectivity phone↔watch bridge
remote-notificationbackground mode: pushes wake the app and play the haptic pattern with the screen off
The product's core loop — feel a message, don't read it — dictates the architecture: delivery must terminate in a locally rendered haptic, so every backend decision below optimises for push latency and wake reliability rather than payload size.
Identity & Sessions
Sign in with Apple, Verified Server-Side
The client exchanges its Apple identity token at POST /auth/exchange. A Lambda validates the token's signature against Apple's published JWKS, upserts the user, and mints a short-lived session JWT — signed with a secret that lives only in SSM Parameter Store (SecureString), never in code, env files, or Terraform state.
Custom Lambda Authorizer
Every subsequent route sits behind an API Gateway custom authorizer that verifies the session JWT before the handler runs. No third-party auth SDK ships in the app; the entire identity path is ~200 lines of auditable code.
API & Data
Five Routes, Zero Dependencies
An API Gateway HTTP API fronts Node 20 Lambdas written with zero npm dependencies — built-in crypto, built-in http2, and the AWS SDK v3 provided by the runtime. Artifacts stay tiny, cold starts stay short, and there is no supply chain to audit.
POST /auth/exchange— Apple token → session JWTPUT /me— set discovery phone-hash + display namePOST /devices— register an APNs device tokenPOST /friends/find— hashed contacts → who's on TapPOST /taps— send a tap → push to recipient devices
DynamoDB, Modelled Per Access Pattern
Three single-purpose tables — users, devices, taps — all DynamoDB on-demand. Every query is a key lookup; there are no scans anywhere in the hot path.
Push Delivery
Raw HTTP/2 to APNs
The send-tap Lambda signs an ES256 provider token from a .p8 key held in SSM and speaks HTTP/2 directly to APNs — no push-provider middleman. Taps fan out to every device the recipient has registered, and the watch renders the haptic pattern on arrival.
Privacy Engineering
The server never sees an address book. Phone numbers are SHA-256 hashed on-device with CryptoKit; /friends/find intersects hashes against hashes. A breach of the backend yields no raw contact data because raw contact data never arrives.
Infrastructure & CI/CD
Terraform, End to End
The entire backend is declarative Terraform: an api module (gateway, authorizer, Lambdas, tables, SSM parameters), a budget module (cost alerts scoped to the project's CostCenter tag), and remote state in a shared S3 bucket with per-project keys and lockfiles.
OIDC-Federated GitHub Actions
A one-time bootstrap stack provisions two least-privilege IAM roles — gha-tap-plan and gha-tap-apply — that GitHub Actions assumes via an OIDC handshake. Pull requests plan; main applies. There are no long-lived AWS credentials in CI, in the repo, or on any machine.
Cost Envelope
Everything is pay-per-request — API Gateway, Lambda, DynamoDB, APNs are free at the protocol level — so the deployed backend idles at effectively $0/month, with a $50 budget alarm as the guardrail. The same architecture scales to real traffic without a re-platform.
Delivered Scope
Tap for iOS + widgets · Tap for watchOS + widgets
Production AWS backend (5 routes, 3 tables, APNs pipeline)
Terraform stack + bootstrap · GitHub Actions plan/apply pipelines