Retour au blog
flutter

Muse Otter — Documentation Technique : Stack, Architecture & RevenueCat

Par Youcef EL KAMEL
18 min de lecture

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

CoucheTechnologiePourquoi ce choix
FrontendFlutter 3.24+Un seul code → iOS, Android, iPad, Web, macOS, Linux
StateRiverpod 2.6+Type-safe, testable, providers générés
Code Genfreezed + json_serializableEntités immutables, zéro boilerplate
NavigationGoRouter 14.8+Routing déclaratif avec auth guards
BackendCloud Functions v2Auto-scaling, pay-per-use, zéro ops
Base de donnéesCloud FirestoreListeners temps réel, sharding automatique
AuthFirebase AuthAnonyme → account linking, zéro friction
Framework IAGenkit 1.27+Orchestration IA de Google, Gemini natif
Modèle IAGemini 2.0 FlashRapide, économique, suffisant pour du coaching
PaiementsRevenueCat 9.10+Abonnements cross-platform en heures, pas en semaines
VoixVapi SDKAppels vocaux temps réel, intégration simple
AnalyticsMixpanel 2.3+Funnels, cohortes, propriétés utilisateur
CrashesFirebase CrashlyticsRapport de crash temps réel
Feature FlagsFirebase Remote ConfigA/B testing paywalls et onboarding
i18nSlang 4.12+Traductions type-safe, code généré
Design SystemWidgetbook 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 :

FonctionDéclencheurRôle
onChatMessageCreateFirestoreGénère la réponse IA via Genkit + Gemini
onReflectionCreateFirestoreExtrait et condense le contexte utilisateur
onCoachCreateFirestoreGénère le prompt système pour les coaches custom
onUserCreateFirestoreSeed coaches, username, crédits vocaux, questions Know You
onUserDeleteAuthNettoie toutes les sous-collections, storage, username
onKnowYouQuestionAnsweredFirestoreTraite la réponse, génère les questions suivantes
transcribeAudioFirestoreCloud Speech-to-Text pour l’entrée vocale
revenueCatWebhookHTTPSynchronise le statut d’abonnement depuis RevenueCat
vapiWebhookHTTPGère les événements de session vocale Vapi
weeklyDigestEmailPlanifiéEmail récapitulatif hebdomadaire des conversations
sendScheduledNotificationsPlanifié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 IDTypePrixPériode
monthly_10Auto-renouvelable9,99$Mensuel
yearly_80Auto-renouvelable79,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 RevenueCatActionNouveau statut
INITIAL_PURCHASEAccorder Propro
RENEWALGarder Propro
UNCANCELLATIONRestaurer Propro
EXPIRATIONRévoquer Profree
CANCELLATIONPas de changement (garde Pro jusqu’à expiration),
BILLING_ISSUEPas 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 :

  1. Limite de messages atteinte, L’utilisateur free envoie le message #21 → soft paywall
  2. Création de coach custom, L’utilisateur free tape “Créer un Coach” → paywall
  3. Copie marketplace, L’utilisateur free essaie de copier un coach communautaire → paywall
  4. 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

PlateformeStatutNotes
iOS 14.0+ProductionSoumission App Store
Android SDK 24+ProductionSoumission Play Store
iPadProduction (P0)Layout 3 colonnes, écriture Apple Pencil
WebProductionFirebase Hosting : museotter.web.app
macOSBetaDesktop natif
LinuxBetaDesktop 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.

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