5 KiB
5 KiB
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: <recipient ExpoPushToken>, 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-locationonly when "attach location" tapped (iOS When-In-Use). - Ads abstraction: single gated
<AdBanner/>routed through oneisAdFreeflag. - 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_adsin 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.