Appearance
0009 — iOS app delivery ​
Status: Accepted (2026-06-18 — owner accepted during ADR review)
Date: 2026-06-17
ADO work item: AB#3077
Deciders: Kristopher Turner (platform owner)
Context ​
ADR 0002 locks the mobile technology choice — one React Native + Expo codebase that ships both iOS and Android — and ADR 0008 locks the composition contract: every surface is a thin client that reaches the platform only through packages/api-client over the backend API. What neither ADR addresses is the iOS-specific delivery story: the concerns that are genuinely iOS-only and require deliberate decisions before Phase 1 platform work begins.
The iOS surface is Epic AB#3077, decomposed into four Features:
- AB#3096 — React Native + Expo iOS build configuration (monorepo wiring, EAS Build, CI/CD)
- AB#3097 — Apple Push Notification service (APNs) integration, wired into the platform notification transport
- AB#3098 — Offline cache and sync of platform data (read-heavy content: sermons, calendar, announcements)
- AB#3099 — EAS Build → App Store review and publish pipeline
Several of these features carry non-trivial risk that should be resolved before implementation:
EAS Build + pnpm compatibility. EAS Build historically assumed Yarn (see ADR 0002 caveat). pnpm support has improved but requires explicit
eas.jsonconfiguration and validation early. A confirmed npm fallback exists if pnpm proves problematic on the EAS managed runtime.Sign in with Apple requirement. App Store Review Guidelines §4.8 require that any app offering third-party social login must also offer Sign in with Apple. ADR 0003 already includes Apple as a Clerk social provider, but the Apple Developer Program ($99/year) must be enrolled before any build or APNs configuration can be completed. This is the only unavoidable recurring cost for iOS delivery under the governing cost constraint.
App Store privacy / data-safety labels. Apple's "nutrition label" requires the publisher to disclose every data type the app collects, how it is used, and whether it is linked to identity. The platform collects name, email, family composition, content interaction, and device push tokens — all must be declared. For child sub-accounts (parent-managed, ADR 0007), COPPA compliance must be documented in both the privacy label and the App Store review notes.
Release cadence independence. The iOS App Store review cycle (typically 24–72 hours, can be longer) means the iOS release pipeline cannot share a deploy cadence with the web surface (which deploys on merge). EAS Build → TestFlight → App Store submission is a separate, explicitly managed pipeline. This is the one defensible exception to the single-CI-CD-pipeline model established in ADR 0004.
APNs push tokens. Device push tokens are Apple-issued, rotate on reinstall, and must be stored server-side (
DevicePushTokensor equivalent) and referenced by the platform notification transport. No client-side logic holds or acts on a push token beyond registering it viaapi-client.
The decisions below specify how the iOS surface composes on the platform, where the iOS-only concerns live, and how the release pipeline is structured.
This is a planning/architecture decision. It introduces no code; it defines the boundaries and sequencing that Feature AB#3096–AB#3099 implementations must honor.
Decision ​
We will deliver the iOS surface as a thin React Native + Expo client that reaches the platform exclusively via
packages/api-client, wiring the five iOS-specific concerns (APNs, offline cache, EAS Build pipeline, Sign in with Apple, and App Store labels/COPPA) into the platform's existing contracts — with a dedicated EAS Build → TestFlight → App Store submission pipeline that runs separately from the web deploy cadence.
How iOS composes on the platform ​
text
┌────────────────────────────────────────────────────────────┐
│ apps/mobile (RN + Expo) │
│ │
│ iOS-specific config layer │
│ • app.config.ts — Expo managed config (bundle ID, │
│ entitlements, APNs capability) │
│ • eas.json — EAS Build profiles (dev / preview / prod) │
│ • /ios/** — Expo-generated native project (not │
│ hand-edited; regenerated by `expo prebuild`) │
│ │
│ Shared RN feature code (web+mobile where applicable) │
│ src/features/<feature>/ — platform data via api-client │
│ src/features/notifications/ — registers APNs token │
│ src/features/offline/ — MMKV or SQLite read cache │
└──────────────────┬─────────────────────────────────────────┘
│ packages/api-client (typed SDK)
│ packages/shared-types (contracts)
┌──────────────────▼─────────────────────────────────────────┐
│ Platform core — apps/api │
│ Notification transport ← APNs channel (server-side send) │
│ DevicePushTokens storage ← token registered via api-client│
│ Offline-served data ← standard API endpoints (no bypass) │
└────────────────────────────────────────────────────────────┘The apps/mobile directory is a single codebase shared with Android (ADR 0002). iOS-specific items are limited to the Expo config layer and to the APNs notification channel inside the platform notification transport. No business logic, authorization, or data access lives in the mobile client.
iOS-only platform extensions ​
The following additions extend the platform (apps/api and packages/*) to support the iOS surface; they are not iOS client logic:
- APNs channel in the notification transport (platform responsibility 4, ADR 0008): the API sends push notifications via APNs using a server-side APNs auth key stored in Key Vault (kv-hcs-vault-01). Azure Notification Hubs free tier (≤500 devices) is the preferred dispatch layer; revisit at scale. The mobile client registers its device token once at login via
api-client; the API stores it in aDevicePushTokenstable and uses it to fan out notifications. The token registration endpoint is protected by the same Clerk-verified RBAC flow as all other API endpoints (ADR 0003, ADR 0006). shared-typesextension —DevicePushTokenRegistrationRequestandPushNotificationPayloadtypes are defined inpackages/shared-types; both the API and the mobile client import them.- Offline data cache (AB#3098) — the mobile client may cache read-heavy, non-sensitive platform responses (sermon recordings list, calendar events, announcements) locally using MMKV or Expo SQLite (to be confirmed in research spike S4). Cache population goes through
api-client; no direct DB access. Stale-while-revalidate refresh on reconnect. Child-profile data is not cached persistently on-device.
Release pipeline ​
EAS Build is the only sanctioned way to produce iOS binaries. The pipeline is:
GitHub Actions CI (lint + unit test)
└─► EAS Build (managed Expo runtime, pnpm or npm fallback)
├─► Internal distribution / TestFlight (preview profile)
└─► App Store submission (production profile)
└─► Apple Review (~24–72 h)
└─► Phased rollout → General availability- EAS Build runs as a GitHub Actions job triggered by a
release/mobile-*branch push or manual workflow dispatch. It is explicitly not coupled to the web deploy workflow (ADR 0004). - EAS secret management: APNs auth key and App Store Connect API key are stored in EAS Secrets (linked to the Expo project) and are not committed to the repository.
eas.jsonprofiles:development(Expo Go / simulator),preview(TestFlight internal), andproduction(App Store). Each profile pins the Expo SDK version to minimize build variance.
pnpm + EAS Build validation (required early) ​
Before AB#3096 development begins, a build-validation spike must confirm:
- EAS managed runtime accepts a pnpm workspace with
packageManagerfield set inpackage.json. eas buildresolves workspacepackages/*dependencies without falling back to npm install.- If pnpm is not supported in the EAS managed runtime at the time of validation,
npmis used for theapps/mobilesubtree only (scoped override in.npmrc); the rest of the monorepo stays pnpm. This is the documented fallback (ADR 0002) and does not require a new ADR.
Sign in with Apple and Apple Developer enrollment ​
Apple Developer Program enrollment (kris@hybridsolutions.cloud, $99/year) must be completed before any EAS Build profile, APNs configuration, or App Store submission can proceed. The Clerk Apple social provider (ADR 0003) uses the same App ID. This cost is accepted under the governing cost constraint as the minimum necessary for App Store distribution.
App Store privacy labels and COPPA ​
The privacy label submitted with each App Store release must accurately reflect the platform's data collection. At launch the label must cover, at minimum: name, email address, user ID (linked to identity; used for app functionality and account management); device push token (linked to device; used for notifications); and coarse usage data / crash diagnostics (not linked to identity; used for analytics via App Insights). Apple's "Managed Apple ID / parental controls" pathway does not apply to this app; instead, child sub-accounts are parent-managed within the platform (ADR 0007). App Store review notes must explain this model and state that the app does not allow children to create or manage their own accounts. Legal review of the privacy policy for COPPA applicability is flagged as a dependency before App Store submission.
Alternatives considered ​
| Option | Pros | Cons | Why not chosen |
|---|---|---|---|
| React Native + Expo + EAS Build (chosen) | One TS codebase for iOS + Android; reuses shared-types / api-client / ui packages; EAS Build abstracts Xcode toolchain; aligns with ADR 0002 | EAS Build + pnpm requires early validation; $99/yr Apple Developer fee; App Store review latency | — chosen |
| Native Swift (iOS only, separate codebase) | Full access to iOS platform APIs; no RN overhead | Separate codebase — duplicates all feature logic, breaks the ~1.3× effort model, requires a separate Swift developer; contradicts ADR 0002 | Rejected — cost and effort are prohibitive; ADR 0002 is locked |
| PWA only — no native app | Zero App Store overhead; free; single web surface to maintain | No App Store presence; Web Push on iOS (iOS 16.4+) is limited and requires Safari; no offline storage guarantees; lower member trust for a faith community app | Rejected — the platform strategy explicitly includes native mobile as a first-class surface |
| Capacitor (web-to-native wrapper) | Re-uses web HTML/CSS; single codebase for web + native | Slower than RN for complex native interactions; the ui package targets RN primitives; switching cost from the ADR 0002 commitment; EAS Build does not support Capacitor projects | Rejected — ADR 0002 is locked; Capacitor would require a parallel web-component design system |
| Expo Go only (no standalone build) | Near-zero build infrastructure | Cannot be distributed via App Store; APNs not available in Expo Go; not a production deployment model | Rejected — not viable for production |
Consequences ​
Positive ​
- iOS and Android share one codebase (
apps/mobile); feature work done for one surface benefits both with no duplication of business logic. - APNs push notifications are wired into the platform notification transport — the same transport serves email, SMS, and in-app channels, so a single API call fans out across all channels.
- Offline caching of read-heavy content (sermons, calendar, announcements) improves the experience for members in low-connectivity areas of rural southwest Virginia without any data-bypass risk.
- The App Store release pipeline is decoupled from web deploys, giving iOS its own review-aware cadence without blocking web hotfixes.
- APNs auth key and App Store Connect credentials live in EAS Secrets and Key Vault — never in the repo, consistent with ADR 0004 secret management.
Negative / trade-offs ​
- Apple Developer fee ($99/year) is an unavoidable ongoing cost. Accepted under the cost constraint as the minimum for App Store distribution; revisit if a nonprofit fee waiver becomes available.
- Release latency — App Store review (24–72 h typical, occasionally longer) means iOS bug fixes cannot ship as fast as web. This is inherent to the App Store model and cannot be mitigated without an enterprise distribution profile (not available to this organization type).
- EAS Build minutes — the EAS free tier includes limited build minutes per month. Monitor usage; upgrade or optimize build profiles if minutes are exhausted. Android builds share the same quota.
- Native project regeneration —
expo prebuildoverwrites the/iosdirectory; any hand-edits to Xcode project files are lost. All native configuration must live inapp.config.tsor Expo config plugins. This is an Expo managed-workflow constraint, not unique to this project. - Offline cache scope is narrow — child-profile data and anything requiring fresh authorization is explicitly excluded from the local cache. This limits the offline experience but is the correct trade-off for a closed, RBAC-enforced community platform.
Risks ​
- EAS Build + pnpm incompatibility — if the EAS managed runtime does not support pnpm workspaces at the time of AB#3096 development, the npm fallback (scoped to
apps/mobile) must be applied. Mitigation: validate in a spike before any Feature AB#3096 story begins; document the outcome and update this ADR if needed. - Apple Review rejection — an incomplete or inaccurate privacy label, or an unclear explanation of the child sub-account model, can cause App Store rejection and delay launch. Mitigation: draft the privacy label and review notes in parallel with AB#3099; obtain legal review of COPPA applicability before submission.
- APNs token rotation — device tokens are invalidated on app reinstall or OS upgrade. Stale tokens cause silent notification failures. Mitigation: the platform notification transport must handle APNs feedback (invalid-token errors) and purge stale
DevicePushTokensrecords automatically. - Expo SDK version lock — pinning the Expo SDK version in
eas.jsonprevents surprise breakage but accumulates upgrade debt. Mitigation: schedule an Expo SDK upgrade review each quarter and treat it as platform maintenance, not a feature. - Apple Developer account continuity — if the account lapses (billing failure) or the Apple ID is locked, distribution certificates expire and the app cannot be updated. Mitigation: use an organizational Apple ID (kris@hybridsolutions.cloud); monitor renewal; add a calendar reminder 30 days before the annual renewal date.
References ​
- ADR 0001 — Monorepo + three-layer structure
- ADR 0002 — Mobile: React Native + Expo
- ADR 0003 — Authentication (Clerk, social login)
- ADR 0004 — Cloud/hosting stack + CI/CD
- ADR 0005 — Observability model
- ADR 0006 — Two-plane RBAC
- ADR 0007 — Account & Family-Group identity
- ADR 0008 — Platform composition
- Platform strategy — Layer 2: Mobile apps
- ADO: Epic AB#3077 (iOS app), Feature AB#3096 (RN+Expo build), AB#3097 (APNs push), AB#3098 (offline cache/sync), AB#3099 (App Store publish via EAS)