Back to blog
flutter

Muse Otter — Technical Documentation: Stack, Architecture & RevenueCat

By Youcef EL KAMEL
18 min read

Muse otter, technical documentation

RevenueCat Shipyard Creator Contest 2026 Stack, Architecture & RevenueCat Implementation

1. tech Stack overview

I chose every piece of this stack for one reason: ship fast, scale later, don’t compromise. As a solo developer with a 3-week deadline, I needed tools that do the heavy lifting for me.

Full dependency map

LayerTechnologyWhy I chose it
FrontendFlutter 3.24+One codebase → iOS, Android, iPad, Web, macOS, Linux
StateRiverpod 2.6+Type-safe, testable, code-generated providers
Code Genfreezed + json_serializableImmutable entities, zero boilerplate
NavigationGoRouter 14.8+Declarative routing with auth guards
BackendCloud Functions v2Auto-scaling, pay-per-use, zero ops
DatabaseCloud FirestoreReal-time listeners, automatic sharding
AuthFirebase AuthAnonymous → account linking, zero friction
AI FrameworkGenkit 1.27+Google’s AI orchestration, Gemini native
AI ModelGemini 2.0 FlashFast, cheap, good enough for coaching
PaymentsRevenueCat 9.10+Cross-platform subs in hours, not weeks
VoiceVapi SDKReal-time voice calls, simple integration
AnalyticsMixpanel 2.3+Funnels, cohorts, user properties
CrashesFirebase CrashlyticsReal-time crash reporting
Feature FlagsFirebase Remote ConfigA/B testing paywalls and onboarding
i18nSlang 4.12+Type-safe translations, code-generated
Design SystemWidgetbook 3.10+Visual component catalog
IconsHeroiconsClean, consistent icon set
EmojiGoogle Fonts (Noto Emoji)Monochrome emoji, consistent across platforms

2. architecture

High-level overview

┌──────────────────────────────────────────────────────────┐
│ FLUTTER APP │
│ iOS · Android · iPad · Web · macOS │
│ │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────────┐ │
│ │Presentation │ │ Domain │ │ Data │ │
│ │ │ │ │ │ │ │
│ │ Screens │ │ Entities │ │ DTOs │ │
│ │ Widgets │──│ Use Cases │──│ Repositories │ │
│ │ Providers │ │ Interfaces │ │ Datasources │ │
│ └─────────────┘ └─────────────┘ └────────┬────────┘ │
│ │ │
│ Riverpod · GoRouter · Atomic Design │ │
└──────────────────────────────────────────────┼───────────┘


┌──────────────────────────────────────────────────────────┐
│ FIREBASE BACKEND │
│ │
│ ┌────────────┐ ┌────────────┐ ┌────────────────────┐ │
│ │ Firestore │ │ Auth │ │ Cloud Functions v2 │ │
│ │ │ │ │ │ │ │
│ │ users/ │ │ Anonymous │ │ onChatMessage │ │
│ │ coaches/ │ │ Google │ │ onReflection │ │
│ │ chats/ │ │ Apple │ │ onCoachCreate │ │
│ │ shared/ │ │ │ │ onUserCreate │ │
│ └────────────┘ └────────────┘ │ onUserDelete │ │
│ │ revenueCatWebhook │ │
│ │ vapiWebhook │ │
│ │ weeklyDigestEmail │ │
│ └─────────┬──────────┘ │
│ │ │
│ ┌────────────┐ ┌────────────┐ ┌─────────┴──────────┐ │
│ │ Storage │ │ Remote │ │ Genkit + Gemini │ │
│ │ (audio, │ │ Config │ │ 2.0 Flash │ │
│ │ images) │ │ (flags) │ │ (AI responses) │ │
│ └────────────┘ └────────────┘ └────────────────────┘ │
└──────────────────────────────────────────────────────────┘

 ┌──────────────┼──────────────┐
 ▼ ▼ ▼
 ┌────────────┐ ┌────────────┐ ┌────────────┐
 │ RevenueCat │ │ Vapi │ │ Mixpanel │
 │ Subscript. │ │ Voice │ │ Analytics │
 └────────────┘ └────────────┘ └────────────┘

Clean architecture, feature-First

Every feature in the app follows the same structure. No exceptions. This is what keeps a solo codebase manageable at 60+ stories.

lib/features/{feature}/

├── data/ ← Talks to Firebase
│ ├── dtos/ Entity serialization
│ ├── mappers/ DTO ↔ Entity conversion
│ └── repositories/ Implementation

├── domain/ ← Pure business logic
│ ├── entities/ Immutable (freezed)
│ ├── repositories/ Abstract interfaces
│ └── usecases/ One action = one class

└── presentation/ ← What users see
 ├── providers/ Riverpod state
 ├── screens/ Full pages
 └── widgets/
 ├── molecules/ Pure UI, no state
 └── organisms/ Composed widgets

