Go to file
null 2c44cc6ff2 feat(crypto): couple-key rotation, phase 1 — rotate forward (Future.md security #3)
A couple-key compromise currently exposes everything, forever, because the key
never changes. This adds the rotation ceremony: a fresh AES-256-GCM key becomes
the keyset's primary while the old keys stay for reads. The keyset is a Tink
keyring and every enc:v1: blob carries its key-id internally, so all history
keeps decrypting with zero wire-format changes — none of the 25 isCiphertext
rule sites move. Phase 1 protects FUTURE content only (a stolen keyset still
contains the old key); forward secrecy for history is phase 2, which builds on
the keyGeneration plumbing laid here. The Security-screen copy says so plainly.

The ceremony (CoupleRepositoryImpl.rotateCoupleKey): read the couple fresh so
concurrent rotations collide at the rules instead of overwriting each other →
prepareRotation builds the rotated keyset and re-wraps it under the SAME phrase
(fail-closed with a typed error when this device lacks keyset or phrase; nothing
persisted anywhere) → ONE merge write lands the new wrap + a strictly-increasing
keyGeneration atomically, so the partner can never observe a bumped generation
pointing at the old wrap → only then commitRotation stores locally. A failed
server write leaves the device coherent on the old key; a crash after it
self-heals through the same adoption path as the partner.

Adoption (CoupleEncryptionManager.adoptRotationIfNeeded, hooked into Home's
healing block, synchronously before the screen settles — until the rotated
keyset is stored, new content renders locked): couple.keyGeneration ahead of the
local generation → unwrap the published wrap with the locally-stored phrase →
replace the keyset. Replaced only on success, never deleted on failure, so old
content survives anything. No phrase on this device → needsRecovery, and both
recovery flows already deliver the rotated keyset for free (phrase entry unwraps
the current wrap; partner-assist exports the current keyset). Same phrase both
sides is the entire distribution trick — no new ceremony, no partner action.

Server: onCoupleKeyRotated (couples/{id} update, pure isKeyGenerationIncrease
edge guard so streak/rhythm/re-wrap updates never fire it, and a rules-forbidden
downgrade or redelivered stale event never alerts) sends both members the 🔑
security alert through the house pipeline, bypassing quiet hours like the
restore self-alerts. The push is also functional: the partner's closed app can't
read new-key content until it next loads Home — the tap takes them there.

Rules: isUpdatingRecoveryWrap admits keyGeneration, strictly increasing
(monotonic like encryptionVersion), untouched for plain phrase re-wraps.

Tests (real Tink, mocks stop at storage): history readable after rotation + NEW
writes unreadable by the old keyset — mutation-checked by dropping setPrimary,
which kills exactly that test (a rotation that forgets setPrimary passes
everything else while protecting nothing) — same-phrase unwrap reads both eras
(the partner's whole adoption, proven), prepare persists nothing until commit,
fail-closed without phrase/keyset, adoption state machine incl. corrupt-wrap.
Android suite green, assembleDebug clean, functions 105/105, tsc clean.

Deploy (scoped): firebase deploy --only firestore:rules and
--only functions:onCoupleKeyRotated. Live verify follows deploys.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-15 02:49:50 -05:00
.github fix(crash): LocalDate.ofInstant crashes on API<34 + add Lint/R8 CI gates 2026-07-11 18:41:04 -05:00
.vscode feat(db): add 2 new v2 question categories (difficult_conversations, home_life) — 20 total, 5,500 questions 2026-06-15 21:38:22 -05:00
app feat(crypto): couple-key rotation, phase 1 — rotate forward (Future.md security #3) 2026-07-15 02:49:50 -05:00
docs fix(auth): stop isSignInStub from looping every new signup into profile setup 2026-07-15 01:25:25 -05:00
firestore-tests feat(functions): privacy-safe outcome-stat aggregation 2026-07-06 21:06:57 -05:00
functions feat(crypto): couple-key rotation, phase 1 — rotate forward (Future.md security #3) 2026-07-15 02:49:50 -05:00
gradle feat(avatar): frame a picked photo, and fall back to initials not a heart 2026-07-14 23:19:37 -05:00
iphone docs: reconcile stale docs with project reality (Batch 1 doc hygiene) 2026-07-11 18:29:08 -05:00
qa fix(qa): qa_push.js picks newest FCM token, not docs[0] 2026-07-10 10:06:07 -05:00
scripts test(qa): add nav-scan.sh — prove navigation dead ends statically 2026-07-14 21:46:24 -05:00
seed fix(content): give packs a pack_id, and stop icon_name leaking into the UI 2026-07-14 19:10:37 -05:00
.eslintrc.js feat: Expo project bootstrap (v1.0.1) + untrack private docs 2026-05-13 04:21:43 -05:00
.firebaserc feat: code push -- notifications, cloud functions, iOS updates 2026-06-22 08:53:23 -05:00
.gitignore feat(seed): rebuild asset db from JSON; verify Room loads it on-device 2026-07-14 18:49:20 -05:00
.gitleaksignore feat: security hardening, CI, analytics, invite deep links, solo mode, weekly recap 2026-07-06 20:33:38 -05:00
.prettierrc feat: Expo project bootstrap (v1.0.1) + untrack private docs 2026-05-13 04:21:43 -05:00
Engineering_Reference_Manual_Plan.md docs(manual): batch 6 - fix broken iOS-Android sealed-answer bridge anchor (→ slug mismatch) 2026-07-11 16:25:36 -05:00
Future.md docs(future): reconcile the QA backlog with what actually shipped 2026-07-14 21:50:04 -05:00
HISTORY.md docs(history): fix markdown lint issues 2026-07-15 02:14:51 -05:00
IOS_E2EE_STATUS.md docs: reconcile stale docs with project reality (Batch 1 doc hygiene) 2026-07-11 18:29:08 -05:00
README.md chore(cleanup): remove the unused server/ Express backend (Tier 3) 2026-07-09 00:48:02 -05:00
UI_UPGRADE.md docs: reconcile stale docs with project reality (Batch 1 doc hygiene) 2026-07-11 18:29:08 -05:00
build.gradle.kts build: adopt Gradle version catalog (libs.versions.toml) 2026-07-06 22:05:10 -05:00
firebase.json feat: strict E2EE — encryption migration, Firestore rules enforcement, version 2 protocol (batch v0.2.11) 2026-06-19 20:53:52 -05:00
firestore.indexes.json feat(functions): hourly cleanup of expired restore requests (Future.md security #1) 2026-07-15 02:32:59 -05:00
firestore.rules feat(crypto): couple-key rotation, phase 1 — rotate forward (Future.md security #3) 2026-07-15 02:49:50 -05:00
gradle.properties feat: Firebase + RevenueCat stack, PLAN.md integration, 11 implementation batches (v0.1.0) 2026-06-15 18:43:43 -05:00
gradlew refactor: package rename from com.couplesconnect.app to app.closer, update build config and firebase setup 2026-06-16 20:03:58 -05:00
gradlew.bat refactor: package rename from com.couplesconnect.app to app.closer, update build config and firebase setup 2026-06-16 20:03:58 -05:00
local.properties.example feat: security hardening, CI, analytics, invite deep links, solo mode, weekly recap 2026-07-06 20:33:38 -05:00
seed_generator.py refactor: package rename from com.couplesconnect.app to app.closer, update build config and firebase setup 2026-06-16 20:03:58 -05:00
settings.gradle.kts security: kimi-k2.7 review fixes — Ed25519 crypto API, Firestore rules try/catch removal, atomic idempotency, RevenueCat 8.20.0, rate limiter fix, remove plaintext fallback, tighten push wording 2026-06-16 22:42:53 -05:00
storage.rules feat(backup): add Firestore rules (backup manifest/chunks, restore_requests with isPublicKey helper) and Storage rules (users/{uid}/backups/) 2026-06-30 20:43:26 -05:00

README.md

Closer Couples feature graphic

Closer Couples

A private space for two.
Daily questions, mutual reveals, shared games, and calm rituals for couples.

Screenshots · What makes it different · Status · Run locally · QA

Android reference app · iOS scaffold in progress · Firebase backend · RevenueCat billing · Private repo

Android iOS Firebase RevenueCat License


Closer Couples is a native relationship app for couples who want a quieter way to check in with each other. Each partner answers privately, reveals intentionally, and keeps a record of the conversations that matter.

It is not a social network, therapy replacement, or productivity tracker. There are no public feeds, likes, followers, or infinite-scroll loops. The product loop is small on purpose:

answer honestly -> reveal together -> keep the conversation going

Screenshots

Fresh Android captures from the current emulator build.

Ready · dark Ready · light Your turn · dark Reveal · dark Reveal · light
Home screen with the daily question ready to answer in dark mode Home screen with the daily question ready to answer in light mode Home screen after the partner answered and it is your turn in dark mode Home screen with the daily question reveal ready in dark mode Home screen with the daily question reveal ready in light mode
Play This or That Today Challenge
Play hub in dark mode This or That in dark mode Daily question in dark mode Connection challenge in dark mode

Visual system

Closer Couples uses paired light and dark illustration assets so the product can stay warm without fighting the selected theme. The home daily-question card is intentionally people-forward: ready-to-answer uses the partner-prompt couple artwork, reveal-ready uses the card reveal artwork, and quieter waiting states can fall back to the answer-card ritual art.

What makes it different

Private first
Partners answer independently before seeing each other's response.
Mutual reveal
The app is built around intentional sharing, not performative posting.
Couple-owned trust
Android encrypts answers, chat, history, and media on-device with couple-owned keys.
One subscription per couple
Premium unlocks for both partners through server-verified entitlements.
Curated prompts
Question packs are written and reviewed, not generated at answer time.
Calm by design
No feeds, likes, followers, public profiles, or pressure mechanics.
Theme-aware art
Light and dark illustrations are paired for the same product moments.
Human warmth
The home ritual leads with people and recognizable Closer Couples card artwork.
Private rituals
The visual language reinforces answering alone, revealing together, and continuing gently.

Product surface

Area Free Premium
Daily question Included Included
Private answers + mutual reveal Included Included
Curated question packs Free + mixed access Full access
Spin the Wheel Included Included
This or That / How Well Do You Know Me Included Included
Recent answer history Included Included
Memory Lane capsules Limited Full access
Desire Sync Limited Full access
Connection Challenges Free + mixed access Full access
Date ideas, matches, and bucket list Included Premium ideas gated
Push reminders + quiet hours Included Included
Account deletion Included Included

Data export is not currently offered. The in-app privacy copy intentionally says so until a real export flow exists.

Platform status

Platform Status Notes
Android Reference implementation Kotlin, Jetpack Compose, Material 3, Hilt, Room, DataStore, Firebase, Tink
iOS Scaffold in progress SwiftUI screen parity exists; pairing is blocked until E2EE interop is complete
Backend Shared source of truth Firebase Auth, Firestore, Cloud Functions, FCM, App Check
Billing Server verified RevenueCat webhook writes Firestore entitlements observed by the app

Android is the product source of truth today. iOS has the app shell, Firebase/RevenueCat integration, and SwiftUI screens, but end-to-end pairing waits on CryptoKit/Tink interoperability.

Architecture

Android (Kotlin/Compose)          iOS (SwiftUI)
Hilt · Room · DataStore           MVVM · AppState · SPM
Tink AEAD · Argon2id              CryptoKit interop in progress
          \                       /
           \                     /
            v                   v
             Firebase + RevenueCat
             Auth · Firestore · Functions · FCM
             App Check · server-verified billing

Core rules:

  • Couple-scoped Firestore data. Users only read/write their own couple surface.
  • Server-mediated pairing. Invite lifecycle runs through Cloud Functions.
  • Local-first question content. Prompts ship with the app; assignment/sync uses Firebase.
  • Server-verified premium. Clients observe entitlements; they do not self-grant access.
  • No anonymous auth. Accounts use email/password or Google sign-in.

Security and privacy

  • Android encrypts answer content, chat messages, media, capsules, and backed-up conversation history on-device before sync.
  • The server stores ciphertext for private content and never receives plaintext answers.
  • Recovery phrase wrapping uses Argon2id-derived keys.
  • Partner-assisted restore lets a trusted partner help restore the couple key to a new device.
  • Quiet hours are enforced server-side before push delivery.
  • Account deletion exists; data export does not yet.

The canonical technical reference is docs/Engineering_Reference_Manual.md.

Local development

Prerequisites
  • Android Studio, Android SDK, and JDK 17
  • Node 20 for Firebase Functions tooling
  • Firebase project with Auth, Firestore, Cloud Messaging, Crashlytics, Analytics, and App Check
  • app/google-services.json
  • RevenueCat project and Android API key

For iOS work:

  • macOS, Xcode 16, XcodeGen, and iOS 17+
  • iphone/Closer/GoogleService-Info.plist
  • RevenueCat iOS API key
Android
cp local.properties.example local.properties
sdk.dir=/path/to/Android/Sdk
RC_API_KEY_ANDROID=your_revenuecat_android_key
RC_API_KEY_IOS=your_revenuecat_ios_key
./gradlew :app:assembleDebug
./gradlew :app:installDebug
./gradlew :app:testDebugUnitTest
iOS
cd iphone
xcodegen generate
xed Closer.xcodeproj
xcodebuild -project iphone/Closer.xcodeproj \
  -scheme Closer \
  -destination 'platform=iOS Simulator,name=iPhone 15' \
  build
Firebase Functions
cd functions
npm install
npm run build
npm test
npm run serve

QA and release

The repo keeps repeatable QA checks close to the code:

Check Purpose
scripts/theme-scan.sh Finds hardcoded theme/color regressions
scripts/wiring-scan.sh Finds orphaned routes, dead actions, and silent wiring breaks
docs/qa/ Manual QA playbooks and private MVP checklist
docs/release/ Internal testing, store assets, and release prep

Live emulator fixtures and related credentials are intentionally local-only. Keep them in gitignored files under qa/, never in the tracked README or public docs.

Repository map

app/                    Android app
iphone/                 iOS app
functions/              Firebase Cloud Functions
seed/                   Question content and seed tooling
scripts/                Static QA scanners
qa/                     Local-only emulator fixture notes and smoke helpers
docs/                   Architecture, release, screenshots, and QA docs
firestore.rules         Firestore rules source of truth

Roadmap

In progress:

  • iOS E2EE interoperability with Android's Tink key material
  • More on-device/instrumented smoke coverage
  • Activity uiMode sync for in-app theme and dark artwork
  • Internal-testing release configuration, legal/support URLs, and RevenueCat offering validation

Out of scope for now:

  • AI-generated core questions
  • Group relationship spaces beyond dyadic couples
  • Wear OS / watchOS companions
  • Live video or voice sessions

Project docs

Doc Purpose
docs/Engineering_Reference_Manual.md Architecture, security model, data model, and known landmines
docs/brand/ Visual identity, asset system, generated art
docs/release/ Release prep and store assets
docs/qa/ Manual QA checklist
Future.md Backlog and roadmap
HISTORY.md Changelog and release notes
PROJECT.md Scope, feature matrix, architectural decisions

License

Private project. All rights reserved.