Go to file
null afd1eab299 feat(users): wire pruneDeadTokens into onUserDelete 2026-06-30 23:45:36 -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(backup): lock-less phrase reveal, copy-to-clipboard, partner-restore empty-state, manual helper entry (R24-d) 2026-06-30 21:24:52 -05:00
docs docs: R25 coverage entries, instrumented test landmine warning, tokenized snapshot URL note 2026-06-30 23:33:54 -05:00
firestore-tests fix: prevent invite code enumeration via Cloud Function (batch v0.2.18) 2026-06-19 21:46:12 -05:00
functions feat(users): wire pruneDeadTokens into onUserDelete 2026-06-30 23:45:36 -05:00
gradle/wrapper feat: Firebase + RevenueCat stack, PLAN.md integration, 11 implementation batches (v0.1.0) 2026-06-15 18:43:43 -05:00
iphone feat(tools+docs): add androidTest deps for paired-CI vector harness; add SCHEMA_VERSION_DECISION + SPEC §19 pre-deploy checklist (iOS E2EE Batch 8) 2026-06-28 17:56:44 -05:00
qa chore: working tree changes — QA docs, app tweaks, Cloud Functions updates 2026-06-27 13:31:09 -05:00
scratchpad Revert "chore(scratchpad): rules arrays and positions scratch work" 2026-06-29 11:18:50 -05:00
scripts feat(notifications): QuietHoursManager + NotificationSettingsScreen rewrite, Cloud Functions (streakReminder, quietHours, reengagement, gameRetention), UserRepository E2EE wiring, SettingsDataStore, firestore rules, wiring-scan 2026-06-30 00:38:06 -05:00
seed docs(questions): v5 rewrite — Daily Fun Gate, restructured guides, trimmed schema 2026-06-30 23:33:48 -05:00
server feat: add onMessageWritten cloud function, notification settings screen, user repo cleanup 2026-06-20 18:25:05 -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 chore: add ClaudeQAPlan.md and ClaudeReport.md to gitignore 2026-06-30 19:06:25 -05:00
.prettierrc feat: Expo project bootstrap (v1.0.1) + untrack private docs 2026-05-13 04:21:43 -05:00
ClaudeBrandingReview.md docs(qa/brand): update ClaudeBrandingReview + ClaudeReport 2026-06-28 17:52:57 -05:00
ClaudeQACoverage.md docs: R25 coverage entries, instrumented test landmine warning, tokenized snapshot URL note 2026-06-30 23:33:54 -05:00
ClaudeQAPlan.md docs: R25 coverage entries, instrumented test landmine warning, tokenized snapshot URL note 2026-06-30 23:33:54 -05:00
ClaudeReport.md docs: R25 coverage entries, instrumented test landmine warning, tokenized snapshot URL note 2026-06-30 23:33:54 -05:00
ClaudeiOSPlan.md docs: update Future.md, ClaudeQAPlan.md, ClaudeReport.md, ClaudeiOSPlan.md, Engineering_Reference_Manual.md for R24 backup/restore 2026-06-30 20:43:34 -05:00
Engineering_Reference_Manual_Plan.md docs(plan): Mark Engineering Reference Manual plan complete 2026-06-28 11:15:30 -05:00
Future.md docs: update Future.md, ClaudeQAPlan.md, ClaudeReport.md, ClaudeiOSPlan.md, Engineering_Reference_Manual.md for R24 backup/restore 2026-06-30 20:43:34 -05:00
IOS_E2EE_STATUS.md docs: add IOS_E2EE_STATUS.md handoff memo (iOS E2EE phase code-complete pending Mac/CI) 2026-06-28 18:27:01 -05:00
README.md docs(readme): revamped screenshot grid (dark mode), updated tagline and badges 2026-06-29 11:21:50 -05:00
build.gradle.kts feat: Firebase + RevenueCat stack, PLAN.md integration, 11 implementation batches (v0.1.0) 2026-06-15 18:43:43 -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(app-check): stable debug token via BuildConfig; feat(firestore): indexes for questions + bucket_list 2026-06-23 12:17:17 -05:00
firestore.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
gitleaks-current.json feat: update PrivacyScreen, add Firestore test scripts, gitleaks audit artifacts 2026-06-19 03:45:53 -05:00
gitleaks-history.json feat: update PrivacyScreen, add Firestore test scripts, gitleaks audit artifacts 2026-06-19 03:45:53 -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: Firebase + RevenueCat stack, PLAN.md integration, 11 implementation batches (v0.1.0) 2026-06-15 18:43:43 -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 feature graphic

