Namaste Ji by Ayushman Dash

Docs / frontend.md · mirrored from the repo

Namaste Ji — Consumer Frontend (the app plane)

Status: design — stack decided, nothing built yet. This doc resolves the last open architecture question (ARCHITECTURE.md §8: “frontend stack”) and designs the consumer app surface. It sits downstream of DISTRIBUTION.md (the app renders what distribution decides) and consumes the catalog spine. Publishing/store operations live in APP-STORE-OPS.md; the UX north star is the Feed Lab’s simulated consumer app (FEED-LAB.md §3 + mockup).

Decisions carry stable IDs (FE-*) — cite them. Grounding research (July 2026, web-sourced, partially adversarially verified): framework comparison, Play/App Store process, India auth/OTP economics, analytics/push at scale, competitor benchmarks. Time-sensitive facts (store deadlines, prices) are marked ⚠ re-verify in APP-STORE-OPS.md — policies move faster than docs.

1. What this is

The consumer mobile app: the daily-ritual greeting feed + WhatsApp share for the next-billion audience (35+, Tier-2/3, Android-dominant, hi/en/hi-Latn). One codebase for Android + iOS. Android is the production launch target; iOS is a first-class build (the founder tests on iPhone) published to the App Store when ready.

The app is deliberately thin: it renders precomputed playlists, collects five signal types, shares images, and stops. All intelligence (ranking, personalization, generation) is server-side (DS-8, P-15). The client’s only “smart” component is the in-session re-rank — and that is the same code the server runs (FE-1, FL-1).

2. Stack: React Native + Expo (FE-1)

React Native + Expo (current SDK, New Architecture, Hermes), managed workflow, EAS Build/Submit/Update. Chosen over Flutter, Kotlin Multiplatform, and dual-native. In order of decisiveness:

  1. The isomorphic-ranker invariant (FL-1) is only literal in RN. Hermes runs the actual @namaste-ji/feed npm package and @namaste-ji/shared Zod schemas on-device for the in-session re-rank. Flutter (Dart) or KMP (Kotlin) would force a second implementation of the most drift-sensitive logic in the product — silent score-parity bugs between server playlist and device re-rank.
  2. Indic text is platform-rendered. RN delegates text to the OS stack (Minikin/HarfBuzz + Noto on Android, Core Text on iOS) — hi/hi-Latn shape exactly as in WhatsApp. Flutter owns its text engine (SkParagraph); its historical Devanagari defects are fixed/web-scoped today, but framework-owned text bugs that wait on Flutter releases are an unforced risk for a Hindi-text-on-image product.
  3. Agent-operability. One language (TS) across Workers, console, and app: the densest ecosystem in coding-agent training data; cross-surface refactors land in one PR (P-1).
  4. Precedent at this exact audience. Sri Mandir (AppsForBharat — largest devotional app, same 35+ devotional audience) ships React Native. ShareChat/Moj/Josh are native Kotlin — with hundreds of Android engineers; their choice reflects team size, not a ceiling RN hits at our scale.
  5. The needed feed stack is RN’s best-in-class pairing: FlashList v2 (recycling list, snap paging) + expo-image (native cache, BlurHash/ThumbHash, AVIF/WebP, recyclingKey) is close to a reference implementation of DS-7/DS-8’s client. expo-background-task = the overnight prefetch primitive; react-native-share shareSingle = the direct WhatsApp intent; app-private cache dirs mean the gallery never sees prefetched bytes (the .nomedia concern dissolves by construction, P-7).
  6. OTA updates. EAS Update ships JS-only fixes same-day, store-compliant (Apple DPLA §3.3.1(B) interpreted-code carve-out; Play’s VM/interpreter exemption). CodePush retired 2025; Flutter’s answer is third-party Shorebird. For a solo founder shipping agent-authored code, review-free fix velocity is a major operational lever. Rule: OTA for fixes/iteration, never feature-unlocks past review (that’s a 2.3.1 violation).

Accepted costs, actively managed:

