A private iOS app that helps two people connect, grow, and stay in sync.
Privacy model · Architecture · Run it locally · Quality gates · Security
Mend is for couples, whether dating, married, co-parenting, or unlabeled, who want a structured practice they do together: people who are doing fine and want to understand each other better, and people working through a hard season. Partners move through a five-chapter journey of guided conversations, games, card decks, pulse checks, and challenges drawn from attributed frameworks (the Gottman Method, Emotionally Focused Therapy, PREP, and Nonviolent Communication). Mend is an educational tool. It does not diagnose, and it points people to crisis lines and professional help rather than standing in for them.
Beta on iOS, distributed through TestFlight. Mend is not on the App Store yet. The latest tagged
milestone is the v1.0.0-beta.1
pre-release, and CHANGELOG.md tracks changes since. Android and web targets are
configured but not released.
- The Journey. Five chapters, from making room for each other to building what comes next. Steps complete from evidence the app can see where possible (finished sessions, both partners' quiz results, adopted rituals), and each chapter ends with a pulse check. From chapter two on, both partners answer.
- Shared space. Two phones join one room with an invite code. Both partners answer the same daily question, and the app shows a partner's answer only after you have sent your own. A shared notes board sits alongside.
- Guided conversations and play. Structured talk sessions, card decks, couple games, multi-day challenges, stories, and a technique toolkit. Focused-support tracks cover hard seasons such as an affair, grief, serious illness, money stress, or a new baby, and differences such as neurodivergence or culture.
- Safety first. During onboarding Mend asks directly whether either person is afraid of the other, and an "I'm not sure" answer goes straight to help. The crisis-resources screen is reachable in every account and onboarding state, and a low pulse score shows professional-help options privately, on that person's own turn, before the phone is handed on.
- No streak pressure. Progress counts practiced days, not consecutive ones, and activity in future chapters cannot inflate the progress percentage. Both rules are covered by tests.
- Localized interface. English, Spanish, French, German, and Portuguese. Long-form guides and decks are English-only for now, and the app says so.
- Mend Plus. The first journey chapter, starter decks and games, the shared space, and all safety resources are included without paying. A Plus plan unlocks the remaining chapters and full content for both partners in a room. App Store purchasing is not enabled yet, so iOS builds have no purchase path today.
Mend holds some of the most sensitive things people write down. This section describes what the code in this repository does.
On the device
- Everything starts local. Profile, journey progress, session reflections, plans, pulse scores, and
quiz results are stored in AsyncStorage under
mend.*keys (src/lib/store.ts). The app keeps working offline, and creating the background account never blocks the UI. - An optional app lock (
src/lib/lock.ts) covers the app with a Face ID, Touch ID, or device-passcode prompt at launch and whenever it returns from the background. It is an access gate; the app does not add its own encryption to local data.
Accounts
- People can continue with Apple, use email and password, or skip sign-in. When someone skips, Mend
creates an anonymous Supabase account in the background, with no email or password, so backup and the
shared space can work (
src/lib/auth.tsx).
What leaves the device
- Backup. Whenever a Supabase session exists, including the anonymous one, the app copies its local
state into the user's row in
mend_state(at most once a minute) and the two partner names intomend_profiles, so a reinstall or new phone can restore progress (src/lib/sync.ts). That snapshot includes free-text session reflections and pulse scores. Settings describes this backup. - Shared space. Display names, daily answers, and notes are stored in Supabase so both phones can
see them. The app reads today's answers through the
mend_daily_revealRPC, which returns a partner's answer only after you have answered. Pulse scores and quiz results are not written to shared-space tables. - Nothing else. The app has no analytics, crash-reporting, or advertising SDKs in its dependencies. Network requests go to Supabase, to Apple when Sign in with Apple is used, and to web pages the user chooses to open.
- Data is encrypted in transit (TLS) and stored by Supabase. It is not end-to-end encrypted.
Server-side boundaries
- The only key the client ships is the Supabase publishable key, which is designed to be public
(
src/lib/supabase.ts). Server secrets (the service-role key and Stripe keys) are read only from Edge Function environment variables. - The Postgres
anonrole has no privileges on Mend's tables or RPCs. Signed-in users get narrow grants: rooms and memberships are read-only and change only throughSECURITY DEFINERRPCs with a pinnedsearch_path, entitlements are readable only throughmend_my_access(), and a unique constraint on room and role holds the two-person limit under concurrent joins (supabase/migrations). - Paid access is written only by the Stripe webhook after signature verification, with idempotent event
tracking. The client has no write grant on entitlements, and
tests/monetization-contract.test.tsguards that boundary. supabase/migrationsstarts from an existing schema: it records the grant, constraint, and billing changes applied since, not the original table and policy definitions.
Deletion
- Settings → Delete my account and data calls the
mend-delete-accountEdge Function and then removes everymend.*key from the device.
flowchart LR
subgraph Device["iOS app (Expo SDK 57)"]
Lock["Optional device lock"] --> Router["Expo Router screens<br/>src/app"]
Router --> Domain["Domain logic<br/>src/lib"]
Content["Attributed content library<br/>src/lib/content"] --> Domain
Domain --> Local[("AsyncStorage<br/>mend.* keys")]
end
Domain -- "publishable key + user JWT" --> Auth["Supabase Auth<br/>Apple · email · anonymous"]
Domain -- "backup, shared space" --> DB[("Postgres<br/>mend_* tables, definer RPCs")]
Domain -- "checkout, billing, deletion" --> Fn["Edge Functions"]
Stripe["Stripe"] -- "signed webhook" --> Fn
Fn --> DB
App. Expo SDK 57, React Native 0.86, React 19.2 with the React Compiler, TypeScript, Expo Router with
typed routes, Reanimated 4, and i18next. src/app/_layout.tsx gates navigation in
a fixed order: device lock, then sign-in or onboarding (which holds the safety question), then the four
tabs (Home, Journey, Talk, Explore). /safety stays reachable from every state.
Backend. One Supabase project.
| Piece | What it is |
|---|---|
| Tables | mend_state, mend_profiles, mend_spaces, mend_space_members, mend_daily_answers, mend_notes, mend_entitlements, mend_billing_events |
| Client RPCs | mend_create_space, mend_join_space, mend_leave_space, mend_daily_reveal, mend_space_progress, mend_my_access |
| Edge Functions | mend-checkout, mend-billing-portal, mend-stripe-webhook, and mend-delete-account (deployed; its source is not yet in this repository) |
| Contract check | scripts/check-backend-contract.mjs fails the build if the app uses a table, RPC, or function that is not on the reviewed list, or if the list goes stale |
Repository map
| Path | Purpose |
|---|---|
src/app |
Expo Router screens: tabs, onboarding, shared space, journey tools, settings, safety |
src/lib |
Auth, local store, backup, shared space, journey and momentum logic, entitlements, device lock |
src/lib/content |
Tracks, decks, games, challenges, daily questions, and safety resources, with sources |
src/locales |
Interface translations |
supabase/ |
Migrations, Edge Functions, and function config |
tests/ |
Vitest suites for journey math, momentum, recommendations, safety resources, and product contracts |
docs/research, docs/review |
Framework sources, plus safety, honesty, inclusivity, and contract reviews |
content/, social/ |
Reviewed insight notes and the generator for their social posts |
The product specification is in SPEC.md.
Requirements: Node.js 22.12+ or 24 (CI uses 22), npm, and Xcode for the iOS Simulator.
npm ci
npm run ios # build a development build and launch it in the iOS Simulator
npm start # start Metro for a development build that is already installed
npm run web # run the web target in a browserThe app talks to the project's Supabase instance by default. To use your own, set
EXPO_PUBLIC_SUPABASE_URL and EXPO_PUBLIC_SUPABASE_KEY (a publishable key). Never put a service-role
key in an EXPO_PUBLIC_* variable; those values are bundled into the app.
npm run verify is the single gate, and the Verify workflow runs exactly that command.
| Step | Command | Checks |
|---|---|---|
| Lint | npm run lint |
ESLint with eslint-config-expo |
| Types | npm run typecheck |
tsc --noEmit |
| Tests | npm test |
Vitest: journey progress, momentum, recommendations, crisis routes and the coercive-control boundary, monetization and positioning contracts |
| Content | npm run content:check |
Insight notes load and validate |
| Backend contract | npm run backend:contract |
App references match the reviewed Supabase surface |
| Expo | npm run expo:check |
Installed packages match Expo SDK 57 and the app config resolves |
npm run backend:smoke is an end-to-end check against a live Supabase project: three anonymous users,
room creation, a race between two joiners where exactly one may win, sealed then revealed daily answers,
shared notes, and account cleanup. It needs the two EXPO_PUBLIC_SUPABASE_* variables.
- Verify (
verify.yml) triggers on pull requests and pushes tomain, and runsnpm ciandnpm run verifyon Node 22. Itsverifyjob is the required status check formain. Third-party actions are pinned to commit SHAs and the token is read-only. - TestFlight (
testflight.yml) has no automatic trigger; it runs only on manual dispatch, behind thetestflightenvironment. It builds witheas build --localand submits with an App Store Connect API key held in repository secrets. The same build can be produced locally on a Mac. - Supply chain. Dependabot is configured for weekly npm and GitHub Actions updates, with
SDK-managed packages left to
npx expo install --fix. Secret scanning with push protection is enabled.
Read CONTRIBUTING.md before proposing a change. Changes that touch crisis resources, coercion boundaries, professional-help language, or content attribution need focused review and tests. Report vulnerabilities privately through SECURITY.md, and never put relationship data in a public issue. Mend is released under the MIT License.