Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,30 @@ updates:
dependency-type: production
development-dependencies:
dependency-type: development
# The Expo SDK pins compatible versions of these packages. Move them with
# `npx expo install --fix` or a deliberate SDK upgrade, not one bump at a
# time: a jump past the SDK's versions fails `expo install --check` and
# has crashed the app at launch before (see CHANGELOG). Updates inside
# the SDK line still come through, and alerts are unaffected.
ignore:
- dependency-name: "expo"
update-types: ["version-update:semver-major"]
- dependency-name: "expo-*"
update-types: ["version-update:semver-major"]
- dependency-name: "@expo/*"
update-types: ["version-update:semver-major"]
- dependency-name: "eslint-config-expo"
update-types: ["version-update:semver-major"]
- dependency-name: "react"
update-types: ["version-update:semver-major", "version-update:semver-minor"]
- dependency-name: "react-dom"
update-types: ["version-update:semver-major", "version-update:semver-minor"]
- dependency-name: "react-native"
update-types: ["version-update:semver-major", "version-update:semver-minor"]
- dependency-name: "react-native-*"
update-types: ["version-update:semver-major", "version-update:semver-minor"]
- dependency-name: "@react-native-async-storage/async-storage"
update-types: ["version-update:semver-major", "version-update:semver-minor"]
open-pull-requests-limit: 5
- package-ecosystem: github-actions
directory: /
Expand Down
11 changes: 11 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,17 @@ All notable changes to Mend are documented in this file. The project follows
everyday growth as well as difficult seasons.
- Updated maintained translations and product documentation to match the broader
relationship-improvement promise.
- Rewrote the README around the app's actual privacy model, data flows, and
release status.

### Security

- Updated Expo SDK 57 packages to the versions Expo expects and refreshed the
lockfile, resolving advisories in @xmldom/xmldom, brace-expansion,
browserslist, baseline-browser-mapping, js-yaml, nanoid, postcss,
source-map-js, sharp, vitest, shell-quote, compression, and image-size.
- Upgraded vitest to 5.x.
- Stopped Dependabot from proposing version jumps that break the Expo SDK pins.

### Fixed

Expand Down
241 changes: 180 additions & 61 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,98 +1,217 @@
# Mend

<p align="center">
<img src="assets/images/icon.png" alt="Mend app icon" width="120" />
<img src="assets/images/icon.png" alt="Mend app icon" width="112" />
</p>

<p align="center"><strong>A free, private app for two people who want to understand each other better, grow closer, and stay in sync.</strong></p>
<h1 align="center">Mend</h1>

<p align="center"><strong>A private iOS app that helps two people connect, grow, and stay in sync.</strong></p>

<p align="center">
<a href="#product">Product</a> |
<a href="#architecture">Architecture</a> |
<a href="#development">Development</a> |
<a href="SECURITY.md">Security</a>
<img alt="Expo SDK 57" src="https://img.shields.io/badge/Expo_SDK-57-000020?logo=expo&logoColor=white" />
<img alt="React Native 0.86" src="https://img.shields.io/badge/React_Native-0.86-61DAFB?logo=react&logoColor=black" />
<img alt="TypeScript" src="https://img.shields.io/badge/TypeScript-6-3178C6?logo=typescript&logoColor=white" />
<img alt="Supabase" src="https://img.shields.io/badge/Supabase-Postgres_%2B_Auth-3FCF8E?logo=supabase&logoColor=white" />
<a href="LICENSE"><img alt="MIT License" src="https://img.shields.io/badge/license-MIT-blue" /></a>
</p>

Mend guides two people through a staged journey of attention, curiosity, communication, trust, and shared direction. It supports everyday relationship growth as well as repair when things feel difficult. Its exercises draw from attributed relationship frameworks including Gottman, EFT, PREP, and NVC. Mend does not diagnose, replace professional care, or invent clinical guidance.

**Stack:** Expo 57, React Native, TypeScript, Expo Router, Supabase, AsyncStorage, i18next, EAS local builds, and GitHub Actions.
<p align="center">
<a href="#privacy-model">Privacy model</a> ·
<a href="#architecture">Architecture</a> ·
<a href="#run-it-locally">Run it locally</a> ·
<a href="#quality-gates">Quality gates</a> ·
<a href="SECURITY.md">Security</a>
</p>

## Product
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.

- **The Journey:** a five-chapter path with shared steps, visible progress, pulse movement, and earned milestones
- **Shared Space:** a private place for answers, notes, plans, appreciation, and progress
- **Connection tools:** guided conversations, games, card decks, pulse checks, and challenges
- **Focused support:** optional tracks for conflict, trust, intimacy, major changes, and other specific seasons
- **Safety routing:** crisis and professional-help resources remain reachable without a paywall
- **Privacy:** optional device lock, private local records, explicit account controls, and no advertising
- **Independence:** Mend builds repeatable skills without streak pressure or dependence on the app
## Status

