Anti-doomscrolling · Stop scrolling, start living.
Click is built as a people-first social utility: fewer infinite feeds, more real-world presence. We optimize for intentional connection—not passive consumption—so moments in the room matter more than minutes on the timeline.
Click — The seamless offline-to-online connection app.
This repository is the Kotlin Multiplatform mobile client: Compose Multiplatform UI with Android and iOS targets. Backend pieces (Postgres, Edge Functions, companion HTTP APIs) live in sibling services; the app integrates via Supabase and a configurable web base URL for QR and chat APIs.
Click is a digital handshake: it turns brief, in-person encounters into lasting connections using a multi-modal proximity mesh—not a single hardware sandbox. Phones agree they shared the same physical context through corroborating signals, then open a lightweight path to chat and memory—without treating the OS as the source of truth for “who was really there.”
New connections use a simultaneous blend of:
- Bluetooth Low Energy (BLE) advertising — local presence and session framing in the room.
- 18.5 kHz ultrasonic audio chirps — inaudible to most listeners, detectable on the ultrasonic-capable microphone path for tight co-location.
- Progressive high-accuracy GPS — refined over the handshake window so devices can assert they occupy the same space, not merely the same building.
Together, these factors mathematically verify that two (or more) phones were in the exact same room, reducing reliance on legacy OS-imposed proximity restrictions.
When three or more people connect at the same time (Multi-Tap), the app coordinates a verified group clique: the backend performs O(1) validation so the cohort forms a fully connected subgraph (everyone mutually verified with everyone else). On success, participants drop into a private group chat—a real clique, not a loose list of pairwise guesses. The current chat threat model is documented in composeApp/src/commonMain/kotlin/compose/project/click/click/crypto/CRYPTO_README.md; product copy must not overstate it.
Memory Capsules pair objective signals with subjective story:
- Hardware-backed context —
BarometricHeightMonitorcaptures elevation trends;AmbientNoiseMonitorreads exact decibel levels from the same stream used for ultrasonic detection, grounding “how loud / how high” the moment felt in data. - Polymorphic subjective tagging — users layer meaning (“met at open mic,” “after class”) in a flexible schema; tagged payloads participate in data fan-out so capsules stay rich without collapsing into one rigid field.
There is no “hard lock” or forced expiry that deletes relationships by timer alone. Instead, Click runs a 48-Hour Auto-Archive Sweep: connections that are not acted on within 48 hours move to archive, keeping the active surface calm and intentional. Archived items remain recoverable according to product policy—this is about UI hygiene and focus, not punishing users with arbitrary countdowns.
| Location | Purpose |
|---|---|
composeApp/src/commonMain/kotlin/ |
Shared UI (Compose), ViewModels, repositories, Supabase client, chat, connections, maps, most business logic |
composeApp/src/androidMain/kotlin/ |
Android actual implementations: FCM service, TokenStorage, crypto, location |
composeApp/src/iosMain/kotlin/ |
iOS actual implementations: TokenStorage, push helpers, permission requesters, stubs/bridges to Swift where needed |
iosApp/iosApp/ |
Xcode app: Swift for UserNotifications, Google Sign-In, app lifecycle and Kotlin entry |
composeApp/build.gradle.kts |
Multiplatform targets, dependencies (e.g. Supabase KMP, Ktor) |
Guideline: add new features in commonMain first; use expect/actual (or small platform facades) only when you must touch BLE / audio / GPS pipelines, Keychain, EncryptedSharedPreferences, or platform location.
- Tri-Factor proximity mesh + QR — In-person discovery and connection via BLE + ultrasonic + progressive GPS; QR remains a fallback path that uses HTTP against the configured web base URL (see
QRModels.kt/QrCodeView.kt). Connection metadata can recordconnectionMethod(e.g."qr","tri_factor"). - Multi-Tap verified cliques — Simultaneous 3+ person handshakes validated server-side; client-side encrypted group chat on success.
- Real-time chat — Supabase Realtime channels,
SupabaseChatRepository/ChatViewModel, typing and presence-oriented state, push hooks for background delivery (Edge Function + FCM/APNs). - Presence — Realtime subscriptions in home/chat/map-related ViewModels for online status and activity signals. - Maps — Map screens and Realtime channels (e.g. connection discovery) backed by Supabase-backed repositories.
- Memory Capsules (opt-in) —
rememberAmbientNoiseMonitor,rememberBarometricHeightMonitor, and subjective tagging flows when the user opts in (settings + connection sheets). - Prior Connections — Optional, skippable onboarding step. Contacts are hashed on-device (SHA-256 of E.164 / email) and matched via
POST /api/contacts/discover. Self-reported edges usesource=priorand never mintconnection_encountersor handshake vanity metrics.
- Kotlin Multiplatform + Compose Multiplatform
- Android: Gradle, Jetpack lifecycle/viewmodel where used, FCM (
google-services.jsonincomposeApp/) - iOS: Xcode project, APNs (via backend)
- Supabase KMP: Auth (with
SettingsSessionManager+ appTokenStoragesync inSupabaseConfig), Postgrest, Realtime - Ktor client for companion HTTP APIs (QR, waitlist)
Edit composeApp/src/commonMain/kotlin/compose/project/click/click/data/SupabaseConfig.kt:
SUPABASE_URL,SUPABASE_ANON_KEY- Auth redirect scheme/host (
click/login) must match Supabase Auth and the iOS/Android URL handlers.
SupabaseConfig.startSessionSync(tokenStorage) keeps the SDK session aligned with TokenStorage (Keychain on iOS / encrypted prefs on Android). iOS migrates the previous plaintext session once into Keychain and purges it after the secure write succeeds. Cold boot imports TokenStorage when the SDK session is empty or expired. Access-token wall-clock headroom is not enough to skip GoTrue refresh: TestFlight updates and dual-store drift otherwise keep a JWT that click-web rejects as 401 Unauthorized until the user signs out. Boot, 45-minute ticker, foreground resume, and HTTP 401/403 retries call refreshSession(forceRefresh = true) (which always hits /token unless another refresh is already in-flight), then drop/reconnect the Realtime socket so hub subscribe and sends use the new bearer. connect() is a no-op on an open socket — rebindRealtimeSocket() disconnects first.
composeApp/src/commonMain/kotlin/QRModels.kt defines CLICK_WEB_BASE_URL. Point it at your deployed or local companion app that exposes /api/qr, etc. (typically the click-web project next to this repo).
Place google-services.json in composeApp/ (package compose.project.click.click). The Supabase Edge Function uses a Firebase service account for server-side FCM.
Enable Push Notifications, Background Modes (Remote notifications), and associated entitlements.
From this directory:
./gradlew :composeApp:assembleDebugUse Android Studio’s composeApp run configuration, or install the debug APK from composeApp/build/outputs/.
Create local.properties at the repo root with sdk.dir and MAPS_API_KEY (see AGENTS.md). If the file is missing, Gradle uses checked-in local.defaults.properties so the project still configures (required for Xcode’s Kotlin framework step).
- Open
iosApp/iosApp.xcodeprojin Xcode. - Resolve Swift packages (GoogleSignIn).
- Select the iosApp scheme, set signing team, build and run.
Xcode’s Compile Kotlin Framework phase runs ./gradlew :composeApp:embedAndSignAppleFrameworkForXcode. That Gradle configure step needs MAPS_API_KEY via local.properties or the checked-in local.defaults.properties fallback (Xcode Cloud writes local.properties in iosApp/ci_scripts/ci_pre_xcodebuild.sh).
Black-box flows live in .maestro/. Install the Maestro CLI (Java 17+) — do not vendor the Maestro source:
curl -fsSL "https://get.maestro.mobile.dev" | bash# Rebuild, uninstall any snapshot APK, install this tree, then smoke.
# assembleDebug alone is not enough — AVDs keep the last installed Click app.
bash scripts/maestro-smoke-android.shSmoke flows (no secrets): smoke/launch_login.yaml, smoke/login_signup_toggle.yaml.
Email/password journeys (dedicated test user that has finished onboarding and has an avatar):
maestro test .maestro/auth --include-tags auth \
-e TEST_EMAIL=you@example.com \
-e TEST_PASSWORD=secretauth/tabs.yaml also asserts onboarding-avatar is not visible and map chrome (map-screen) after nav-map.
Package id is compose.project.click.click on Android and iOS. Grant runtime permissions in flows via launchApp.permissions. Cloud uploads (optional) use .github/workflows/maestro-cloud.yml (workflow_dispatch, secrets MAESTRO_API_KEY + MAESTRO_PROJECT_ID).
SQL migrations and ordering: database/. Full operator checklist (Edge Function secrets, APNs, FCM): EXTERNAL_SETUP.md.
Optional: quick_start_chat.sh for guided prompts around Supabase config.
See docs/archive/PERFORMANCE.md (archived July 2026 notes) for hotspots, scale failure modes, and remediation (inbox RPC, RealtimeCoordinator, gated map prefetch).
Compile (Android + iOS):
./gradlew :composeApp:compileDebugKotlinAndroid :composeApp:compileKotlinIosSimulatorArm64| Area | Starting points |
|---|---|
| Auth & session | data/SupabaseConfig.kt, viewmodel/AuthViewModel.kt, data/repository/AuthRepository.kt |
| Chat | viewmodel/ChatViewModel.kt, data/repository/SupabaseChatRepository.kt |
| Connections / proximity / QR | viewmodel/ConnectionViewModel.kt, ui/screens/ConnectionsScreen.kt, ui/components/QrCodeView.kt |
| Maps | viewmodel/MapViewModel.kt |
| Push (Kotlin side) | notifications/ChatPushNotifier.kt, data/repository/PushTokenRepository.kt |
If your checkout includes click-web beside click/, run the Next.js app locally when developing QR, waitlist, or widget-vibe flows:
# Terminal 1 — click-web
cd click-web && npm run dev # http://localhost:3000
# Terminal 2 — mobile
# Set CLICK_WEB_BASE_URL in QRModels.kt / build config to http://localhost:3000 (simulator)
# or your machine LAN IP for physical devicesThe KMP app does not embed click-web—it calls it over HTTP with the user's Supabase JWT where required.
| Flow | Mobile entry | Web route |
|---|---|---|
| QR token issue / redeem | ApiClient, QrCodeView |
GET/POST /api/qr |
| Waitlist | Marketing links | POST /api/waitlist |
| Home widget vibe | ApiClient.getWidgetVibe() |
GET /api/insights/widget-vibe |
| Home activity recap | ApiClient.getActivityRecap() |
GET /api/me/recap?window=day|week |
Mobile handshakes create connections and connection_encounters rows. When the user has Include in business insights enabled (AppDataManager.locationPreferences.includeInInsightsEnabled), rows carry include_in_business_insights: true and feed anonymized venue analytics on click-web /insights/*. Availability intents power Vibe Radar hexbins.
Mobile does not host the B2B insights dashboard—that is web-only (click-web/lib/insights/README.md).
- Consumer dashboard parity:
click-web/lib/dashboard/README.md - Connection / QR / proximity:
click-web/lib/connections/README.md - Payload contracts (push, encrypted chat):
click-web/AI.md§2,click-web/lib/chat/README.md - Insights testing playbook:
click-web/lib/insights/README.md§ Real-world testing