Key rules I follow:

  • Entities are immutable (freezed), never have Firebase imports
  • Use Cases return Either<Exception, T>, success or failure, always explicit
  • Repositories are abstract in domain/, implemented in data/
  • Screens are the only place where Riverpod, navigation, and orchestration happen
  • Molecules never import Riverpod, they’re pure UI with callbacks

Widget architecture, atomic design

  • Molecules, Pure UI components. No Riverpod. No logic. Callbacks only. Widgetbook stories required.
  • Organisms, Combine molecules. Callbacks for actions. Layout logic. Compose, don’t inherit.
  • Screens, Riverpod providers. Navigation logic. Orchestration. The only place with state.

3. AI pipeline, the document-Status pattern

This is how every AI interaction works in Muse Otter. No WebSockets, no complex streaming. Just Firestore.

 FLUTTER APP FIREBASE BACKEND
 ────────── ────────────────

1. User sends message


2. Write to Firestore ──────────► 3. Cloud Function triggers
 status: "pending" (onChatMessageCreate)
 │ │
 │ 4. Load context:
 5. Show typing · Coach system prompt
 indicator · User personal context
 │ · Last 20 messages
 │ · Language directive
 │ │
 │ ▼
 │ 5. Call Genkit + Gemini
 │ │
 │ ▼
 │ 6. Write assistant message
 7. Firestore listener ◄────────── status: "done"
 updates UI


8. Response appears
 (perceived <500ms)

Context injection, what makes it personal

Every AI call includes the user’s condensed context. This is what makes the same Gemini model feel like a completely different coach for each user.

