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** | Answers **and chat messages** are 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. Answers, messages, and history โ€” server never sees plaintext. Recover with your phrase *or* your partner. - ๐ŸŒ™ **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 ```text โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ 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/premium` โ†’ `CouplePremiumChecker` 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 ```text . โ”œโ”€โ”€ 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](https://github.com/yonaskolb/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 ```bash # Android cp local.properties.example local.properties # iOS cp iphone/Closer/GoogleService-Info.plist.example iphone/Closer/GoogleService-Info.plist ``` ```properties sdk.dir=/path/to/Android/Sdk RC_API_KEY_ANDROID=your_revenuecat_android_key RC_API_KEY_IOS=your_revenuecat_ios_key ``` ### Android ```bash ./gradlew :app:assembleDebug ./gradlew :app:installDebug ``` ```bash ./gradlew :app:compileDebugKotlin # fast verification ./gradlew :app:testDebugUnitTest # 205 unit tests ``` ### iOS ```bash cd iphone xcodegen generate xed Closer.xcodeproj ``` ```bash xcodebuild -project iphone/Closer.xcodeproj \ -scheme Closer \ -destination 'platform=iOS Simulator,name=iPhone 15' \ build ``` ### Firebase Functions ```bash cd functions npm install npm run build npm run serve ``` ```bash npm test # 24 functions tests ``` ### Optional server ```bash cd server npm install npm run dev ``` --- ## Security & privacy - ๐Ÿ” **E2EE content.** Tink AEAD with couple-owned keys. Answers, **chat messages** (text *and* images), locked capsules, and conversation history are encrypted on-device โ€” the server only ever sees ciphertext. Messages live under `couples/{coupleId}/conversations/โ€ฆ`; images upload to Storage as opaque encrypted bytes, and even the inbox preview line is encrypted. - ๐Ÿ’พ **Encrypted conversation backup.** Chat and thread history is backed up as couple-key ciphertext โ€” cheap incremental appends plus periodic full snapshots โ€” so history can return to a new device. The backup is never readable server-side. - ๐Ÿง‚ **Key wrapping.** Argon2id KDF over the recovery phrase; keys wrapped client-side. - ๐Ÿชช **Recovery phrase.** Server-blind; wiped from the inviter on acceptance. One of **two** recovery paths. - ๐Ÿค **Partner-assisted restore.** Lost or wiped your device? Your partner can restore your full history with **no recovery phrase**: your new device publishes a fresh public key, you read a 6-digit code aloud on a separate channel, and they wrap the couple key to that key (ECIES `keybox:v1:`). The server only relays the sealed blob โ€” never the key itself. Your own devices get a *"was this you?"* security alert whenever a restore is requested or completes. - ๐Ÿšง **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`](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`](docs/Engineering_Reference_Manual.md) | Architecture, security model, data model, known landmines | | [`docs/release/`](docs/release/) | Release prep + store assets | | [`docs/qa/`](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.