The complete product contract is in [SPEC.md](SPEC.md). Research, safety, honesty, inclusivity, and content-review artifacts live under [`docs/`](docs/).
**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`](https://github.com/erickdronski/mend-app/releases/tag/v1.0.0-beta.1)
pre-release, and [CHANGELOG.md](CHANGELOG.md) tracks changes since. Android and web targets are
configured but not released.

## Features

- **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.

## Privacy model

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`](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`](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`](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 into
`mend_profiles`, so a reinstall or new phone can restore progress ([`src/lib/sync.ts`](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_reveal` RPC, 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`](src/lib/supabase.ts)). Server secrets (the service-role key and Stripe keys) are
read only from Edge Function environment variables.
- The Postgres `anon` role has no privileges on Mend's tables or RPCs. Signed-in users get narrow grants:
rooms and memberships are read-only and change only through `SECURITY DEFINER` RPCs with a pinned
`search_path`, entitlements are readable only through `mend_my_access()`, and a unique constraint on
room and role holds the two-person limit under concurrent joins
([`supabase/migrations`](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.ts`](tests/monetization-contract.test.ts) guards that boundary.
- `supabase/migrations` starts 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-account` Edge Function and then removes
every `mend.*` key from the device.

## Architecture

```mermaid
flowchart LR
Router["Expo Router screens"] --> Domain["Journey, space, content, and safety domains"]
Domain --> Local["Private local state and device lock"]
Domain --> Auth["Supabase Auth"]
Domain --> Sync["Explicit shared-space sync"]
Sync --> DB["Supabase data boundary"]
Content["Attributed content library"] --> Domain
Safety["Safety gate and crisis resources"] --> Domain
Actions["GitHub Actions"] --> Verify["Lint, TypeScript, tests, and Expo checks"]
Verify --> Release["Manual local EAS build and TestFlight submission"]
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
```

Safety content and routing are product-critical code. Changes to crisis resources, coercion boundaries, professional-help language, or content attribution require focused review and tests.
**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`](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.

## Repository map
**Backend.** One Supabase project.

| Path | Purpose |
| Piece | What it is |
| --- | --- |
| `src/app` | Expo Router screens and navigation |
| `src/lib` | Auth, privacy, sync, progression, safety, and content domains |
| `src/components` | Shared product UI |
| `src/locales` | Maintained translations |
| `docs/research` | Source material and decision records |
| `docs/review` | Safety, inclusivity, honesty, and contract audits |
| `.github/workflows` | Continuous verification and manual TestFlight delivery |
| 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`](supabase/functions/mend-checkout/index.ts), [`mend-billing-portal`](supabase/functions/mend-billing-portal/index.ts), [`mend-stripe-webhook`](supabase/functions/mend-stripe-webhook/index.ts), and `mend-delete-account` (deployed; its source is not yet in this repository) |
| Contract check | [`scripts/check-backend-contract.mjs`](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 |

## Development
**Repository map**

Requirements: Node.js 22 and npm.
| 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 |

```sh
npm ci
npm run start
```
The product specification is in [SPEC.md](SPEC.md).

## Run it locally

Verification:
Requirements: Node.js 22.12+ or 24 (CI uses 22), npm, and Xcode for the iOS Simulator.

```sh
npm run lint
npm run typecheck
npm test
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 browser
```

Useful platform commands:
The 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.

```sh
npm run ios
npm run android
npm run web
```
## Quality gates

Do not commit private credentials. Supabase's publishable client key is not a server secret; privileged operations must remain behind protected server boundaries.
`npm run verify` is the single gate, and the Verify workflow runs exactly that command.

## Delivery
| 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 |

Pull requests run lint, TypeScript, tests, and Expo configuration validation. `.github/workflows/testflight.yml` is manual-only and performs a local EAS iOS build before submitting the resulting artifact to TestFlight.
`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.

## Status
## CI and release

Mend is in TestFlight release development. Public claims should stay aligned with the tested build and the evidence in this repository.
- **Verify** ([`verify.yml`](.github/workflows/verify.yml)) triggers on pull requests and pushes to
`main`, and runs `npm ci` and `npm run verify` on Node 22. Its `verify` job is the required status
check for `main`. Third-party actions are pinned to commit SHAs and the token is read-only.
- **TestFlight** ([`testflight.yml`](.github/workflows/testflight.yml)) has no automatic trigger; it runs
only on manual dispatch, behind the `testflight` environment. It builds with `eas build --local` and
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.

## Contributing, security, and license

Read [CONTRIBUTING.md](CONTRIBUTING.md) before proposing a change. Report vulnerabilities privately using [SECURITY.md](SECURITY.md). Mend is available under the MIT License; see [LICENSE](LICENSE).
Read [CONTRIBUTING.md](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](SECURITY.md), and never put relationship data in a
public issue. Mend is released under the [MIT License](LICENSE).
Loading
Loading