System prompt structure:
┌─────────────────────────────────────────┐
│ 1. Coach personality & methodology │
│ (scoping, questions-first, rules) │
├─────────────────────────────────────────┤
│ 2. User personal context │
│ (project, values, goals, challenge) │
├─────────────────────────────────────────┤
│ 3. Foundation entries (recent) │
│ (notes, summaries, reflections) │
├─────────────────────────────────────────┤
│ 4. Language directive │
│ (respond in user's locale: en/fr) │
├─────────────────────────────────────────┤
│ 5. Conversation history (last 20 msgs) │
└─────────────────────────────────────────┘

4. data model (Firestore)

users/{userId}
├── name, locale (en|fr), subscriptionStatus
├── messageCountToday, lastMessageDate
├── context: { summary, currentProject, values[], goals, challenge }

├── coaches/{coachId} ← User's coaches (default + custom + copied)
│ ├── name, emoji, description, color (hex)
│ ├── systemPrompt, status (draft|active)
│ ├── isDefault, sharedCoachId?, originalSharedCoachId?
│ └── createdAt, updatedAt

├── chats/{chatId}
│ ├── coachId, coachSource (user|public)
│ └── messages/{messageId}
│ ├── role (user|assistant), content
│ ├── inputType (text|voice|handwriting)
│ └── status (pending|done|failed)

├── knowYouQuestions/{questionId} ← Dynamic Q&A
├── knowYouMeta/stats ← Generation tracking
├── foundation/{entryId} ← Personal growth entries
├── voiceCredits/credits ← Balance in seconds
└── voiceSessions/{sessionId} ← Voice call history

sharedCoaches/{coachId} ← Marketplace (public)
├── name, emoji, description, systemPrompt
├── originalCreatorId, originalCreatorName
└── copyCount

defaultCoaches/{coachId} ← Coach templates (system)
usernames/{username} ← Username reservation

Key design decisions

  • Coaches are copies, not references. When a user gets default coaches, they’re copied to users/{uid}/coaches/. Users can edit or delete them freely. This avoids cascading updates and keeps the data model simple.
  • Marketplace is separate. sharedCoaches/ is a top-level collection independent from user data. If a user deletes their account, their published coaches survive.
  • Draft status for coaches. Custom coaches start as draft, users can save progress and come back later. First message sent → status becomes active.

5. cloud functions

I have 15 active Cloud Functions that handle all backend logic:

FunctionTriggerWhat it does
onChatMessageCreateFirestoreGenerates AI response via Genkit + Gemini
onReflectionCreateFirestoreExtracts and condenses user context from Q&A
onCoachCreateFirestoreAI-generates system prompt for custom coaches
onUserCreateFirestoreSeeds default coaches, username, voice credits, Know You questions
onUserDeleteAuthCleans up all subcollections, storage, username
onKnowYouQuestionAnsweredFirestoreProcesses answer, generates next questions
onQuestionBatchRequestCreateFirestoreBatch-generates Know You questions
transcribeAudioFirestoreCloud Speech-to-Text for voice input
revenueCatWebhookHTTPSyncs subscription status from RevenueCat
vapiWebhookHTTPHandles Vapi voice session events
checkUsernameAvailabilityCallableUsername uniqueness check
updateUsernameCallableAtomic username update with reservation
sendScheduledNotificationsScheduledPush notifications for coaching reminders
weeklyDigestEmailScheduledWeekly conversation summary email
seedVoicePromptsCallableAdmin: seeds Vapi voice assistant prompts

All functions run in europe-west1 on Cloud Functions v2 with automatic scaling.

6. revenueCat implementation

This is the part I’m most proud of from an integration standpoint. RevenueCat replaced what would have been weeks of StoreKit and Google Billing complexity.

Products & pricing

Product IDTypePricePeriod
monthly_10Auto-renewable$9.99Monthly
yearly_80Auto-renewable$79.99Yearly (save 17%)

Entitlement: Muse Otter Pro Offering: default with monthly + yearly packages

Architecture

┌───────────────┐ Purchase ┌───────────────┐
│ Flutter App │──────────────►│ RevenueCat │
│ │ │ (App Store / │
│ purchases_ │ Entitle- │ Play Store) │
│ flutter SDK │◄──────────────│ │
└───────┬───────┘ ments └───────┬───────┘
 │ │
 │ Read status │ Webhook events
 │ (stream) │ (server-to-server)
 ▼ ▼
┌─────────────────────────────────────────────────┐
│ FIRESTORE │
│ │
│ users/{userId} │
│ ├── subscriptionStatus: free|pro|donation|lifetime │
│ └── subscriptionUpdatedAt: timestamp │
│ │
│ ═══ Source of truth for the app ═══ │
└─────────────────────────────────────────────────┘

Client-side implementation

The app uses Firestore as the single source of truth for subscription status. The RevenueCat SDK handles purchases, but the status comes from Firestore, this makes it simple and reliable.

Key providers:

isProProvider
 └── Reads user.subscriptionStatus from Firestore stream
 └── Returns true if status != 'free'

ProGateWidget
 └── Wraps any Pro-only UI
 └── Shows upgrade prompt if not Pro

ProRequiredScreen
 └── Wraps entire screens
 └── Redirects to /paywall if not Pro

Server-side webhook

The revenueCatWebhook Cloud Function is an HTTP endpoint that RevenueCat calls on every subscription event:

RevenueCat EventActionNew Status
INITIAL_PURCHASEGrant Propro
RENEWALKeep Propro
UNCANCELLATIONRestore Propro
EXPIRATIONRevoke Profree
CANCELLATIONNo change (keeps Pro until expiry),
BILLING_ISSUENo change (grace period),

Protected statuses: donation and lifetime are never modified by the webhook. They can only be set manually in Firestore, this lets me grant free access to testers, contest judges, or early supporters without RevenueCat interfering.

Paywall triggers

The paywall appears naturally at the right moments:

  1. Message limit reached, Free user sends message #21 → soft paywall
  2. Custom coach creation, Free user taps “Create Coach” → paywall
  3. Marketplace copy, Free user tries to copy a community coach → paywall
  4. Settings, “Upgrade to Pro” button always visible

Each trigger is tracked in Mixpanel with paywallViewed event + trigger property for conversion analysis.

A/B testing with remote config

I use Firebase Remote Config to experiment with:

  • Paywall design variants
  • Free message limit (currently 20/day)
  • Onboarding flow (with or without Know You)
  • Which Know You questions to show first

7. platform support & responsive design

PlatformStatusNotes
iOS 14.0+ProductionApp Store submission
Android SDK 24+ProductionPlay Store submission
iPadProduction (P0)Three-column layout, Apple Pencil handwriting
WebProductionFirebase Hosting at museotter.web.app
macOSBetaDesktop native
LinuxBetaDesktop native

Responsive breakpoints

Phone (<600dp) → Single column, bottom nav
Tablet (600-840dp) → Side panel for coach list + chat
Large Tablet (>840dp) → Three columns: coaches | chat | context

iPad is a first-class citizen. Not a stretched phone UI.

8. deployment

  • Web App, museotter.web.app (Firebase Hosting)
  • Design System, museotter-widgetbook.web.app (Widgetbook)
  • Landing Page, museotter-landing.web.app (Astro + Tailwind)

Firebase region: europe-west1 (Belgium) for Functions Firestore location: eur3 (Europe multi-region)

Summary

  • Stack, Flutter + Firebase + Genkit/Gemini. One codebase, six platforms. Built for speed and scale.
  • Architecture, Clean Architecture + Atomic Design + Feature-First. 15 Cloud Functions. Document-Status Pattern for AI.
  • RevenueCat, Firestore as source of truth. Server webhooks. Protected statuses. A/B testing paywalls with Remote Config.

Built solo in 3 weeks. Designed to scale to millions.

#Muse Otter #Architecture #Flutter #Firebase #RevenueCat #Shipyard #Clean Architecture #Genkit