Muse Otter — Documentation Technique : Stack, Architecture & RevenueCat
Muse otter, documentation technique
RevenueCat Shipyard Creator Contest 2026 Stack, Architecture & Implémentation RevenueCat
1. Stack technique
J’ai choisi chaque brique de ce stack pour une seule raison : livrer vite, scaler après, sans compromis. En tant que développeur solo avec 3 semaines de deadline, j’avais besoin d’outils qui font le gros du travail pour moi.
Carte complète des dépendances
| Couche | Technologie | Pourquoi ce choix |
|---|---|---|
| Frontend | Flutter 3.24+ | Un seul code → iOS, Android, iPad, Web, macOS, Linux |
| State | Riverpod 2.6+ | Type-safe, testable, providers générés |
| Code Gen | freezed + json_serializable | Entités immutables, zéro boilerplate |
| Navigation | GoRouter 14.8+ | Routing déclaratif avec auth guards |
| Backend | Cloud Functions v2 | Auto-scaling, pay-per-use, zéro ops |
| Base de données | Cloud Firestore | Listeners temps réel, sharding automatique |
| Auth | Firebase Auth | Anonyme → account linking, zéro friction |
| Framework IA | Genkit 1.27+ | Orchestration IA de Google, Gemini natif |
| Modèle IA | Gemini 2.0 Flash | Rapide, économique, suffisant pour du coaching |
| Paiements | RevenueCat 9.10+ | Abonnements cross-platform en heures, pas en semaines |
| Voix | Vapi SDK | Appels vocaux temps réel, intégration simple |
| Analytics | Mixpanel 2.3+ | Funnels, cohortes, propriétés utilisateur |
| Crashes | Firebase Crashlytics | Rapport de crash temps réel |
| Feature Flags | Firebase Remote Config | A/B testing paywalls et onboarding |
| i18n | Slang 4.12+ | Traductions type-safe, code généré |
| Design System | Widgetbook 3.10+ | Catalogue visuel de composants |
2. architecture
Vue d’ensemble
┌──────────────────────────────────────────────────────────┐
│ APP FLUTTER │
│ iOS · Android · iPad · Web · macOS │
│ │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────────┐ │
│ │Présentation │ │ Domaine │ │ Données │ │
│ │ │ │ │ │ │ │
│ │ Screens │ │ Entités │ │ DTOs │ │
│ │ Widgets │──│ Use Cases │──│ Repositories │ │
│ │ Providers │ │ Interfaces │ │ Datasources │ │
│ └─────────────┘ └─────────────┘ └────────┬────────┘ │
│ │ │
│ Riverpod · GoRouter · Atomic Design │ │
└──────────────────────────────────────────────┼───────────┘
│
▼
┌──────────────────────────────────────────────────────────┐
│ BACKEND FIREBASE │
│ │
│ ┌────────────┐ ┌────────────┐ ┌────────────────────┐ │
│ │ Firestore │ │ Auth │ │ Cloud Functions v2 │ │
│ │ │ │ │ │ │ │
│ │ users/ │ │ Anonyme │ │ onChatMessage │ │
│ │ coaches/ │ │ Google │ │ onReflection │ │
│ │ chats/ │ │ Apple │ │ onCoachCreate │ │
│ │ shared/ │ │ │ │ onUserCreate │ │
│ └────────────┘ └────────────┘ │ revenueCatWebhook │ │
│ │ vapiWebhook │ │
│ └─────────┬──────────┘ │
│ │ │
│ ┌────────────┐ ┌────────────┐ ┌─────────┴──────────┐ │
│ │ Storage │ │ Remote │ │ Genkit + Gemini │ │
│ │ (audio, │ │ Config │ │ 2.0 Flash │ │
│ │ images) │ │ (flags) │ │ (réponses IA) │ │
│ └────────────┘ └────────────┘ └────────────────────┘ │
└──────────────────────────────────────────────────────────┘
│
┌──────────────┼──────────────┐
▼ ▼ ▼
┌────────────┐ ┌────────────┐ ┌────────────┐
│ RevenueCat │ │ Vapi │ │ Mixpanel │
│ Abonnement │ │ Voix │ │ Analytics │
└────────────┘ └────────────┘ └────────────┘
Clean architecture, feature-First
Chaque feature de l’app suit exactement la même structure. Pas d’exception. C’est ce qui rend un codebase solo gérable à 60+ stories.
lib/features/{feature}/
│
├── data/ ← Communique avec Firebase
│ ├── dtos/ Sérialisation des entités
│ ├── mappers/ Conversion DTO ↔ Entité
│ └── repositories/ Implémentation
│
├── domain/ ← Logique métier pure
│ ├── entities/ Immutables (freezed)
│ ├── repositories/ Interfaces abstraites
│ └── usecases/ Une action = une classe
│
└── presentation/ ← Ce que l'utilisateur voit
├── providers/ State Riverpod
├── screens/ Pages complètes
└── widgets/
├── molecules/ UI pure, pas de state
└── organisms/ Widgets composés
Règles que je suis :
- Entités immutables (freezed), jamais d’import Firebase
- Use Cases retournent
Either<Exception, T>, succès ou échec, toujours explicite - Repositories abstraits dans domain/, implémentés dans data/
- Screens sont le seul endroit avec Riverpod, navigation et orchestration
- Molecules n’importent jamais Riverpod, UI pure avec callbacks
Architecture widget, atomic design
- Molecules, Composants UI purs. Pas de Riverpod. Pas de logique. Callbacks seulement. Stories Widgetbook obligatoires.
- Organismes, Combinent les molecules. Callbacks pour les actions. Logique de layout. Composer, pas hériter.
- Screens, Providers Riverpod. Logique de navigation. Orchestration. Le seul endroit avec du state.
3. pipeline IA, le document-Status pattern
Voici comment chaque interaction IA fonctionne dans Muse Otter. Pas de WebSockets, pas de streaming complexe. Juste Firestore.
APP FLUTTER BACKEND FIREBASE
─────────── ────────────────
1. L'utilisateur envoie un message
│
▼
2. Écriture Firestore ──────────► 3. Cloud Function se déclenche
status: "pending" (onChatMessageCreate)
│ │
│ 4. Charge le contexte :
5. Affiche indicateur · Prompt système du coach
de saisie · Contexte personnel user
│ · 20 derniers messages
│ · Directive de langue
│ │
│ ▼
│ 5. Appel Genkit + Gemini
│ │
│ ▼
│ 6. Écrit la réponse assistant
7. Le listener Firestore ◄──────── status: "done"
met à jour l'UI
│
▼
8. La réponse apparaît
(perçu <500ms)
Injection de contexte, ce qui rend le coaching personnel
Chaque appel IA inclut le contexte condensé de l’utilisateur. C’est ce qui fait que le même modèle Gemini donne l’impression d’être un coach complètement différent pour chaque utilisateur.
Structure du prompt système :
┌─────────────────────────────────────────┐
│ 1. Personnalité & méthodologie du coach │
│ (scoping, questions d'abord, règles) │
├─────────────────────────────────────────┤
│ 2. Contexte personnel de l'utilisateur │
│ (projet, valeurs, objectifs, défi) │
├─────────────────────────────────────────┤
│ 3. Entrées Foundation (récentes) │
│ (notes, résumés, réflexions) │
├─────────────────────────────────────────┤
│ 4. Directive de langue │
│ (répondre dans la locale : en/fr) │
├─────────────────────────────────────────┤
│ 5. Historique conversation (20 derniers)│
└─────────────────────────────────────────┘
4. Modèle de données (Firestore)
users/{userId}
├── name, locale (en|fr), subscriptionStatus
├── messageCountToday, lastMessageDate
├── context: { summary, currentProject, values[], goals, challenge }
│
├── coaches/{coachId} ← Coaches de l'utilisateur
│ ├── 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} ← Q&A dynamiques
├── knowYouMeta/stats ← Suivi de génération
├── foundation/{entryId} ← Entrées de croissance personnelle
├── voiceCredits/credits ← Solde en secondes
└── voiceSessions/{sessionId} ← Historique des appels vocaux
sharedCoaches/{coachId} ← Marketplace (public)
├── name, emoji, description, systemPrompt
├── originalCreatorId, originalCreatorName
└── copyCount
defaultCoaches/{coachId} ← Templates de coaches (système)
usernames/{username} ← Réservation de noms d'utilisateur
Décisions de design clés
- Les coaches sont des copies, pas des références. Quand un utilisateur reçoit les coaches par défaut, ils sont copiés dans
users/{uid}/coaches/. Les utilisateurs peuvent les modifier ou les supprimer librement. - Le marketplace est séparé.
sharedCoaches/est une collection top-level indépendante des données utilisateur. Si un utilisateur supprime son compte, ses coaches publiés survivent. - Statut draft pour les coaches. Les coaches custom commencent en
draft, les utilisateurs peuvent sauvegarder leur progression et revenir plus tard. Premier message envoyé → le statut passe àactive.
5. cloud functions
J’ai 15 Cloud Functions actives qui gèrent toute la logique backend :
| Fonction | Déclencheur | Rôle |
|---|---|---|
onChatMessageCreate | Firestore | Génère la réponse IA via Genkit + Gemini |
onReflectionCreate | Firestore | Extrait et condense le contexte utilisateur |
onCoachCreate | Firestore | Génère le prompt système pour les coaches custom |
onUserCreate | Firestore | Seed coaches, username, crédits vocaux, questions Know You |
onUserDelete | Auth | Nettoie toutes les sous-collections, storage, username |
onKnowYouQuestionAnswered | Firestore | Traite la réponse, génère les questions suivantes |
transcribeAudio | Firestore | Cloud Speech-to-Text pour l’entrée vocale |
revenueCatWebhook | HTTP | Synchronise le statut d’abonnement depuis RevenueCat |
vapiWebhook | HTTP | Gère les événements de session vocale Vapi |
weeklyDigestEmail | Planifié | Email récapitulatif hebdomadaire des conversations |
sendScheduledNotifications | Planifié | Notifications push pour rappels de coaching |
Toutes les fonctions tournent en europe-west1 sur Cloud Functions v2 avec scaling automatique.
6. implémentation revenueCat
C’est la partie dont je suis le plus fier côté intégration. RevenueCat a remplacé des semaines de complexité StoreKit et Google Billing.
Produits & tarifs
| Product ID | Type | Prix | Période |
|---|---|---|---|
monthly_10 | Auto-renouvelable | 9,99$ | Mensuel |
yearly_80 | Auto-renouvelable | 79,99$ | Annuel (économie 17%) |
Entitlement : Muse Otter Pro
Offering : default avec packages mensuel + annuel
Architecture
┌───────────────┐ Achat ┌───────────────┐
│ App Flutter │──────────────►│ RevenueCat │
│ │ │ (App Store / │
│ purchases_ │ Entitle- │ Play Store) │
│ flutter SDK │◄──────────────│ │
└───────┬───────┘ ments └───────┬───────┘
│ │
│ Lire statut │ Événements webhook
│ (stream) │ (serveur-à-serveur)
▼ ▼
┌─────────────────────────────────────────────────┐
│ FIRESTORE │
│ │
│ users/{userId} │
│ ├── subscriptionStatus: free|pro|donation|lifetime │
│ └── subscriptionUpdatedAt: timestamp │
│ │
│ ═══ Source de vérité pour l'app ═══ │
└─────────────────────────────────────────────────┘
Implémentation côté client
L’app utilise Firestore comme source unique de vérité pour le statut d’abonnement. Le SDK RevenueCat gère les achats, mais le statut vient de Firestore, c’est simple et fiable.
Providers clés :
isProProvider
└── Lit user.subscriptionStatus depuis le stream Firestore
└── Retourne true si status != 'free'
ProGateWidget
└── Wrappe n'importe quelle UI Pro-only
└── Affiche un prompt d'upgrade si pas Pro
ProRequiredScreen
└── Wrappe des écrans entiers
└── Redirige vers /paywall si pas Pro
Webhook côté serveur
La Cloud Function revenueCatWebhook est un endpoint HTTP que RevenueCat appelle à chaque événement d’abonnement :
| Événement RevenueCat | Action | Nouveau statut |
|---|---|---|
| INITIAL_PURCHASE | Accorder Pro | pro |
| RENEWAL | Garder Pro | pro |
| UNCANCELLATION | Restaurer Pro | pro |
| EXPIRATION | Révoquer Pro | free |
| CANCELLATION | Pas de changement (garde Pro jusqu’à expiration) | , |
| BILLING_ISSUE | Pas de changement (période de grâce) | , |
Statuts protégés : donation et lifetime ne sont jamais modifiés par le webhook. Ils ne peuvent être définis que manuellement dans Firestore, ça me permet d’accorder un accès gratuit aux testeurs, juges du concours ou early supporters sans que RevenueCat interfère.
Déclencheurs de paywall
Le paywall apparaît naturellement aux bons moments :
- Limite de messages atteinte, L’utilisateur free envoie le message #21 → soft paywall
- Création de coach custom, L’utilisateur free tape “Créer un Coach” → paywall
- Copie marketplace, L’utilisateur free essaie de copier un coach communautaire → paywall
- Paramètres, Bouton “Passer à Pro” toujours visible
Chaque déclencheur est tracké dans Mixpanel avec l’événement paywallViewed + propriété trigger pour l’analyse de conversion.
A/B testing avec remote config
J’utilise Firebase Remote Config pour expérimenter :
- Variantes de design du paywall
- Limite de messages gratuits (actuellement 20/jour)
- Flow d’onboarding (avec ou sans Know You)
- Quelles questions Know You montrer en premier
7. support multi-plateforme & design responsive
| Plateforme | Statut | Notes |
|---|---|---|
| iOS 14.0+ | Production | Soumission App Store |
| Android SDK 24+ | Production | Soumission Play Store |
| iPad | Production (P0) | Layout 3 colonnes, écriture Apple Pencil |
| Web | Production | Firebase Hosting : museotter.web.app |
| macOS | Beta | Desktop natif |
| Linux | Beta | Desktop natif |
Points de rupture responsive
Téléphone (<600dp) → Colonne unique, nav en bas
Tablette (600-840dp) → Panel latéral pour la liste des coaches + chat
Grande tablette (>840dp) → Trois colonnes : coaches | chat | contexte
L’iPad est un citoyen de première classe. Pas une UI de téléphone étirée.
8. déploiement
- App Web, museotter.web.app (Firebase Hosting)
- Design System, museotter-widgetbook.web.app (Widgetbook)
- Landing Page, museotter-landing.web.app (Astro + Tailwind)
Région Firebase : europe-west1 (Belgique) pour les Functions
Localisation Firestore : eur3 (Europe multi-région)
Résumé
- Stack, Flutter + Firebase + Genkit/Gemini. Un seul codebase, six plateformes. Construit pour la vitesse et le scale.
- Architecture, Clean Architecture + Atomic Design + Feature-First. 15 Cloud Functions. Document-Status Pattern pour l’IA.
- RevenueCat, Firestore comme source de vérité. Webhooks serveur. Statuts protégés. A/B testing paywalls avec Remote Config.
Construit solo en 3 semaines. Conçu pour scaler à des millions.