HushMap is an iOS app (SwiftUI + SwiftData) with a thin, secure serverless layer for AI. This document explains how the pieces fit together and the decisions behind them.
- Sensory data is the product. Every architectural choice serves getting an accurate noise/crowd/lighting read to the user quickly and honestly.
- Never ship secrets. The OpenAI key lives only in server-side Secret Manager; the app authenticates to the proxy with App Check.
- Degrade gracefully. AI is an enhancement, not a dependency — the map, reports, and a rule-based prediction fallback all work if AI is unavailable.
- Respect the device. Rendering and clustering adapt to device capability so older iPhones stay smooth.
flowchart TD
subgraph device["iOS App - SwiftUI + SwiftData"]
V["Views (SwiftUI)"] --> VM["ViewModels"]
VM --> S["Service layer<br/>(MainActor singletons)"]
S --> DB[("SwiftData")]
S --> AC["App Check"]
end
subgraph cloud["Firebase"]
CF["Cloud Functions<br/>predictSensory / interestingFact"]
FS[("Firestore")]
SM["Secret Manager"]
end
OA["OpenAI gpt-4.1-mini"]
GM["Google Maps SDK"]
GP["Google Places API"]
S -->|App Check token| CF
CF --> SM
CF --> OA
S <--> FS
S --> GM
S --> GP
- SwiftUI views using
@State/@StateObjectfor owned state and@ObservedObjectfor injected view models. - View models expose
@Publishedproperties for reactive updates. - SwiftData
@Querydrives automatic UI refresh from the local store. - Navigation is sheet-based (no UIKit navigation controllers); onboarding/welcome flow is managed at app level in
HushMapApp.
The heart of the app: 20+ @MainActor singletons, each owning one concern. Highlights:
| Service | Responsibility |
|---|---|
PredictionService |
Orchestrates AI + algorithmic sensory predictions; blends real reports, time, weather. |
OpenAIService |
Thin client for the Cloud Functions AI proxy (no OpenAI key on device). |
SensoryProfileService |
On-device learning of the user's personal sensory tolerances. |
SmartNotificationService |
Proactive warnings near incompatible venues. |
AudioAnalysisService |
Real-time dB measurement mapped to a sensory scale. |
GoogleMapsService / PlaceService |
Map configuration, POI/place lookups. |
ReportSyncService / FirestoreService |
Community report sync with Firestore. |
AuthenticationService |
Google / Apple / anonymous auth. |
DeviceCapabilityService |
Categorizes device tier to scale rendering. |
- SwiftData — local persistence via
@Modeltypes (Report,User,SensoryProfile,QuickUpdate, …). Relationships wired with proper inverses. - Firestore — shared community reports, synced in the background.
TypeScript Cloud Functions (Node 20) that proxy OpenAI:
predictSensory— Chat Completions with a strict JSON schema (Structured Outputs). Returnsnoise_level/crowd_level/lighting_level/summary.interestingFact— Responses API with the hostedweb_searchtool for grounded facts, plus citation cleanup and aNO_FACTsentinel.
Both enforce App Check (enforceAppCheck: true) and read the OpenAI key from Secret Manager. See functions/DEPLOY.md.
sequenceDiagram
participant U as User
participant App as iOS App
participant CF as predictSensory (Cloud Function)
participant AI as OpenAI
U->>App: Tap a place on the map
App->>App: Gather venue, time, weather, nearby reports
App->>CF: call(payload) + App Check token
CF->>AI: Chat Completions (strict JSON schema)
AI-->>CF: { noise, crowd, lighting, summary }
CF-->>App: Structured result
App->>U: Render sensory levels + advice
Note over App: On any failure → local rule-based fallback
SensoryProfileService updates the user's tolerance model from each report using comfort-weighted exponential moving averages — it learns preferences from comfortable experiences rather than inverse-correlating, which prevents preference collapse. Confidence grows logarithmically with report count and feeds both recommendations and smart notifications.
- No embedded secrets — OpenAI key is server-side only.
- App Check (App Attest) attests that requests come from a genuine build before the functions run.
- Google keys are kept out of version control in
Config-Local.xcconfig(gitignored) and should be bundle-ID-restricted in Cloud Console. - Privacy manifest (
PrivacyInfo.xcprivacy) declares data collection and required-reason API use.
DeviceCapabilityServicecategorizes devices (High/Medium/Low) and scales marker complexity, clustering thresholds, and animation durations.- Map markers are capped and clustered on lower-end devices.
- AI predictions are cached to reduce API calls.
../functions/DEPLOY.md— deploying the AI proxy + IAM/org-policy troubleshooting.../CONTRIBUTING.md— development workflow.../CLAUDE.md— detailed guidance and patterns for contributors and tooling.