Closer

A private space for two.
Private daily questions, intentional reveals, shared games, and calm rituals for couples.

Android iOS Backend Min Android Min iOS Kotlin Swift License


Private daily questions for couples — end-to-end encrypted, never read, never sold. You and your paired partner hold the only key.

A native couples-relationship app that turns check-ins into small, intentional rituals: one daily question, curated conversation packs, private answers, mutual reveal, gentle reminders, shared games, and date planning — with real E2EE and calmer UX.

Not a social network. Not therapy. Not a productivity tracker. No public feeds, no likes, no followers, no infinite scroll.

The core loop is simple: answer honestly → choose what to reveal → keep a record of the conversations that mattered.

TL;DR

What Why it matters
🔐 Real E2EE Answer content is encrypted on-device. Server only sees ciphertext. Couple-owned keys via Tink (Android).
💑 One subscription per couple No double-billing partners. Premium unlocks for both — server-verified.
🚫 No engagement traps No infinite scroll. No likes. No follower counts. One daily question is the loop.
🌗 Decoupled theme + art In-app light/dark controls art; system theme isn't required to match.
📱 Native on both platforms Kotlin/Compose on Android, SwiftUI on iOS — same Firebase backend, same data model.
🧪 QA you can run scripts/theme-scan.sh (Pass C) and scripts/wiring-scan.sh (Pass N) catch the silent-dead-feature and theme-hardcoding classes before merge.

Screenshots

Fresh Android dark-mode captures from the current emulator build.

Home Play This or That Today Challenge
Home dashboard in dark mode Play hub in dark mode This or That in dark mode Daily question in dark mode Connection challenge in dark mode

Why Closer exists

Subscription apps for couples have a trust problem — confusing trial wording, hard-to-cancel flows, partners getting double-billed. Couples products have a different trust problem: partners are asked to be vulnerable in the same space where everything else (social, productivity, dating) wants their engagement, their data, and their attention.

Closer treats both the same way: clear, straightforward, and built on honesty.

  • 🪞 Private first, reveal second. Each partner answers independently. Both decide what to share.
  • 🧠 Curated, not generated. 6,000+ hand-written prompts across 22 categories — no AI confabulation in the core loop.
  • 💸 One sub, not two. Subscription unlocks for both partners. Server-verified. No silent trial conversions.
  • 🔒 Encryption that earns the word. Tink AEAD with couple-owned keys. Recovery phrase. Server never sees plaintext.
  • 🌙 Quiet hours, server-side. Partner pushes respect the recipient's in-app window — not just foreground detection.

What Closer does

Feature Free Premium
Daily question (text / scale / multi / this-or-that)
6,000+ prompts · 22 question packs (free) + 🎟️ (premium tiers) (incl. premium-only packs)
Private answers + mutual-reveal flow
Spin the wheel — category-randomized questions
Recent answer history (last 30 days)
Full answer history (search, filter, export)
Saved spin-wheel sessions
Memory Lane (locked time capsules)
Desire Sync (preferences alignment exercise)
Select Connection Challenges (multi-day programs) (free) + 🎟️ (premium tiers)
Push reminders with quiet-hour support
Account deletion + data export

One subscription unlocks premium for both partners — couples/{coupleId}/entitlements is per-couple, not per-user.


Platform status

Platform Stack Status Notes
Android Kotlin · Jetpack Compose · Material 3 · Hilt · Room · DataStore 🟢 Reference implementation Feature-complete MVP, light/dark theme polished
iOS SwiftUI · MVVM · async/await · Firebase iOS SDK 🟡 Scaffold landed on dev Full screen parity; pairing from iOS is blocked until E2EE keys are wired (Android-only today)
Backend Firebase Auth · Firestore · Cloud Functions · FCM · App Check 🟢 Shared source of truth 17 callable/trigger/scheduled/webhook functions
Billing RevenueCat · Google Play Billing · StoreKit 🟢 Server-verified Webhook → Firestore entitlements → CouplePremiumChecker

