AD
All projects
Vela: AI Therapist icon
AI2026 proprietary, code private

Vela: AI Therapist

Gamified AI mental-wellness companion with real-time voice

Play Store Flutter engineer, built the app end-to-end

Vela is an AI companion for anxiety, stress and the 2am moments: text or talk, any hour, with mood tracking and weekly insights that show patterns over time. I built the Flutter client end-to-end: a strictly layered Riverpod codebase with real-time voice sessions, gamified wellness progression, subscription billing and eight fully localised languages.

The Problem

A wellness app is judged on whether someone opens it on their worst day. That puts the weight on the client: the conversation has to stream without stalling, voice has to connect on a bad network, progress has to feel earned rather than gamified for its own sake, and nothing can break in the seven languages the developer does not read.

The Solution

A feature-first clean architecture: data, domain and presentation layers with `Either<Failure, T>` across every repository boundary, so no widget ever sees a raw exception. Riverpod code-gen keeps one provider per concern, GoRouter owns every auth and paywall redirect in a single pure redirect function, and a custom SSE client streams chat tokens. LiveKit handles real-time voice; RevenueCat handles purchases; Sentry and a Firebase-plus-Mixpanel analytics facade give one sink each for errors and journey telemetry.

Architecture

  • Flutter with Riverpod code-gen (`@riverpod`) and freezed immutable state: one provider per concern, no god-providers
  • Feature-first clean architecture: widget → provider → use case → repository → datasource, with fpdart `Either<Failure, T>` at every domain boundary
  • GoRouter with named routes; all auth, onboarding and paywall redirects resolved in one pure redirect function
  • Dio `ApiClient` with a retry + 401 interceptor, plus a custom `SseClient` for token streaming
  • LiveKit real-time voice sessions; RevenueCat subscriptions; Firebase Auth with Google and Apple sign-in
  • Eight locales (en, de, es, fr, ja, ko, nl, ru): every user-visible string and accessibility label lives in ARB files
  • Sentry as the single error sink; a Firebase Analytics + Mixpanel facade as the single analytics entry point
  • Centralised session teardown so every user-scoped provider, token and analytics identity resets together on sign-out

Challenges & How I Solved Them

Logged out only on Play Store builds

Users were signed out on cold start, but only on installs from Play, never locally, and with no error surfaced because release builds strip debug logging. The cause was Android Auto Backup: it restores a Firebase Auth keyset that the new install cannot decrypt, silently invalidating the session. Disabling backup for the auth keystore fixed it.

Eight languages without a translation layer

Every string, tooltip and screen-reader label is an ARB key across all eight locales: a missing key throws at runtime rather than falling back. Sentences with inline links needed per-language treatment: the English prefix-plus-link split breaks under Japanese and Korean verb-last word order, so those locales carry the full phrase in the link key.

A design system that survives both themes

A three-layer colour rule (raw palette, semantic `ColorScheme` entries, then widgets that only read the resolved theme) removed every `isDark ? :` ternary from the UI. Adding a semantic colour means adding it to both schemes once, not patching call sites.

My Contributions

  • The entire Flutter client: architecture, state, navigation, theming and release flow
  • Real-time voice sessions over LiveKit and SSE-streamed chat
  • Gamified wellness progression: XP, levels and streak logic
  • Full eight-locale internationalisation including accessibility labels
  • Observability and analytics wiring: Sentry reporting plus a single Firebase + Mixpanel facade

Impact

  • Live on Google Play as Vela: Your AI Therapist
  • Shipped in eight languages with localisation enforced at build time
  • Sole author of the Flutter codebase across 250+ commits

Lessons Learned

Strict layering pays for itself the first time a bug is invisible in debug builds. Because failures were typed and routed through one reporter, the Play-only logout was a matter of finding where the platform lied, not of untangling where the error went.

Tech Stack

FlutterDartRiverpodfreezedGoRouterDiofpdartLiveKitRevenueCatFirebaseSentryMixpanelmocktail