2026-06-29 11:18:02 -05:00
< p align = "center" >
< img src = "docs/store/feature-graphic-1024x500.png" alt = "Closer feature graphic" width = "860" / >
< / p >
2026-06-20 22:20:31 -05:00
2026-06-29 11:18:02 -05:00
< h1 align = "center" > Closer< / h1 >
2026-06-20 22:20:31 -05:00
2026-06-29 11:18:02 -05:00
< p align = "center" >
< strong > A private space for two.< / strong > < br / >
Private daily questions, intentional reveals, shared games, and calm rituals for couples.
< / p >
2026-06-17 19:59:07 -05:00
2026-06-28 15:53:41 -05:00
< p align = "center" >
< img alt = "Android" src = "https://img.shields.io/badge/Android-Active%20development-3DDC84?style=for-the-badge&logo=android&logoColor=white" / >
< img alt = "iOS" src = "https://img.shields.io/badge/iOS-Scaffold%20landed-007AFF?style=for-the-badge&logo=ios&logoColor=white" / >
< img alt = "Backend" src = "https://img.shields.io/badge/Backend-Firebase-FFCA28?style=for-the-badge&logo=firebase&logoColor=black" / >
< img alt = "Min Android" src = "https://img.shields.io/badge/Android-26%2B-3DDC84?style=for-the-badge&logo=android&logoColor=white" / >
< img alt = "Min iOS" src = "https://img.shields.io/badge/iOS-17%2B-007AFF?style=for-the-badge&logo=ios&logoColor=white" / >
< img alt = "Kotlin" src = "https://img.shields.io/badge/Kotlin-2.x-7F52FF?style=for-the-badge&logo=kotlin&logoColor=white" / >
< img alt = "Swift" src = "https://img.shields.io/badge/Swift-6.0-F05138?style=for-the-badge&logo=swift&logoColor=white" / >
< img alt = "License" src = "https://img.shields.io/badge/license-Private-red?style=for-the-badge" / >
< / p >
2026-06-21 08:15:20 -05:00
2026-06-28 15:53:41 -05:00
---
2026-06-21 08:15:20 -05:00
2026-06-29 11:18:02 -05:00
> **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.*
2026-06-28 15:53:41 -05:00
## TL;DR
2026-06-21 08:15:20 -05:00
2026-06-28 15:53:41 -05:00
| What | Why it matters |
2026-06-21 08:15:20 -05:00
| --- | --- |
2026-06-28 15:53:41 -05:00
| 🔐 **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. |
2026-06-21 08:15:20 -05:00
---
2026-06-17 19:59:07 -05:00
## Screenshots
2026-06-29 11:18:02 -05:00
Fresh Android dark-mode captures from the current emulator build.
2026-06-17 19:59:07 -05:00
2026-06-29 11:18:02 -05:00
| Home | Play | This or That | Today | Challenge |
| :---: | :---: | :---: | :---: | :---: |
| < img src = "docs/screenshots/readme/home-dark.png" alt = "Home dashboard in dark mode" width = "160" / > | < img src = "docs/screenshots/readme/play-dark.png" alt = "Play hub in dark mode" width = "160" / > | < img src = "docs/screenshots/readme/this-or-that-dark.png" alt = "This or That in dark mode" width = "160" / > | < img src = "docs/screenshots/readme/today-dark.png" alt = "Daily question in dark mode" width = "160" / > | < img src = "docs/screenshots/readme/challenge-dark.png" alt = "Connection challenge in dark mode" width = "160" / > |
2026-06-28 12:55:24 -05:00
2026-06-20 22:20:31 -05:00
---
2026-06-28 15:53:41 -05:00
## Why Closer exists
2026-06-17 19:59:07 -05:00
2026-06-28 15:53:41 -05:00
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.
2026-06-17 19:59:07 -05:00
2026-06-28 15:53:41 -05:00
Closer treats both the same way: **clear, straightforward, and built on honesty.**
2026-06-20 22:20:31 -05:00
2026-06-28 15:53:41 -05:00
- 🪞 **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.
2026-06-20 22:20:31 -05:00
---
2026-06-28 15:53:41 -05:00
## What Closer does
2026-06-20 22:20:31 -05:00
2026-06-28 15:53:41 -05:00
| Feature | Free | Premium |
2026-06-20 22:20:31 -05:00
| --- | --- | --- |
2026-06-28 15:53:41 -05:00
| Daily question (text / scale / multi / this-or-that) | ✅ | ✅ |
| 6,000+ prompts · 22 question packs | ✅ | ✅ (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.
2026-06-20 22:20:31 -05:00
---
2026-06-28 15:53:41 -05:00
## Platform status
2026-06-21 08:15:20 -05:00
2026-06-28 15:53:41 -05:00
| 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` |
2026-06-21 08:15:20 -05:00
2026-06-28 15:53:41 -05:00
> 📐 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.
2026-06-20 22:20:31 -05:00
2026-06-28 15:53:41 -05:00
---
2026-06-21 08:15:20 -05:00
2026-06-28 15:53:41 -05:00
## Architecture at a glance
2026-06-21 08:15:20 -05:00
2026-06-28 15:53:41 -05:00
```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 │
└─────────────────────┘
```
2026-06-20 22:20:31 -05:00
2026-06-28 15:53:41 -05:00
- **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.
2026-06-20 22:20:31 -05:00
---
2026-06-16 20:16:47 -05:00
2026-06-28 15:53:41 -05:00
## Tech stack
2026-06-16 20:16:47 -05:00
2026-06-20 22:20:31 -05:00
### Android
2026-06-28 15:53:41 -05:00
2026-06-17 19:59:07 -05:00
| Layer | Stack |
| --- | --- |
2026-06-20 22:20:31 -05:00
| Language | Kotlin 2.x |
2026-06-28 15:53:41 -05:00
| UI | Jetpack Compose · Material 3 · Navigation Compose |
2026-06-20 22:20:31 -05:00
| Architecture | Clean architecture — `core/` · `data/` · `domain/` · `ui/` |
| State | ViewModel · Kotlin Coroutines · Kotlin Flow |
2026-06-28 15:53:41 -05:00
| 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 |
2026-06-20 22:20:31 -05:00
### iOS
2026-06-28 15:53:41 -05:00
2026-06-20 22:20:31 -05:00
| Layer | Stack |
| --- | --- |
2026-06-28 12:55:24 -05:00
| Language | Swift 6.0 |
2026-06-20 22:20:31 -05:00
| UI | SwiftUI · NavigationStack · TabView |
| Architecture | MVVM · `AppState` ObservableObject · `EnvironmentObject` |
2026-06-28 15:53:41 -05:00
| 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)
2026-06-20 22:20:31 -05:00
| Layer | Stack |
| --- | --- |
2026-06-28 15:53:41 -05:00
| Auth | Firebase Authentication — **email/password · Google** (Android uses Credential Manager) |
| Database | Cloud Firestore (couple-scoped rules) |
| Server logic | Firebase Cloud Functions (TypeScript) |
2026-06-20 22:20:31 -05:00
| Push | Firebase Cloud Messaging (FCM) |
2026-06-28 15:53:41 -05:00
| Security | Firebase App Check · Play Integrity (Android) · DeviceCheck (iOS, planned) |
2026-06-20 22:20:31 -05:00
| Billing | RevenueCat (server-verified entitlements) |
| Analytics | Firebase Analytics · Crashlytics |
2026-06-28 15:53:41 -05:00
> 🚫 **No anonymous auth.** There is no anonymous sign-in or account-linking flow in either platform. Accounts are email/password or Google.
2026-06-20 22:20:31 -05:00
---
2026-06-17 19:59:07 -05:00
2026-06-28 15:53:41 -05:00
## Repository layout
2026-06-17 19:59:07 -05:00
```text
.
2026-06-28 15:53:41 -05:00
├── app/ # Native Android app (Kotlin)
2026-06-17 19:59:07 -05:00
│ └── src/main/java/app/closer
2026-06-28 15:53:41 -05:00
│ ├── 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
2026-06-20 22:20:31 -05:00
│ └── Closer/
2026-06-28 15:53:41 -05:00
│ ├── 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)
2026-06-20 22:20:31 -05:00
│ └── src/
2026-06-28 15:53:41 -05:00
│ ├── 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)
2026-06-17 19:59:07 -05:00
```
2026-06-28 15:53:41 -05:00
> 🧪 `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.
2026-06-20 22:20:31 -05:00
---
2026-06-28 15:53:41 -05:00
## Getting started
2026-06-17 19:59:07 -05:00
### Prerequisites
2026-06-16 20:16:47 -05:00
2026-06-20 22:20:31 -05:00
- **Android:** Android Studio · Android SDK · JDK 17
- **iOS:** Xcode 16 · macOS · [XcodeGen ](https://github.com/yonaskolb/XcodeGen ) (`brew install xcodegen`)
2026-06-28 15:53:41 -05:00
- **Firebase:** Project with Auth · Firestore · Cloud Messaging · Crashlytics · Analytics · App Check
2026-06-20 22:20:31 -05:00
- **Android config:** `app/google-services.json`
- **iOS config:** `iphone/Closer/GoogleService-Info.plist`
2026-06-28 15:53:41 -05:00
- **Billing:** RevenueCat project with Android + iOS API keys
2026-06-20 22:20:31 -05:00
- **Node 20** for Firebase Functions tooling
2026-06-17 19:59:07 -05:00
2026-06-20 22:20:31 -05:00
### Local config
2026-06-17 19:59:07 -05:00
```bash
2026-06-20 22:20:31 -05:00
# Android
2026-06-17 19:59:07 -05:00
cp local.properties.example local.properties
2026-06-20 22:20:31 -05:00
# iOS
cp iphone/Closer/GoogleService-Info.plist.example iphone/Closer/GoogleService-Info.plist
2026-06-16 20:16:47 -05:00
```
2026-06-17 19:59:07 -05:00
```properties
sdk.dir=/path/to/Android/Sdk
2026-06-20 22:20:31 -05:00
RC_API_KEY_ANDROID=your_revenuecat_android_key
RC_API_KEY_IOS=your_revenuecat_ios_key
2026-06-16 20:16:47 -05:00
```
2026-06-17 19:59:07 -05:00
### Android
2026-06-16 20:16:47 -05:00
2026-06-17 19:59:07 -05:00
```bash
./gradlew :app:assembleDebug
./gradlew :app:installDebug
```
2026-06-16 20:16:47 -05:00
```bash
2026-06-28 15:53:41 -05:00
./gradlew :app:compileDebugKotlin # fast verification
./gradlew :app:testDebugUnitTest # 205 unit tests
2026-06-16 20:16:47 -05:00
```
2026-06-20 22:20:31 -05:00
### 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
```
2026-06-17 19:59:07 -05:00
### Firebase Functions
```bash
cd functions
npm install
npm run build
npm run serve
```
2026-06-28 15:53:41 -05:00
```bash
npm test # 24 functions tests
```
### Optional server
2026-06-17 19:59:07 -05:00
```bash
cd server
npm install
npm run dev
```
2026-06-20 22:20:31 -05:00
---
2026-06-28 15:53:41 -05:00
## Security & privacy
2026-06-17 19:59:07 -05:00
2026-06-28 15:53:41 -05:00
- 🔐 **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.
2026-06-17 19:59:07 -05:00
2026-06-28 15:53:41 -05:00
> 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.
2026-06-20 22:20:31 -05:00
---
2026-06-17 19:59:07 -05:00
2026-06-28 15:53:41 -05:00
## Roadmap
2026-06-17 19:59:07 -05:00
2026-06-28 15:53:41 -05:00
In progress:
2026-06-20 22:20:31 -05:00
2026-06-28 15:53:41 -05:00
- 🔐 **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.
2026-06-20 22:20:31 -05:00
2026-06-28 15:53:41 -05:00
Out of scope (for now):
2026-06-16 20:16:47 -05:00
2026-06-28 15:53:41 -05:00
- AI-assisted question suggestions
- Native group/relationship types beyond dyadic couples
- Wearable (Wear OS / watchOS) companions
- Live video or voice sessions
2026-06-16 20:16:47 -05:00
2026-06-28 15:53:41 -05:00
See `Future.md` for the full backlog.
2026-06-16 20:16:47 -05:00
2026-06-20 22:20:31 -05:00
---
2026-06-28 15:53:41 -05:00
## Project history & docs
2026-06-20 22:20:31 -05:00
2026-06-28 15:53:41 -05:00
| 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 |
2026-06-20 22:20:31 -05:00
---
2026-06-16 20:16:47 -05:00
## License
Private project. All rights reserved.