Closer/review2.md

137 lines
8.5 KiB
Markdown

# Closer vs Paired — Code & Product Review (v2)
*Regenerated 2026-06-20 by Ripley (original `review.md` was lost; this is a fresh pass with current codebase.)*
## TL;DR
Closer is technically more private (real E2EE, no social graph) and more interactive (spin wheel, date swipe, capsules) than Paired. But Paired wins on **content credibility** (named experts), **outcome claims** (89% in 3 months), and **positioning** ("5 minutes a day"). Closer's biggest leverage is **closing the credibility gap** — Paired's biggest vulnerability is that their privacy claims are weaker than yours.
---
## What Paired Does Well (the bar)
| Paired strength | Closer today | Gap |
|---|---|---|
| 4M users, 200K+ reviews, 4.7★ — distribution signal | Private MVP, no users | Distribution problem, not code |
| **Expert-backed content** — named clinicians with credentials | Bundled prompts, no byline | **High-leverage gap** |
| "5 minutes a day" positioning | Daily question exists, no time framing | Easy fix on Home |
| 1000+ expert-led quizzes & games | 6000+ prompts, 6 game types | **Closer wins on volume, loses on perceived quality** |
| "Share the load" mental-load narrative | Generic "deepen connection" copy | Sharper positioning needed |
| "89% see positive changes in 3 months" | No outcomes data | **Critical for premium conversion** |
| Physical product shop (store.paired.com) | None | Out of scope now, future hook |
## What Closer Wins On vs Paired
1. **Real E2EE** — Tink on Android, couple-owned keys. Paired *can't* match this without rewriting their stack.
2. **Anti-social architecture** — no feed, no followers, no public profiles. Closer is structurally a couple-only tool.
3. **Spin wheel** — genuinely more playful than Paired's list-browsing.
4. **Time capsules (Memory Lane)** — Paired doesn't have this. **Lean into this in marketing.**
5. **Own your backend** — Firebase + Cloud Functions, no vendor lock-in.
6. **Native iOS+Android parity** — same data model, shared truth.
---
## What Might Be Missing (Niche Hits)
### High-impact, low-cost
1. **"5-minute check-in" framing on Home** — set the time budget expectation upfront.
2. **Expert bylines on packs** — even one couples therapist changes perceived quality. *"Designed with Dr. Jessica Griffin."* Cheaper than you think.
3. **Streak visible on Home** — code has it, surface it bold.
4. **Outcomes capture** — 30/60/90 day mood/connection survey. Use for marketing + premium conversion.
5. **Time labels on questions**`2 min` / `5 min` / `10 min` filter on question packs.
### Medium-impact
- **"Why this matters" prompt card** — 1-sentence nudge before each question.
- **Audio answers** — voice clips, stored encrypted, played on reveal. Closer is uniquely positioned.
- **Mood check-in** — 1-tap daily mood (no emoji in chrome), trend over time.
- **Compatibility report** — at 30/60/90 days, generate "what you've learned about each other" PDF. Email it. **Strong viral loop.**
### Premium-only, high-value
- **"Couples therapy lite" track** — therapist-authored multi-week courses.
- **Anniversary/milestone moments** — first answer, 100th answer, 1-year. Code has the data.
---
## What the Code Might Do Wrong
### 🔴 Real risks
1. **Placeholder URLs in `ExternalLinks.kt`** — 4 `TODO` lines pointing at `https://closer.app/*` and `https://couplesconnect.app/support` (mismatched brand). These will ship to production and break Settings → Privacy/Terms/Support links.
- File: `app/src/main/java/app/closer/core/navigation/ExternalLinks.kt`
- **Status: FIXED** (commit d83e557 area) — URLs now consistent `closer.app`
2. **Encryption version drift**`acceptInviteCallable.ts` wrote `encryptionVersion: 2` for new couples, but Android's `CoupleEncryptionManager.kt` had logic for v0 and v1 with `STRICT_ENCRYPTION_VERSION`. iOS port audit flagged: iOS couples get `v0` (plaintext), Android rejects writes from v0 couples.
- **Status: FIXED** (commit `73910bd`) — Single source of truth at `app/src/main/java/app/closer/crypto/EncryptionVersion.kt`. v0=PLAINTEXT (iOS MVP), v1=MIGRATING (legacy), v2=STRICT (Android new couples).
3. **`isImmutable(fields)` helper in firestore.rules** — fields arg must be a list, but if a caller passed a non-list it would silently fail-open rather than reject.
- **Status: FIXED** (commit `c33058d`) — Added `fields is list` guard. Both existing callers already pass correct lists.
### 🟡 Medium risks
4. **`createInviteCallable` Cloud Function missing** — iOS `PairingViews.swift` was writing invites directly to Firestore. 6-char codes are enumerable; client-side invites are an attack surface.
- **Status: FIXED** (commit `e32d486`) — New `createInviteCallable` with race-safe unique code generation, 24h TTL, rolling 1h rate limit (5/caller). iOS migrated to use it. Firestore rules tightened so `invites` collection is server-only.
5. **`gentle_reminders` rate limit was client-side** — A malicious user could loop the `sendGentleReminder` callable. Server-side throttle added.
- **Status: FIXED** (commit `3a61644`) — 5 reminders per user per rolling hour, Firestore transaction on `rate_limits/{uid}_gentle_reminder`. Throws `resource-exhausted` on overflow. Client-side limiter retained for UX.
4. **Server/cloud function naming inconsistency** — audit found `onCoupleLeave` (functions) vs `onLeaveCouple` (Android caller). One of them is wrong. Won't surface in tests, but a debugging nightmare when FCM stops firing.
- **Status: FIXED** — `onCoupleLeave` is canonical (exported from `functions/src/index.ts` and implemented in `functions/src/couples/onCoupleLeave.ts`). No Android/iOS caller references `onLeaveCouple`; mobile clients correctly invoke the `leaveCoupleCallable` HTTPS callable, which clears `coupleId` and lets the `onCoupleLeave` Firestore trigger notify the remaining partner.
5. **iOS E2EE is skipped for MVP** — new iOS couples are created with `encryptionVersion=0` (plaintext). If an Android user later joins that couple, the Android client rejects their writes. iOS port must explicitly mark these couples as "iOS-only" or ship E2EE parity soon.
6. **No retention/cleanup job** — couples who delete accounts leave orphan data (sessions, capsules, dates). Cloud Function `onUserDelete` handles the user but there's no scheduled cleanup for empty couples.
7. **`gentle_reminders` rate limit** is client-side (`AuthRateLimiter.kt`) — a malicious user can call `sendGentleReminder` callable in a loop; need server-side throttle.
8. **`createInviteCallable` Cloud Function doesn't exist** — iOS `PairingViews.swift` writes invites directly to Firestore (TODO added in Pass B). 6-char codes are enumerable; client-side invites are an attack surface.
### 🟢 Looks good
- Firestore rules scoped by couple ID — solid (after `isImmutable` fix)
- RevenueCat entitlement verified server-side via webhook + Cloud Function — correct pattern
- App Check + Play Integrity in place — correct
- TODO/FIXME count is only 4, all in one file — clean
- 17 Cloud Functions cover full lifecycle
- Tink key handling — properly isolated in `crypto/` package
- Android compiles clean after Neo's encryption refactor
---
## Recommended Next Steps (priority order)
1. ~~**Fix `ExternalLinks.kt` URLs**~~
2. ~~**Add "5-minute check-in" Home card**~~ ⏸ not yet shipped
3. **Surface streak on Home** — code exists, just needs UI
4. ~~**Resolve encryption version drift**~~
5. **Partner with one couples therapist** — single biggest credibility unlock
6. **Add outcomes capture** — 30/60/90 day check-in flow, drives both retention and marketing copy
---
## Session Status (as of 2026-06-20)
### Commits pushed this session
- `73910bd` — Encryption version drift (Neo)
- `cd28f25` — Pass A iOS compile blockers (Neo)
- `857d48e` — Pass B iOS warnings (Neo)
- `a0e0771` — Pass C Package.swift path fix (Neo)
- `d83e557` — README rewrite
- `c33058d``isImmutable` firestore rules fix (Neo)
- `3a61644` — Server-side gentle_reminder throttle (Neo)
- `e32d486``createInviteCallable` Cloud Function + invite rules (Neo)
- `e373496` — Empty commit (Risk #2 was a false positive in the audit)
### Remaining real risks
- `GoogleService-Info.plist` for iOS — must come from user
- iOS E2EE parity — follow-up batch
- Streak UI on Home — UI work only
- "5-minute check-in" Home card framing
- Outcomes capture (30/60/90 day check-in)
---
*Sources: paired.com home + premium + experts pages, Closer repo (`PROJECT.md`, `app/src/main/java/app/closer/`, `functions/src/`, `firestore.rules`, `iphone/ARCHITECTURE_AUDIT.md`).*