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-notification background 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 JWT
  • PUT /me — set discovery phone-hash + display name
  • POST /devices — register an APNs device token
  • POST /friends/find — hashed contacts → who's on Tap
  • POST /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

Zero-Knowledge Contact Matching

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