OpenDating Mobile is built as a layered application where each layer has strict boundaries.
Expo UI (@expo/ui)
│
Screens (app/)
│
Features (src/features/)
│
OpenDatingClient (src/lib/opendating/)
/ \
/ \
opendating-protocol NDK core
(crypto, envelopes, (relay, subscriptions,
gift wraps, types) events, signer)
\ /
\ /
Nostr Protocol
│
▼
OpenDating Relay v0.1
- Screens may import from Features, Components, Theme, and OpenDatingClient
- Features may import from OpenDatingClient, Location, Storage, Theme, Types
- OpenDatingClient may import from opendating-protocol, NDK core, Storage
- Screens must NOT construct Nostr events, gift wraps, or envelopes directly
- No layer may import from backend implementation code
Expo Router file-based routing. Each screen:
- Uses hooks for data and actions
- Composes UI components
- Handles navigation
- Does NOT contain protocol logic
Custom hooks that encapsulate domain logic:
use-discovery— introduction fetching, private decisions, location updatesuse-matches— match list, new match notificationuse-messaging— NIP-17 send/receive, subscription managementuse-safety— block/unblock, unmatch, reportuse-profile— profile CRUD, pause/resumeuse-auth— identity managementuse-bootstrap— app startup state machine
The facade that screens talk to. Domain-oriented API:
createProfile(),getProfile(),updateProfile()getCandidates(),like(),pass()sendMessage(),subscribeToMessages()createBlock(),unmatch(),report()getCapabilities(),ping()
Handles the current transport integration:
- Relay connection and reconnection
- NIP-42 AUTH
- Event publication and subscription
- Local event caching
Pure protocol + crypto:
createEnvelope(),buildGiftWrap()nip44Encrypt(),nip44Decrypt()generateKeypair(),signEvent()
Feature Hook
→ OpenDatingClient.sendRequest(type, payload)
→ createEnvelope(type, requestId, payload)
→ buildGiftWrap(rumor, senderKey, serviceKey)
→ NDK.publish(giftWrap)
→ Wait for response on kind 1059 subscription
→ Decrypt gift wrap
→ Match request_id
→ Return domain result
App Start
→ Load SecureStore identity
→ No identity? → Onboarding
→ Yes? → Initialize NDK
→ Connect relay
→ NIP-42 Auth
→ Fetch NIP-11 capabilities
→ Incompatible? → Error screen
→ Compatible? → Get profile
→ No profile? → Profile onboarding
→ Yes? → Main app
- Single NDK instance — one shared client, screens never independently instantiate NDK
- Request correlation — every request has a unique
request_id, responses matched via map - Service discovery — service pubkeys from NIP-11, never hardcoded
- Location privacy — raw GPS consumed in location module, only geohash_prefix emitted
- Local-first safety — blocks applied locally before network confirmation
- No backend imports — client works against deployed relay only