📐 iOS scaffold has all 49 screens mapped to SwiftUI views, Firebase + RevenueCat integration, and full screen parity on the dev branch. CryptoKit-based E2EE parity (interop with Android's Tink key material) is the only blocker for end-to-end iOS pairing.


Architecture at a glance

 ┌──────────────────────────┐         ┌──────────────────────────┐
 │  Android (Kotlin/Compose)│         │   iOS (SwiftUI)          │
 │  • Hilt DI · Room · DSK  │         │  • MVVM · AppState · SPM │
 │  • Tink AEAD + Argon2id  │         │  • CryptoKit (follow-up) │
 └────────────┬─────────────┘         └────────────┬─────────────┘
              │                                    │
              └──────────────┬─────────────────────┘
                             │
                  ┌──────────▼──────────┐
                  │   Firebase          │
                  │   • Auth (email /   │
                  │     Google via      │
                  │     Credential Mgr) │
                  │   • Firestore       │
                  │     (couple-scoped) │
                  │   • Cloud Functions │
                  │   • FCM             │
                  │   • App Check       │
                  │     (Play Integrity)│
                  └──────────┬──────────┘
                             │
                  ┌──────────▼──────────┐
                  │  RevenueCat         │
                  │  → webhook          │
                  │  → Firestore        │
                  │    entitlements     │
                  └─────────────────────┘
  • Couple-scoped data. Firestore rules deny cross-couple reads/writes. Users only see their own + their partner's surface.
  • Server-mediated pairing. 6-character invite codes are enumerable; invite reads/writes are server-side only.
  • Server-verified billing. RevenueCat → Cloud Function webhook → Firestore users/{uid}/entitlements/premiumCouplePremiumChecker observes both partners' premium state.
  • Local-first questions. Prompts ship in the app so daily questions load instantly; only assignment and sync hit the network.

Tech stack

Android

Layer Stack
Language Kotlin 2.x
UI Jetpack Compose · Material 3 · Navigation Compose
Architecture Clean architecture — core/ · data/ · domain/ · ui/
State ViewModel · Kotlin Coroutines · Kotlin Flow
DI Hilt
Local data Room · DataStore Preferences · bundled SQLite seed
Crypto Google Tink AEAD + Argon2id (Bouncy Castle KDF)
Auth Firebase Auth · Credential Manager for Google Sign-In
Build min 26 · target 35 · compile 35 · Java 17 · KSP

iOS

Layer Stack
Language Swift 6.0
UI SwiftUI · NavigationStack · TabView
Architecture MVVM · AppState ObservableObject · EnvironmentObject
Concurrency async/await · Swift 6 strict concurrency
Dependency management Swift Package Manager · XcodeGen (project.yml)
Auth Firebase Auth · Google Sign-In SDK
Crypto Apple CryptoKit (E2EE parity — follow-up)
SDK iOS 17.0+

Backend (shared)

Layer Stack
Auth Firebase Authentication — email/password · Google (Android uses Credential Manager)
Database Cloud Firestore (couple-scoped rules)
Server logic Firebase Cloud Functions (TypeScript)
Push Firebase Cloud Messaging (FCM)
Security Firebase App Check · Play Integrity (Android) · DeviceCheck (iOS, planned)
Billing RevenueCat (server-verified entitlements)
Analytics Firebase Analytics · Crashlytics

🚫 No anonymous auth. There is no anonymous sign-in or account-linking flow in either platform. Accounts are email/password or Google.


Repository layout

.
├── app/                       # Native Android app (Kotlin)
│   └── src/main/java/app/closer
│       ├── core/              # Firebase, analytics, billing, nav, notifications, security
│       ├── data/              # Room, Firestore data sources, repositories, seed parsing
│       ├── domain/            # Models + repository contracts
│       └── ui/                # Compose screens + feature ViewModels
├── iphone/                    # Native iOS app (SwiftUI)
│   ├── ARCHITECTURE_AUDIT.md  # iOS port blueprint (49 screens, schema, models)
│   ├── project.yml            # XcodeGen project spec
│   ├── Package.swift          # SPM dependency manifest
│   └── Closer/
│       ├── Models/            # Firestore + domain codable types
│       ├── Core/              # Auth · Billing · Notifications
│       ├── Services/          # FirestoreService (callable wrappers)
│       ├── Theme/             # CloserTheme (colors, typography, spacing)
│       ├── Components/        # Shared SwiftUI components
│       ├── Navigation/        # Root ContentView + TabView routing
│       ├── Onboarding/        # Onboarding · login · signup · profile creation
│       ├── Pairing/           # Invite code · partner confirm · recovery
│       ├── Home/              # Home dashboard · streak · partner mirror
│       ├── Questions/         # Daily Q · answer reveal · history · packs
│       ├── Play/              # Play hub + games (ToT, HowWell, DesireSync, …)
│       ├── Wheel/             # Spin wheel
│       ├── Dates/             # Date swipe · matches · builder · bucket list
│       └── Settings/          # Settings · paywall · help · data export
├── functions/                 # Firebase Cloud Functions (TypeScript)
│   └── src/
│       ├── auth/              # Auth + invite lifecycle
│       ├── billing/           # RevenueCat webhook + entitlement sync
│       ├── couples/           # Pairing, leave, daily-question triggers
│       ├── questions/         # onAnswerWritten · onMessageWritten · threads
│       ├── games/             # onGameSessionUpdate · onGamePartFinished
│       ├── notifications/     # quiet-hours helper · reminders
│       └── server/            # Internal Express webhook service
├── scripts/                   # Automated QA / lint scanners
│   ├── theme-scan.sh          # Pass C: light/dark theme-hardcoding scanner
│   └── wiring-scan.sh         # Pass N: dead-feature / orphan-wiring scanner
├── server/                    # Optional Express webhook/health service
├── seed/                      # Question-pack JSON + local DB generation
├── docs/                      # QA notes · release prep · roadmap · screenshots
└── firestore.rules            # Firestore security rules (single source of truth)

🧪 scripts/theme-scan.sh and scripts/wiring-scan.sh are run before every QA pass. They statically catch the two costliest QA classes: hardcoded theme colors and silent dead features.


Getting started

Prerequisites

  • Android: Android Studio · Android SDK · JDK 17
  • iOS: Xcode 16 · macOS · XcodeGen (brew install xcodegen)
  • Firebase: Project with Auth · Firestore · Cloud Messaging · Crashlytics · Analytics · App Check
  • Android config: app/google-services.json
  • iOS config: iphone/Closer/GoogleService-Info.plist
  • Billing: RevenueCat project with Android + iOS API keys
  • Node 20 for Firebase Functions tooling

Local config

# Android
cp local.properties.example local.properties
# iOS
cp iphone/Closer/GoogleService-Info.plist.example iphone/Closer/GoogleService-Info.plist
sdk.dir=/path/to/Android/Sdk
RC_API_KEY_ANDROID=your_revenuecat_android_key
RC_API_KEY_IOS=your_revenuecat_ios_key

Android

./gradlew :app:assembleDebug
./gradlew :app:installDebug
./gradlew :app:compileDebugKotlin   # fast verification
./gradlew :app:testDebugUnitTest    # 205 unit tests

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 run serve
npm test    # 24 functions tests

Optional server

cd server
npm install
npm run dev

Security & privacy

  • 🔐 E2EE answer content. Tink AEAD with couple-owned keys. Server never sees plaintext answers or capsule content.
  • 🧂 Key wrapping. Argon2id KDF over the recovery phrase; keys wrapped client-side.
  • 🪪 Recovery phrase. Server-blind; wiped from the inviter on acceptance.
  • 🚧 Firestore rules. Couple-scoped; deny-by-default; field allowlists on users/{uid} updates; shape-restricted couple create.
  • 🛡️ App Check. Play Integrity (Android), DeviceCheck (iOS, planned) — blocks abusive backend access.
  • 🌙 Quiet hours, server-side. Suppression is enforced where the push is sent, not where it might be foregrounded. Client cannot bypass by being backgrounded.
  • 💸 Server-verified billing. Cloud Function writes users/{uid}/entitlements/premium from the RevenueCat webhook. Client cannot self-grant.

Full architecture reference: docs/Engineering_Reference_Manual.md — the canonical source of truth for the security model, data model, and Cloud Functions wiring.


Roadmap

In progress:

  • 🔐 iOS E2EE parity (CryptoKit interop with Android's Tink key material) — unblocks pairing from iOS.
  • 🧪 On-device / instrumented test coverage (Compose UI / Espresso smoke) — currently app/src/test only.
  • 🎨 Activity uiMode sync to in-app theme (C-DARKART-002) — the dark-variant -night PNGs only render in the right combination today; architectural fix in MainActivity.
  • 🛒 Real release config — version, legal/support URLs, RevenueCat offerings verified end-to-end on internal testing.

Out of scope (for now):

  • AI-assisted question suggestions
  • Native group/relationship types beyond dyadic couples
  • Wearable (Wear OS / watchOS) companions
  • Live video or voice sessions

See Future.md for the full backlog.


Project history & docs

Doc Purpose
docs/Engineering_Reference_Manual.md Architecture, security model, data model, known landmines
docs/release/ Release prep + store assets
docs/qa/ QA playbook + private-MVP checklist
Future.md Backlog + roadmap
HISTORY.md Changelog + release notes
PROJECT.md Scope, feature matrix, architectural decisions

License

Private project. All rights reserved.