Closer/review2.md

8.5 KiB

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 questions2 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 driftacceptInviteCallable.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

  1. 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.
  2. 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.
  3. 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: FIXEDonCoupleLeave 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.
  4. 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.

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

  6. gentle_reminders rate limit is client-side (AuthRateLimiter.kt) — a malicious user can call sendGentleReminder callable in a loop; need server-side throttle.

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

  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
  • c33058disImmutable firestore rules fix (Neo)
  • 3a61644 — Server-side gentle_reminder throttle (Neo)
  • e32d486createInviteCallable 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).