Skip to main content

Architecture

LifeWell is a four-surface product — web app, Android (with iOS in prep), browser extension, and a Firebase Functions backend — that shares a single React + TypeScript codebase wherever possible. The architecture optimizes for code reuse across surfaces, no-cost backend operation under Firebase's free Spark plan, and strict permission hygiene at every boundary.

This page covers the high-level shape: which technology runs where, why those choices were made, and where to look in the codebase for each layer.

The four surfaces at a glance

SurfaceCode locationRuntimeDistribution
Web appsrc/React 19 + Vite 8 in the browserlifewell.aoneahsan.com, Firebase Hosting
Android (and iOS in prep)android/, ios/Capacitor 8 + same src/ React app inside a WebViewPlay Store (pending), App Store (in prep)
Browser extensionextension/WXT-built Manifest V3 extensionChrome Web Store (pending)
Backend functionsfunctions/Node 22 on Firebase Cloud FunctionsInternal — only invoked from clients

The web src/ is the canonical UI. Capacitor wraps it in a native shell with platform-specific plugins; the extension is a separate, much smaller bundle that talks to the same Firestore data.

Frontend stack (web + Android)

LayerChoiceWhy
FrameworkReact 19Server components are not used; we want client-side interactivity everywhere
BuildVite 8Fast HMR in dev, small production bundles, mature ecosystem
LanguageTypeScript 6Strict mode, no any in app code
RoutingTanStack RouterFile-based routes in src/routes/, type-safe params and search
StateZustand 5Lightweight, no Provider wrapping, persist middleware
Formsreact-hook-form + ZodType-safe schemas, no rerenders on every keystroke
UI primitivesRadix UI + @radix-ui/themesAccessible primitives, full theme customizer exposed to users
StylingTailwind CSS 4Utility-first, design tokens in tailwind.config.ts
Data fetchingTanStack Query (selected screens)Server-state caching, background refetch
TablesTanStack Table v8 + react-virtualOne DataTable<T> primitive for every grid in the app
ChartsD3 7.9Direct SVG control, no chart-library lock-in
i18ni18next10 locales (English, Arabic with RTL, Spanish, French, German, Portuguese, Russian, Turkish, Urdu, Chinese)
Mobile shellCapacitor 8 + 25+ pluginsSame React app on Android (and iOS soon)
Push notificationsOneSignal (free tier)Cross-platform delivery, web + Android + iOS
ErrorsSentryDSN configured; gracefully no-ops if env key missing
AnalyticsAmplitudeEvent-level analytics; gracefully no-ops if env key missing

Backend (Firebase + Cloudflare Workers)

LifeWell deliberately avoids paid backend services. The free Firebase Spark plan handles the bulk of state, and Cloudflare Workers (also free) handle the few server-side responsibilities Firebase Security Rules can't.

