Overview
Sentira is an AI-powered mental wellness companion for iOS and Android, and a real-world example of how to develop a mental health app that people actually stick with. It combines daily journaling, mood tracking, guided check-ins, and a streaming AI chat companion — all behind a subscription paywall.
The product required serious infrastructure: three OAuth providers, Stripe in-app purchases, a real-time streaming API, and a production EAS Build pipeline for both platforms.
"Journal, reflect, and grow — every day."
The Challenge
Mental wellness apps live or die on trust and retention. The technical challenges reflected this:
- Multi-provider auth — support email/password, Google (native SDK), and Facebook OAuth without friction
- Subscription infrastructure — Apple Pay / Google Pay–integrated Stripe subscriptions with proper paywall logic
- Streaming AI chat — conversational AI responses that stream token-by-token, not dump all at once
- Keyboard-aware UX — journaling and chat are keyboard-heavy; poor keyboard handling destroys the experience
Full-Stack Architecture
graph TB
subgraph Mobile["Mobile App (Expo)"]
UI[React Native UI<br/>NativeWind]
JO[Jotai State]
TQ[TanStack Query]
end
subgraph Backend["Backend (Node.js / Bun)"]
API[REST API]
AI[AI Chat Service<br/>Streaming]
STRIPE[Stripe Webhooks]
end
subgraph Infra["Infrastructure"]
SB[(Supabase<br/>PostgreSQL + Auth)]
STRIPE_API[Stripe API]
LLM[LLM Provider]
end
UI --> JO
UI --> TQ
TQ --> API
API --> SB
API --> AI
AI --> LLM
STRIPE --> API
API --> STRIPE_API
style Mobile fill:#1A1A2E,stroke:#7DFF00,color:#fff
style Backend fill:#0D0D18,stroke:#7DFF00,color:#fff
style Infra fill:#1A1A2E,stroke:#8A8A9A,color:#fff
The backend runs on Node.js with Bun runtime — fast startup, native TypeScript, and compatible with the Bun workspace monorepo.
Authentication Architecture
Getting three OAuth providers working natively — especially Google on iOS and Android — requires careful setup. Here's the auth flow:
flowchart TD
START([User opens app]) --> ONBOARDING[Onboarding flow]
ONBOARDING --> CHOICE{Sign-in method}
CHOICE -->|Email| EMAIL[Email + password form]
CHOICE -->|Google| GOOGLE[Google native SDK<br/>@react-native-google-signin]
CHOICE -->|Facebook| FACEBOOK[Facebook OAuth<br/>WebView flow]
EMAIL --> VERIFY[Email verification]
GOOGLE --> TOKEN[Google ID token]
FACEBOOK --> CODE[OAuth code]
VERIFY --> SUPABASE[(Supabase Auth)]
TOKEN --> SUPABASE
CODE --> SUPABASE
SUPABASE --> SESSION[JWT session]
SESSION --> APP[Authenticated app]
style START fill:#0D0D18,stroke:#7DFF00,color:#fff
style APP fill:#1A1A2E,stroke:#7DFF00,color:#fff
style SUPABASE fill:#0D0D18,stroke:#7DFF00,color:#fff
Key implementation decisions:
- Google Sign-In uses the native SDK (
@react-native-google-signin/google-signin) — not the WebView flow — for the best UX and reliability - Facebook OAuth runs through Supabase's OAuth redirect flow
- Email/password includes verification, resend, and proper error states
- All three providers funnel into a single Supabase session
Subscription & Paywall
stateDiagram-v2
[*] --> Free: New user
Free --> Paywall: Hit feature gate
Paywall --> Checkout: Tap subscribe
Checkout --> Processing: Apple Pay / Google Pay
Processing --> Active: Payment success
Processing --> Free: Payment failed / cancelled
Active --> Cancelled: User cancels
Cancelled --> Grace: Grace period
Grace --> Free: Period expired
Active --> Active: Renewal
The subscription system uses Stripe React Native with:
- Native Apple Pay and Google Pay sheets — no redirect, no browser
- Webhook handling on the backend to update subscription state in Supabase
- Paywall UI that adapts based on subscription status (gating specific features)
Streaming AI Chat
The AI chat companion streams responses token-by-token. The user sees the reply building character by character — not a loading spinner followed by a wall of text.
sequenceDiagram
participant User
participant App as Mobile App
participant API as Backend API
participant LLM as LLM Provider
User->>App: Send message
App->>API: POST /chat/stream
API->>LLM: Forward with context
LLM-->>API: Stream tokens
API-->>App: SSE stream chunks
App-->>User: Render tokens in real-time
LLM-->>API: [DONE]
API-->>App: Stream close
Implementation: Server-Sent Events (SSE) from the backend, consumed in the React Native app via a streaming fetch. Each token chunk updates the message state, triggering a fast re-render.
Keyboard-Aware UX
Journaling and chat both involve heavy keyboard usage. On React Native, poor keyboard handling is a common and painful bug — content gets obscured, scroll position jumps, or the keyboard dismisses unexpectedly.
We used react-native-keyboard-controller throughout. It provides:
- Smooth animated keyboard transitions (no snap/jump)
KeyboardAvoidingViewreplacement with better native behavior- Works correctly on both iOS and Android
Tech Stack
| Layer | Choice |
|---|---|
| Framework | Expo (React Native) + Expo Router |
| Language | TypeScript |
| Backend | Node.js (Bun runtime) |
| Database & Auth | Supabase (PostgreSQL + Auth) |
| State (client) | Jotai |
| State (server) | TanStack Query |
| Styling | NativeWind |
| Payments | Stripe React Native + EAS |
| Auth Providers | Supabase OAuth — Google native, Facebook, email |
| Keyboard | react-native-keyboard-controller |
| Build | EAS Build (IPA / AAB) |
| Monorepo | Bun workspaces |
Monorepo Structure
sentira/
apps/
mobile/ ← Expo app
api/ ← Node.js backend (Bun)
packages/
types/ ← Shared TypeScript types
config/ ← Shared ESLint, tsconfig
Shared TypeScript types between the mobile app and backend eliminate a whole class of API contract bugs.
Outcome
- Full-stack production app shipped on iOS and Android
- Three OAuth providers working reliably with native flows
- Stripe subscriptions with Apple Pay and Google Pay integrated
- Streaming AI chat with smooth token-by-token rendering
- EAS Build pipeline producing signed IPA and AAB
Building a wellness, health, or consumer subscription app? Let's talk →