# System Patterns — iFarted ## Target Architecture (user decisions locked: Bun backend + Expo Push API) ``` [Sender phone — iOS/Android, Expo/RN] │ POST /v1/farts { recipientId, lat?, lng? } (Bearer apiKey) ▼ [Bun relay server] (TypeScript, Hono or plain fetch handlers, bun:sqlite) │ auth + rate limit → insert message → call Expo Push API: │ POST https://exp.host/--/api/v2/push/send │ { to: , title: senderName, │ body: "I farted.", sound: "fart.caf|mp3", data: {...} } ▼ [Expo Push Service] → [APNs / FCM] ▼ [Recipient phone] → OS notification → tap → in-app fart view (map pin if coords) ``` Guiding rules: - **No P2P push; no raw APNs/FCM on the server.** Bun talks only to the Expo Push API; Expo's service handles APNs/FCM. (Android still needs a Firebase project for the *client's* FCM token.) - Thin client, thin backend: client sends a tiny intent; backend validates, persists a stub, and relays; there is no inbox to serve. - Server must run identically under Bun or Node (keep bun-specific APIs optional so Node is a trivial fallback). ## REST API (draft) | Endpoint | Purpose | |---|---| | `POST /v1/register` | Create user. Body: `{ username?, phoneE164?, inviteCode? }` → returns `{ userId, apiKey }`. Username claim is unique/case-insensitive; inviteCode pre-links. | | `POST /v1/tokens` | Register/refresh device push token `{ expoPushToken, platform }` (Bearer). | | `POST /v1/farts` | Send. Body: `{ recipientId, lat?, lng? }`. Server: check relationship+block list → rate limit → persist → Expo Push. Returns `{ ok, messageId }`. | | `GET /v1/users/search?username=` | Public lookup by username (returns only id/username/displayName — never phone). | | `POST /v1/contacts` | Body: `{ phoneE164: string[] }` → returns which of *my contacts* are users who enabled phone discovery. Contact numbers hashed/normalized server-side; not stored raw. | | `POST /v1/invites` | Create invite code for my username; client renders as code + deep link (exp:// / https link). | | `POST /v1/block` | Body `{ userId }` → stop receiving/sending. | ## Mobile Client Patterns - **Navigation:** expo-router; flows = auth/onboarding, home(recipients), fart-detail(maps), settings. - **State:** React Context or Zustand. No Redux at MVP. - **Home data:** recipient list = contacts + people who sent you a fart (latest first, Yo-style), not a server inbox. - **Screens:** Onboarding (username/phone/invite) → Home (list + big send + location toggle + ad banner) → Send toast → Fart detail (+ map pin, one-tap **fart back**) → Settings (Remove Ads + Restore, sound toggle, phone-discovery toggle, account). - **Permissions:** notifications at first run with purpose text; location lazily via `expo-location` only when "attach location" tapped (iOS When-In-Use). - **Ads abstraction:** single gated `` routed through one `isAdFree` flag. - **Deep links:** invite links and notification taps both resolve into navigation (messageId or username pre-link). - **Push payload (data):** `{ type:"fart", messageId, senderId, senderName, lat?, lng?, sentAt }` — notification body is "I farted.", title is sender name, custom sound file. ## Data Model (SQLite) - `users`: id, username (unique ci), display_name?, phone_e164?, phone_discovery (bool), invite_code (unique), api_key_hash, created_at, updated_at. - `push_tokens`: id, user_id, expo_push_token (unique), platform, last_seen_at. - `relationships`: id, owner_id, peer_id, status (added/blocked/pending-invite), added_via (username|contacts|invite), created_at. - `messages`: id, sender_id, recipient_id, lat?, lng?, created_at — retained only for rate limiting/abuse/receipts; **never rendered as history**. - `invites`: code, creator_id, created_at, accepted_by_user_id?. ## Monetization Architecture - Product: non-consumable **`remove_ads`** in App Store Connect + Play Console. - Entitlement source of truth = store state (expo-iap or RevenueCat — library TBD; RevenueCat favored for cross-platform entitlement mgmt + restore). - Launch + purchase + restore resolve `isAdFree` → ad components unmount and stop loading. - AdMob: **non-personalized** ads initially → no ATT complexity. ## Security & Privacy (direct responses to the 2014 Yo hack) - Every endpoint requires Bearer `apiKey` (random 256-bit, hashed at rest). No unauthenticated PII access. - Username search returns only non-PII profile fields. Contacts matching uses **hash-normalized** numbers and only reveals matches to users who enabled discovery. - Server-side rate limits per sender (e.g., N farts/hour) + per-recipient cap + block list → stops fart-spam/spoofing. - Location: per-message opt-in, only to the chosen recipient; not logged in analytics. - Expo push tokens are the only "FCM/APNs" secret-ish material on the server; no server keys ship in the app. - No API keys/tokens in client source; issued per-install at register. - Invite deep links carry a random unguessable code, not phone numbers.