Muse Otter — Technical Documentation: Stack, Architecture & RevenueCat
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
| Layer | Technology | Why I chose it |
|---|---|---|
| Frontend | Flutter 3.24+ | One codebase → iOS, Android, iPad, Web, macOS, Linux |
| State | Riverpod 2.6+ | Type-safe, testable, code-generated providers |
| Code Gen | freezed + json_serializable | Immutable entities, zero boilerplate |
| Navigation | GoRouter 14.8+ | Declarative routing with auth guards |
| Backend | Cloud Functions v2 | Auto-scaling, pay-per-use, zero ops |
| Database | Cloud Firestore | Real-time listeners, automatic sharding |
| Auth | Firebase Auth | Anonymous → account linking, zero friction |
| AI Framework | Genkit 1.27+ | Google’s AI orchestration, Gemini native |
| AI Model | Gemini 2.0 Flash | Fast, cheap, good enough for coaching |
| Payments | RevenueCat 9.10+ | Cross-platform subs in hours, not weeks |
| Voice | Vapi SDK | Real-time voice calls, simple integration |
| Analytics | Mixpanel 2.3+ | Funnels, cohorts, user properties |
| Crashes | Firebase Crashlytics | Real-time crash reporting |
| Feature Flags | Firebase Remote Config | A/B testing paywalls and onboarding |
| i18n | Slang 4.12+ | Type-safe translations, code-generated |
| Design System | Widgetbook 3.10+ | Visual component catalog |
| Icons | Heroicons | Clean, consistent icon set |
| Emoji | Google 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 becomesactive.
5. cloud functions
I have 15 active Cloud Functions that handle all backend logic:
| Function | Trigger | What it does |
|---|---|---|
onChatMessageCreate | Firestore | Generates AI response via Genkit + Gemini |
onReflectionCreate | Firestore | Extracts and condenses user context from Q&A |
onCoachCreate | Firestore | AI-generates system prompt for custom coaches |
onUserCreate | Firestore | Seeds default coaches, username, voice credits, Know You questions |
onUserDelete | Auth | Cleans up all subcollections, storage, username |
onKnowYouQuestionAnswered | Firestore | Processes answer, generates next questions |
onQuestionBatchRequestCreate | Firestore | Batch-generates Know You questions |
transcribeAudio | Firestore | Cloud Speech-to-Text for voice input |
revenueCatWebhook | HTTP | Syncs subscription status from RevenueCat |
vapiWebhook | HTTP | Handles Vapi voice session events |
checkUsernameAvailability | Callable | Username uniqueness check |
updateUsername | Callable | Atomic username update with reservation |
sendScheduledNotifications | Scheduled | Push notifications for coaching reminders |
weeklyDigestEmail | Scheduled | Weekly conversation summary email |
seedVoicePrompts | Callable | Admin: 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 ID | Type | Price | Period |
|---|---|---|---|
monthly_10 | Auto-renewable | $9.99 | Monthly |
yearly_80 | Auto-renewable | $79.99 | Yearly (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 Event | Action | New Status |
|---|---|---|
| INITIAL_PURCHASE | Grant Pro | pro |
| RENEWAL | Keep Pro | pro |
| UNCANCELLATION | Restore Pro | pro |
| EXPIRATION | Revoke Pro | free |
| CANCELLATION | No change (keeps Pro until expiry) | , |
| BILLING_ISSUE | No 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:
- Message limit reached, Free user sends message #21 → soft paywall
- Custom coach creation, Free user taps “Create Coach” → paywall
- Marketplace copy, Free user tries to copy a community coach → paywall
- 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
| Platform | Status | Notes |
|---|---|---|
| iOS 14.0+ | Production | App Store submission |
| Android SDK 24+ | Production | Play Store submission |
| iPad | Production (P0) | Three-column layout, Apple Pencil handwriting |
| Web | Production | Firebase Hosting at museotter.web.app |
| macOS | Beta | Desktop native |
| Linux | Beta | Desktop 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.