ConcernWhere it livesWhy
User accounts (auth)Firebase AuthenticationGoogle + Apple + email; per-account JWT
User dataFirestore (collection prefix lifewell_*)Real-time, offline-cached, per-user security rules
Indexes + rulesfirestore.indexes.json + firestore.rulesVersioned in the source repo
Files (avatars, memory media)FilesHub API (https://fileshub.zaions.com)Free, simple object storage; FilesHub is another Ahsan project
Token brokering (Google Drive, third-party APIs)Cloudflare Worker lifewell-gdrive-token-proxyKeeps client_secret off the frontend
Push deliveryOneSignal (free tier)We send via the OneSignal REST API, delivery is OneSignal's job
Heavy computeFirebase Cloud Functions (Node 22)Used sparingly — only when Security Rules can't enforce something

Cloudflare Worker secrets follow a strict naming rule: every variable is prefixed with the project name (e.g. LIFEWELL_GOOGLE_CLIENT_SECRET) so multiple Ahsan projects can share one Cloudflare account without collisions.

How the surfaces share code

The React app in src/ is the bulk of the codebase. Capacitor doesn't fork it — Capacitor packages the production web build into an Android app, with native shell + Capacitor plugins providing access to platform features (camera, push, geolocation, biometrics, native back-gesture handling, edge-to-edge layouts, native haptics).

The browser extension at extension/ is a separate small bundle. It can't reuse the full app — Chrome Web Store policy disallows remotely-hosted code in extensions, so the extension can't lazy-load lifewell.aoneahsan.com's bundle. Instead it has a tiny self-contained UI that talks directly to Firestore via the REST API and brokers Google sign-in via Chrome Identity API (no Firebase Auth SDK).

LayerWebAndroidExtension
UI bundlesrc/ (React 19 + Vite)src/ packaged by CapacitorTiny separate extension/ bundle
AuthFirebase Auth SDKFirebase Auth SDK + Capacitor native pluginsChrome Identity API
StorageFirestore (online cache) + Capacitor PreferencesFirestore + native PreferencesChrome storage API
PushOneSignal Web SDKOneSignal Android SDKService worker chrome.notifications

Component organization

src/components/ is grouped by feature domain (one folder each for health/, tools/, family/, memories/, notes/, chat/, etc.) plus shared primitives (ui/, form-fields/, layout/, theme/). The pattern is:

  • src/components/<feature>/* — feature-specific React components.
  • src/components/ui/* — Radix-wrapped primitives shared across features.
  • src/components/form-fields/* — react-hook-form-aware field wrappers (text input, select, date, etc.) used everywhere a form is rendered.
  • src/components/theme/* — theme customizer dialog + utilities.

Services live in src/services/ (one per feature domain). Stores live in src/stores/ (Zustand stores, also one per domain). Hooks live in src/hooks/. Types in src/types/.

Discipline rules baked into the codebase

LifeWell enforces several project-wide rules through ESLint, Husky pre-commit hooks, and explicit code structure:

  • Centralized logger. No direct console.* calls in app code. ESLint flags any. The src/utils/logger.ts module is the single API; level is controllable at runtime via localStorage['lifewell:logLevel'].
  • No raw localStorage. Use @/lib/preferences-storage (Capacitor Preferences with localStorage fallback). Direct localStorage.* is banned in src/.
  • No banned packages. @capacitor-firebase/crashlytics and @capacitor-firebase/performance are explicitly forbidden — they cause Gradle wiring problems on Android. Sentry and Amplitude cover error/perf instead.
  • URL state preservation. Modals, tabs, filters, multi-step forms — anything the user expects to survive a refresh — lives in URL search params via src/hooks/use-url-state.ts, never raw useState.
  • Form fields convention. <FieldLabel required> for required fields (red asterisk); plain <Label> for optional. Optional fields must always allow empty submit.

These rules are enforced because each one was earned the hard way — see About the developer for the back-story on the centralized-logger and Play-Console-permissions rules.

Frequently asked

Why React 19 instead of a server-rendered framework? LifeWell's UI is interaction-heavy (drag-to-edit family tree, real-time chat, calculator inputs that update live). Server rendering buys little here. Vite 8's static build still produces small bundles, gets cached aggressively by the service worker, and integrates cleanly with Capacitor's WebView packaging on Android.

Why Capacitor instead of React Native? Code reuse. Capacitor wraps the same React app you ship on the web — one codebase, three surfaces. React Native would require a separate UI tree. Capacitor's native plugin ecosystem (camera, push, geolocation, biometrics, native back-gesture handling) covers everything LifeWell needs for the mobile surface.

Why Cloudflare Workers if you're already on Firebase? Firebase Functions are paid (Blaze plan) for any non-trivial workload. Cloudflare Workers' free tier (100k requests/day) is generous enough for our token-proxy needs. We only use Workers for things Firestore Security Rules can't guard — like exchanging an OAuth code for an access token, where client_secret needs to stay server-side.

Why FilesHub instead of Firebase Storage? Firebase Storage is paid (counts against Blaze egress). FilesHub (https://fileshub.zaions.com) is a separate free-tier service Ahsan built for exactly this case. The trade-off is that FilesHub is single-region (no global edge), but for user avatars and memory media the latency is fine.

How do you avoid any in TypeScript when integrating Firebase? Firebase's TypeScript types are strong from v9+. Where SDK types are weak (some admin SDK corners, third-party plugins), we wrap them in our own typed services in src/services/. App code never imports the raw SDK; everything goes through the typed wrapper.


Last updated: 2026-05-10 Author: Ahsan Mahmood