CostRealityManagement
App sizeRN is the largest of the options (realistic 20–30 MB download vs <15 MB ideal; Google’s data: ~1% install conversion lost per 6 MB)Download size is a CI-tracked budget from the first build (FE-2): AAB per-ABI splits, dependency audits, no fat SDKs. Escape hatch at scale: a native Lite SKU (ShareChat pattern) — backend + ranker unchanged.
Perf ceilingBelow hand-tuned Kotlin on low-end devicesIrrelevant at launch scale; cold-start time is a tracked metric from day one (Josh case study: 30% faster startup measurably lifted retention). Hot paths can drop to native modules without changing the architecture.

Rejected: Flutter (kills the shared ranker; framework-owned text stack; weaker OTA story), KMP/Compose Multiplatform (shares Kotlin, not TS — the Worker side would still be a second implementation; thinnest ecosystem for this needs-list), dual native (2× work, no OTA, worst agent leverage).

3. Placement & platform targets

  • Workspace: apps/consumer (pkg consumer-app — universal Expo app; apps/ = user-facing UIs). Imports @namaste-ji/shared and @namaste-ji/feed. expo-router. Web target ships first (FE-11): expo export --platform web → Cloudflare Pages namaste-ji-app-staging, same-origin with services/consumer-api (worker route /api/*) on app-staging.namasteji.org — the exact console topology. iOS/Android targets of the SAME codebase come later (Bluesky/X react-native-web precedent).
  • Android: targetSdk 36 from day one (Play mandates API 36 for new apps from Aug 31, 2026 — ⚠ re-verify); minSdk 26 (Android 8.0; Android 11+ ≈ 91% of India mobile traffic, Tier-2/3 installed base a few points older — drop to 24 only if field data demands). AAB + Play App Signing (mandatory).
  • iOS: current Expo default floor. Dev loop on the founder’s iPhone via EAS internal-distribution builds; TestFlight internal → external for betas.
  • Test hardware: a 3–4 GB-RAM Redmi/Infinix-class device (~₹7–10k) joins the loop before any beta. Perf claims are validated there, not on the iPhone.
  • Size + startup budgets (FE-2): CI records download size and cold-start time per build; regressions fail loudly. Targets: ≤25 MB download at launch (aspire lower), cold start ≤2.5s on the low-end reference device.

4. Feed client (consuming the recommender)

The serving contract is DISTRIBUTION.md §4/§7 — the client renders and applies local cooldown; it never ranks from scratch.

  • Playlist consumption: fetch the precomputed per-user/cohort playlist (KV via the serving Worker) + the per-locale “what’s live today” manifest. Feed JSON inlines BlurHash/ThumbHash strings → placeholders render with zero layout shift, images swap in from expo-image’s native cache.
  • In-session re-rank (the ms loop, FL-6): last-N skips/dwells nudge the remaining cards on-device with no server round-trip, using the same rank(candidates, userState, context, policy) from @namaste-ji/feed the Worker uses. One implementation, one test suite (FL-1).
  • Local seen-cooldown (seen_cooldown_hours from the FeedPolicy) enforced client-side so nothing repeats within a session window (DS-5).
  • UI: full-screen vertical snap feed (FlashList v2, pagingEnabled), category tabs (For you · Good morning · your deity · Festival · Fresh), caption hero, right action rail (WhatsApp · save · get) — per the Feed Lab mock (FEED-LAB §3).
  • Edge logistics on-device (DS-7, P-7): prefetch tomorrow’s morning set overnight on unmetered WiFi via expo-background-task (WorkManager/BGTaskScheduler); respect Data Saver (ConnectivityManager.isActiveNetworkMetered); all cached bytes live in app-private storage (gallery never polluted). A share must never trigger a download — bytes are pre-positioned so the forward is instant on 2G.
  • Share (FE-3): hybrid pattern, competitor-validated — a giant one-tap WhatsApp button (direct com.whatsapp ACTION_SEND intent with a FileProvider content:// URI) as the primary CTA, the OS share-sheet as secondary. On tap: log exactly one share_intent event, hand off, stop tracking (DS-10, P-8). No recipient, no attribution, no deferred deep links.

5. Onboarding, auth & user management (FE-4)

Self-hosted Better Auth on Workers + D1 (D7) — validated: native D1 dialect (v1.5+), @better-auth/expo client, phone-number OTP plugin with pluggable sendOTP. Implementation guardrails from documented production failures: one auth instance per Worker isolate (multiple instances → D1 write-lock contention → cascading 503s); rate-limit state in D1/DO, never KV (Better Auth writes TTL=10s; KV minimum is 60s); fire-and-forget sendOTP.

The funnel, cost-ranked for India:

PriorityMethodNotes
Android primaryTruecaller SDK one-tapOfficial RN SDK; removes the OTP step for the majority of the 35+ Android audience. Android-only; requires Truecaller app. ⚠ No public pricing — confirm business terms before committing.
Fallback + iOS primaryPhone OTP, WhatsApp-firstMeta auth template ≈ ₹0.115/msg + GST + BSP markup; no TRAI DLT registration needed — can ship pre-incorporation.
Last resortSMS OTP (MSG91 / 2Factor)₹0.15–0.20/delivered. Requires TRAI DLT (entity + header + template, ≈₹5,900, 2–3 weeks, needs a legal entity) → sequenced after incorporation. Never Twilio/Firebase for India OTP (3–10× cost).
Android secondaryGoogle Sign-In via Credential ManagerLegacy Google Sign-In SDK is deprecated; the free @react-native-google-signin lacks Credential Manager (paid tier) — budget it or use a community module.
iOS at launchPhone-OTP onlyDeliberate: with no third-party login on iOS, App Store guideline 4.8 (Sign in with Apple) is not triggered. When Google lands on iOS later, add Apple at the same time, linking identities on phone number (Apple private-relay emails don’t join otherwise).
  • Sessions are the #1 cost lever: sliding 90–180-day sessions persisted in expo-secure-store → auth events drop to ~0.1–0.2/user/month → ≈₹10–40k/month at 1M MAU (vs ₹1.5–2.5L naive 30-day sessions). Silent re-auth; login is a rare event.
  • Onboarding = the OnboardingSignals contract (FL-10), unchanged: phone → optional language → optional deity. Cold-start floor = device locale + IP geo + signup time.
  • Compliance features built into auth from day one (store-blocking otherwise):
    • In-app account deletion + a public web deletion URL (Play Data-safety requirement; Apple 5.1.1(v)).
    • Fully browsable/sharable logged-out mode — login gates only personalization (Apple requires it; it’s also right for time-to-first-send).
    • Static-OTP review bypass: whitelisted review phone number → fixed code, server side, documented in store review notes (reviewers can’t receive SMS). Standard practice; stays enabled for every review cycle.
  • PII plane: phone number + profile live in D1 (P-27/P-28). DPDP full obligations bite May 13, 2027; launch DPDP-clean anyway (consent notice at phone collection in hi/en, deletion flow doubles as erasure, 72h breach plumbing). D1 has no India jurisdiction today (only eu/fedramp) — lawful under DPDP Rule 15’s negative-list model for a non-SDF; watch item: Significant Data Fiduciary designation at scale.

6. Internationalization — day 0, non-negotiable (FE-5)

The whole app UI is multilingual from the first commit. Content is already multilingual by design (per-language captions, localized EventInstance names/blessings); the app chrome matches it.

  • Stack: i18next + react-i18next (the RN industry standard) + expo-localization (device-locale detection) + ICU messages (i18next-icu) for plurals/interpolation. Typed translation keys (TS augmentation) so agents get compile-time errors on missing keys. (Lingui noted for smaller compiled catalogs — revisit only if FE-2’s size budget forces it.)
  • Launch locales: en, hi, hi-Latnhi-Latn is a first-class BCP-47 locale, not a hack. Fallbacks: hi-Latn → en, hi → en, default en. Adding a locale (mr/ta/… are already content targets, CREATIVE-PLANE §5) = drop in one catalog, zero code changes.
  • Enforcement from commit 1: ESLint forbids raw string literals in JSX (eslint-plugin-i18next); CI fails on untranslated keys in launch locales. This is what makes “day 0” real rather than aspirational.
  • Locale resolution: explicit user choice (onboarding, optional, stored server-side in the profile) → device locale → en. The server-side preference drives localized push templates (the morning push arrives in the user’s language; templates per-locale in KV config) and the share-landing page.
  • One source of truth for the supported-languages list in @namaste-ji/shared — consumed by app, onboarding schema, push templates, landing page. Discovered from config, never hardcoded per-surface.
  • Formatting & fonts: Hermes ships ECMA-402 Intl — use it for dates/numbers (panchang/festival dates render localized); keep Latin digits (audience norm). Platform Devanagari fonts by default; any custom brand font must ship a Devanagari subset or fall back cleanly.
  • RTL forward-compat: no hardcoded left/right styles (logical properties/flex only) — an RTL locale (e.g. ur; Eid content exists in the calendar) becomes a catalog drop, not a layout rewrite. P-26 holds: machine streams stay English; localization is presentation-layer only.
  • Store surfaces localized too: Play listing + App Store metadata in en + hi at launch.

7. Analytics, experimentation & crash (FE-6)

  • Statsig stays the consumer experimentation layer (BO-6), behind a thin port (packages/flags). New vendor risk: Amplitude took over the Statsig platform/brand in May 2026 (OpenAI kept the team) — ⚠ re-verify pricing/continuity at adoption. PostHog is the named plan-B (mature RN SDK, mobile session replay GA, self-hostable if DPDP tightens). The port makes the swap a renewal-time decision, not an architecture problem.
  • Split the event pipeline by economics — this is load-bearing:
    • To Statsig: experiment exposures + a handful of business events (share_intent, session_open, push_received). Statsig meters events (free 2M/mo; ~$0.05/1K after) — the raw feed firehose there would be ≈$15K/month.
    • To Workers Analytics Engine: the full impression/dwell/skip/open firehose (≈10M events/day at scale) via client-side batching → one POST → ingest Worker (validates with the shared Zod event schema) → writeDataPoint. AE absorbs this trivially (250 points/invocation, unlimited cardinality, SQL API). Caveats: 3-month retention, sampled reads — perfect for ranker telemetry + dashboards; tee to R2 via Queues later if raw history is ever needed.
  • Flag evaluation: server-side in the serving Worker via the first-party Statsig Cloudflare KV adapter (sub-50ms edge evaluation, survives a Statsig outage — as designed in BO §8). Client: @statsig/js-client + @statsig/expo-bindings. BO-11 boundary holds: product tunables in Statsig Dynamic Config; anything safety-shaped in the Control DO — never crossed.
  • Crash reporting: Sentry (@sentry/react-native, official Expo config plugin, source-map/dSYM upload via EAS; free 5K errors/mo). Crashlytics rejected (Google coupling, native Firebase modules); BugSnag rejected (7-day free retention).
  • Privacy posture (P-8): no IDFA, no ad SDKs, first-party analytics keyed to opaque user IDs → no ATT prompt on iOS, “Data Not Used to Track You” label. Phone numbers never reach any analytics vendor. Settings include an analytics opt-out toggle (Apple 5.1.1(ii) + DPDP alignment).

8. Push notifications (FE-7)

  • Client: expo-notifications (raw FCM/APNs device tokens). Server: direct FCM HTTP v1 from Workers — Cron Trigger → Queues fan-out → consumer Worker → FCM (default quota 600K req/min; 1M pushes clear in minutes). Expo’s hosted push service rejected for scale (600 notifications/sec cap, extra vendor hop). APNs direct when iOS ships. This extends NOTIFICATION-SERVICE.md to recipient_type: consumer.
  • Send-time: India is one timezone — per-user preferred-slot bucketing across the 5:30–8:30am IST window, cron every 5–10 min, never synchronized to :00/:15/:30/:45 (FCM anti-spike guidance).
  • The OEM reality (product rule): Xiaomi/Oppo/Vivo battery killers silently drop 12–18% of FCM on exactly our devices. Mitigations: high-priority messages; app-side delivery receipts as the only trusted delivery metric; per-OEM autostart/battery-whitelist helper screens (dontkillmyapp patterns); and — the rule — the morning ritual must not depend on push. Push reinforces the habit; it is never the sole trigger.
  • Permission UX: POST_NOTIFICATIONS (Android 13+) asked contextually after the first successful share, with a pre-prompt explainer (“your good-morning image, once a day”); iOS provisional authorization first, upgrade later.
  • Deep links (FE-8): plain Android App Links + iOS Universal Links via Expo config; assetlinks.json + AASA served by a Worker on namasteji.org. No Branch/AppsFlyer/ attribution SDKs — deferred deep linking is attribution and contradicts stop-at-the-share-sheet (P-8). If install-source measurement is ever wanted, Play Install Referrer is a free first-party fallback.

9. UX blueprint (35+, Tier-2/3, low-literacy-aware)

  • Vernacular-first onboarding — language picker is screen one (and skippable); icon+label on every control; large single-purpose tap targets; minimal text (Google Build-for-Billions + NBU low-literacy research).
  • Retention levers, priority-ordered:
    1. Fixed wake-time morning push in the user’s language (§8).
    2. “Today’s greeting” home-screen widget (FE-9) — Google case study: +25% retention among widget users for a daily-ritual app; near-zero competitor adoption; taps straight into share.
    3. Ritual continuity, not gamified streaks — festival-calendar/panchang “today” layer (via events-calendar) is the proven category glue (Sri Mandir’s daily-darshan pattern); a diya/day counter at most. No points, no leaderboards, for this audience.
    4. Obsessive cold-start performance (tracked per FE-2).
  • Time-to-a-good-send is the session metric (DS-2) — the feed opens ready (cached playlist), the first card is sendable, and the share button is the biggest thing on screen.
  • Filter-bubble guard honored: big-festival content surfaces for everyone (DS §5).

10. Web share-landing (the install funnel)

Every watermarked image traveling on WhatsApp needs a landing spot: a tiny Worker-served mobile-web page on namasteji.org (first-level host — free-plan SSL constraint): the greeting full-screen + “Get the app” → Play Store with a Play-referrer param (+ App Store when live), deep link into the app if installed. Localized (§6). Play Instant is dead (Dec 2025) — mobile web is the try-before-install funnel. Same Worker serves assetlinks.json/AASA (§8). No PWA feed at launch; the isomorphic @namaste-ji/feed keeps a future web feed cheap if ever wanted.

11. Decisions log

#DecisionRationale
FE-1React Native + Expo (New Architecture, Hermes, EAS), managed workflow; Flutter/KMP/dual-native rejectedOnly stack where @namaste-ji/feed + Zod contracts run literally on-device (FL-1); platform-rendered Indic text; TS agent-operability; Sri Mandir precedent; FlashList+expo-image = the exact feed stack; EAS Update = store-compliant OTA
FE-2Download size + cold-start are CI-tracked budgets (≤25 MB, ≤2.5s on low-end reference device)RN’s honest costs; install conversion drops ~1%/6 MB in this market; startup speed is a proven retention lever. Native Lite SKU = escape hatch, backend unchanged
FE-3Hybrid share: one-tap direct WhatsApp intent primary + OS sheet secondary; single share_intent event then stopCompetitor-validated (ShareChat playbook); pre-positioned bytes make it instant on 2G; P-8 honored
FE-4Auth funnel: Truecaller one-tap (Android) → WhatsApp OTP → SMS last; Google secondary on Android via Credential Manager; iOS ships phone-OTP-only; 90–180-day sliding sessionsCost-ranked for India (WhatsApp OTP needs no DLT; SMS needs entity+DLT); OTP-only iOS legally avoids Sign-in-with-Apple (guideline 4.8); sessions are the dominant cost lever
FE-5i18n from commit 1: i18next+ICU, typed keys, en/hi/hi-Latn launch locales, no-raw-strings lint in CI, locale = user choice → device → enRetrofitting i18n is the expensive migration; hi-Latn is first-class; new locale = catalog drop
FE-6Event-pipeline split: exposures + key conversions → Statsig (behind a thin port; PostHog = plan-B); raw feed firehose → Workers Analytics Engine. Sentry for crashesFirehose to Statsig ≈ $15K/mo; AE absorbs it free. Amplitude-takeover vendor risk contained by the port
FE-7Push = expo-notifications client + direct FCM v1 fan-out from Workers (Cron→Queues); morning ritual must not depend on pushExpo push service caps at 600/sec; 12–18% silent loss on Chinese OEMs makes push reinforcement, not trigger; delivery receipts are the only true metric
FE-8No attribution SDKs; plain App Links/Universal Links onlyDeferred deep linking = attribution = violates stop-at-share-sheet (P-8); Firebase Dynamic Links is dead anyway
FE-9Home-screen widget (“today’s greeting”) is a launch-adjacent priority, not a nice-to-have+25% retention in the closest published case study; near-zero competitor adoption; perfect fit for a daily-ritual product
FE-10No ATT, no ads, no IDFA at launch; analytics keyed to opaque IDs; opt-out toggleClean privacy labels both stores; matches P-8 and the free-app posture; monetization deferred
FE-11Web-first universal app: the same Expo codebase renders to web (react-native-web, static export → Pages) and ships FIRST as a private beta at app-staging.namasteji.org; iOS/Android targets flip on laterFounder tests on web fast; near-100% reuse when mobile ships (Bluesky/X precedent); consumer-api (BFF) + same-origin topology mirrors the console pair
FE-12Bootstrap auth (pre-OTP): BO-admin-created accounts, username() + admin() Better Auth plugins, placeholder emails, NO email service; users change passwords in-app, forgotten = admin temp-reset; phone-OTP later is additive on the same instance (never add custom columns to Better Auth tables)Zero external deps for the beta; the OTP end-state (FE-4) needs no migration; email verification can be added free later (Resend ~3k/mo) if ever wanted

12. Open questions

  • Truecaller business terms (no public pricing) — confirm before building it as primary.
  • Statsig-under-Amplitude pricing/continuity at adoption time (PostHog port stands by).
  • Widget scope at v1: Android-only first (audience) or both (expo-widgets/native module effort differs)?
  • Save/“get” actions (rail buttons in the mock): local-only bookmarks vs server-synced — spec when the feed skeleton lands.
  • Lite SKU trigger: define the size/telemetry threshold that would justify it (not before real field data).

13. Build order (the scaffold plan)

Status: steps 1 + the auth slice are BUILT (web-first, FE-11/FE-12): apps/consumer scaffolded with i18n-from-commit-1, services/consumer-api (Better Auth) live locally, BO Users page manages accounts. Styling awaits the style-guide validation; feed slice next.

  1. Scaffold apps/consumer: Expo app in the workspace, expo-router, i18n wired from the first commit (i18next + expo-localization, en/hi/hi-Latn catalogs, no-raw-strings lint in CI), FlashList feed skeleton consuming a stub playlist from a local Worker, OnboardingSignals wired, EAS project + dev build on the founder’s iPhone (native targets — after the web beta).
  2. Feed vertical slice: serving Worker endpoint (KV playlist + manifest) → real catalog assets from staging R2/Images → BlurHash inline → share flow with pre-positioned bytes.
  3. Auth slice: Better Auth on a Worker + D1 (staging), phone-OTP (WhatsApp-first) via the sendOTP callback, Expo client, logged-out browsing, deletion flow, static- OTP review bypass.
  4. Signals slice: event batching → ingest Worker → Analytics Engine; Statsig port + KV adapter; Sentry.
  5. Prefetch + push: expo-background-task overnight set; FCM fan-out (NOTIFICATION-SERVICE extension); permission UX after first share.
  6. Store readiness: APP-STORE-OPS.md checklists (accounts are created in parallel with all of the above — they are fixed latency).