# Memento Note — Préparation Déploiement SaaS > **Version:** 1.0 | **Date:** 2026-05-14 | **Statut:** Draft > **Auteur:** Mary (Business Analyst) --- ## 1. Vision & Proposition de Valeur ### 1.1 Vision Produit **Memento Note** est une application de prise de notes intelligente alimentée par l'IA, offrant recherche sémantique, suggestions automatiques, et organisation cognitive. L'objectif SaaS est de démocratiser l'accès à une mémoire numérique augmentée par IA — du particulier autodidacte au département R&D enterprise. ### 1.2 Proposition de Valeur par Tier | Tier | Prix | Positionnement | Proposition Unique | |------|------|----------------|-------------------| | **Basic** | Gratuit | Particuliers, étudiants | "Ma première mémoire numérique" — Capturer sans friction, rechercher sans IA | | **Pro** | 9,90 €/mois | Professionnels, créateurs | "Pensée amplifiée par l'IA" — Accès complet à l'IA avec limites raisonables | | **Business** | 29,90 €/mois | Équipes, entreprises | "Mémoire organisationnelle partagée" — Collaboration, analytics, limites élevées | ### 1.3 Promotional Hook (PRFAQ style) > **If I had a penny for every note I lost track of...** > > Every knowledge worker knows the frustration: the brilliant idea you jotted down at 11pm, the client insight buried in a 3-year-old doc, the connection between two notes you'd never think to link. > > Memento Note doesn't just store your notes. It understands them. It surfaces connections you didn't know existed. It remembers what you forgot. > > **Starting at free.** --- ## 2. Modélisation des Tiers d'Abonnement ### 2.1 Tableau Comparatif des Features | Feature | Basic (Gratuit) | Pro (9,90 €/mois) | Business (29,90 €/mois) | |---------|:--------------:|:----------------:|:----------------------:| | **Notes** | 100 max | Illimitées | Illimitées | | **Notebooks** | 3 | Illimités | Illimités | | **Collaborateurs** | 0 | 0 | 10 included | | **AI — Titres auto** | ❌ | ✅ | ✅ | | **AI — Tags auto** | ❌ | ✅ | ✅ | | **AI — Recherche sémantique** | ❌ | ✅ (100 req/mois) | ✅ (1000 req/mois) | | **AI — Reformulation** | ❌ | ✅ (50 req/mois) | ✅ (500 req/mois) | | **AI — Résumé notebook** | ❌ | ❌ | ✅ | | **AI — Echo Memory** | ❌ | ❌ | ✅ | | **AI — Agents web scraper** | ❌ | ❌ | ✅ | | **Brainstorm collaboratif** | ❌ | ✅ | ✅ | | **API Access** | ❌ | ❌ | ✅ | | **Historique & versionning** | 7 jours | 30 jours | Illimité | | **Export** | PDF | PDF + Markdown | PDF + Markdown + JSON | | **Canevas Excalidraw** | ✅ | ✅ | ✅ | | **Support** | Communauté | Email | Priority + SLA 24h | ### 2.2 Quotas IA — Détail | Service AI | Basic | Pro (mensuel) | Business (mensuel) | |------------|:-----:|:-------------:|:-----------------:| | Titre auto | 0 | 200 | 1000 | | Tags auto | 0 | 200 | 1000 | | Recherche sémantique | 0 | 100 | 1000 | | Reformulation texte | 0 | 50 | 500 | | Résumé notebook | 0 | 0 | 50 | | Echo Memory | 0 | 0 | 100 | | Agent Web Scraper | 0 | 0 | 20 runs | | Chat AI | 0 | 100 | 1000 | | **Total token estimate** | N/A | ~500K tokens | ~5M tokens | ### 2.3 Période d'Essai | Tier | Trial | |------|-------| | Pro | 14 jours gratuits, sans carte | | Business | Pas de trial (tier entry élevé) | --- ## 3. Architecture Nécessaire ### 3.1 Vue d'Ensemble des Changements ``` ┌─────────────────────────────────────────────────────────────────┐ │ PRESENTATION LAYER │ │ ┌─────────────┐ ┌─────────────┐ ┌─────────────────────────┐ │ │ │ UI Upgrade │ │ Paywall │ │ Usage Meter / Quota UI │ │ │ │ Tier Badge │ │ Upgrade │ │ (Pro: X/200 tags auto) │ │ │ └─────────────┘ └─────────────┘ └─────────────────────────┘ │ └─────────────────────────────────────────────────────────────────┘ │ ▼ ┌─────────────────────────────────────────────────────────────────┐ │ API LAYER │ │ ┌─────────────┐ ┌─────────────┐ ┌─────────────────────────┐ │ │ │ Billing │ │ Usage │ │ Entitlement Check │ │ │ │ Stripe │ │ Tracker │ │ (canUseFeature()) │ │ │ └─────────────┘ └─────────────┘ └─────────────────────────┘ │ │ ┌─────────────┐ ┌─────────────┐ ┌─────────────────────────┐ │ │ │ Subscription│ │ Rate Limit │ │ Analytics Events │ │ │ │ Webhook │ │ Per Tier │ │ (trackFeatureUsage()) │ │ │ └─────────────┘ └─────────────┘ └─────────────────────────┘ │ └─────────────────────────────────────────────────────────────────┘ │ ▼ ┌─────────────────────────────────────────────────────────────────┐ │ DATA LAYER │ │ ┌─────────────┐ ┌─────────────┐ ┌─────────────────────────┐ │ │ │ Subscription│ │ UsageLog │ │ TenantId / Workspace │ │ │ │ Model │ │ (per user) │ │ (future multi-tenancy) │ │ │ └─────────────┘ └─────────────┘ └─────────────────────────┘ │ └─────────────────────────────────────────────────────────────────┘ ``` ### 3.2 Nouveau Schéma Base de Données (Prisma) ```prisma // ===== SUBSCRIPTION MODELS ===== enum SubscriptionTier { BASIC // Free PRO // 9,90 €/mois BUSINESS // 29,90 €/mois } enum SubscriptionStatus { ACTIVE PAST_DUE CANCELED TRIALING INACTIVE } model Subscription { id String @id @default(cuid()) userId String @unique tier SubscriptionTier @default(BASIC) status SubscriptionStatus @default(ACTIVE) // Stripe integration stripeCustomerId String? @unique stripeSubscriptionId String? @unique stripePriceId String? // Trial trialEndsAt DateTime? // Billing currentPeriodStart DateTime currentPeriodEnd DateTime // Cancellations canceledAt DateTime? cancelAtPeriodEnd Boolean @default(false) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt user User @relation(fields: [userId], references: [id], onDelete: Cascade) } model UsageLog { id String @id @default(cuid()) userId String feature String // e.g., "semantic_search", "auto_tag", "chat" tokensUsed Int @default(0) requestsCount Int @default(1) periodStart DateTime // Monthly reset periodEnd DateTime metadata Json? // Extra context (noteId, etc.) createdAt DateTime @default(now()) user User @relation(fields: [userId], references: [id], onDelete: Cascade) @@index([userId, feature, periodStart]) @@index([userId, periodStart]) } model FeatureFlag { id String @id @default(cuid()) key String @unique enabled Boolean @default(false) tiers SubscriptionTier[] // Which tiers have access metadata Json? // Rollout percentage, etc. updatedAt DateTime @updatedAt } // ===== ENHANCED USER MODEL ===== model User { // ... existing fields ... // NEW: Subscription subscription Subscription? // NEW: Workspace (for future multi-tenancy / Business tier collaboration) workspaceId String? workspace Workspace? @relation(fields: [workspaceId], references: [id]) // NEW: Usage tracking usageLogs UsageLog[] // NEW: Preferred language for AI responses aiLanguage String @default("en") } model Workspace { id String @id @default(cuid()) name String plan SubscriptionTier @default(BASIC) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt members User[] // Future: workspace-level settings, analytics, billing } // ===== ENHANCED EXISTING MODELS ===== model Note { // ... existing fields ... // NEW: AI usage tracking at note level aiTokensUsed Int @default(0) lastAiFeature String? // Last AI feature used } ``` ### 3.3 Endpoints API à Ajouter | Endpoint | Méthode | Purpose | Tier | |----------|---------|---------|------| | `/api/billing/create-checkout` | POST | Initier checkout Stripe | Pro+ | | `/api/billing/portal` | POST | Customer portal Stripe | Pro+ | | `/api/billing/webhook` | POST | Stripe webhook handler | All | | `/api/billing/cancel` | POST | Annuler abonnement | Pro+ | | `/api/billing/upgrade` | POST | Upgrade tier | Basic→Pro | | `/api/usage/current` | GET | Quotas actuels utilisateur | All | | `/api/usage/history` | GET | Historique usage | Pro+ | | `/api/entitlements` | GET | Features accessibles pour tier | All | ### 3.4 Fonctions d'Entitlement Core ```typescript // lib/entitlements.ts const TIER_LIMITS = { BASIC: { maxNotes: 100, maxNotebooks: 3, collaborators: 0, aiRequestsPerMonth: 0, // No AI historyDays: 7, }, PRO: { maxNotes: Infinity, maxNotebooks: Infinity, collaborators: 0, aiRequestsPerMonth: { autoTitle: 200, autoTag: 200, semanticSearch: 100, reformulate: 50, chat: 100, }, historyDays: 30, }, BUSINESS: { maxNotes: Infinity, maxNotebooks: Infinity, collaborators: 10, aiRequestsPerMonth: { autoTitle: 1000, autoTag: 1000, semanticSearch: 1000, reformulate: 500, notebookSummary: 50, memoryEcho: 100, webScraperAgent: 20, chat: 1000, }, historyDays: Infinity, }, } as const; export function canUseFeature(userId: string, feature: string): Promise { // 1. Check subscription tier // 2. Check usage within limits // 3. Return boolean } export async function trackFeatureUsage( userId: string, feature: string, tokensUsed: number, metadata?: Record ): Promise { // Increment UsageLog for current period } export async function getRemainingQuota( userId: string, feature: string ): Promise<{ remaining: number; total: number; resetsAt: Date }> { // Return remaining quota for feature } ``` --- ## 4. Modèle de Monétisation ### 4.1 Stripe Integration ``` Pricing Table Stripe Products: ├── Memento Basic (Free) → No Stripe, free tier ├── Memento Pro (9,90 €/mois) → Stripe Price: monthly + annual ├── Memento Business (29,90 €/mois) → Stripe Price: monthly + annual └── Add-ons (future): ├── +5 Collaborators → Seat-based └── +AI Pack (extra tokens) → Usage-based ``` ### 4.2 Stratégie de Conversion ``` Funnel de Conversion: [Basic Free] ──────────────────────────────────────────► [Churn) │ │ 14-day trial offer │ in-app prompt ▼ [Upgrade to Pro] │ │ Team needs │ Advanced features ▼ [Upgrade to Business] ``` **Touchpoints de Conversion:** 1. **Inline upgrade prompt** — Quand utilisateur hit AI limit (Basic tries AI) 2. **Usage meter in UI** — Barres de progression quota (Pro/Business) 3. **Upgrade modal** — Au clic sur feature payante depuis Basic 4. **Trial banner** — Pour Basic users, "Essayez Pro 14 jours" ### 4.3 Rétention & Engagement | Tactique | Description | |----------|-------------| | Onboarding optimisé | 3-step wizard: Capture → Search → AI Discover | | Usage-based email | "Vous avez utilisé X feature Y fois ce mois" | |milestone celebrate | "100 notes créées! Débloquez Pro pour l'IA" | | Health dashboard | "Votre score mémoire: 87/100" | --- ## 5. Métriques & Analytics pour SaaS ### 5.1 Métriques à Implémenter #### A. Product Analytics (qui ne coûte pas cher) | Métrique | Description | Outil | |---------|-------------|-------| | DAU/WAU/MAU | Active users daily/weekly/monthly | DB query | | Feature Adoption | % users using each AI feature | UsageLog | | Activation Rate | % new users who create 1st note | events | | Retention Curve | Week 1, 2, 4, 8 retention | events | | Conversion Rate | Free → Pro → Business | Subscription | #### B. Usage Metrics (par tier) ```typescript // Types pour analytics interface SaaSMetrics { // Growth newUsersToday: number; newUsersThisMonth: number; churnedUsersThisMonth: number; // Revenue mrr: number; // Monthly Recurring Revenue arr: number; // Annual Run Rate arpu: number; // Average Revenue Per User // Usage totalAiRequests: number; avgAiRequestsPerUser: number; topFeatures: { feature: string; count: number }[]; // Engagement medianNotesPerUser: number; medianNotebooksPerUser: number; activeUsersLast30Days: number; } ``` #### C. Health Dashboard (Admin) ``` ┌─────────────────────────────────────────────────────────┐ │ SaaS Health │ ├──────────────┬──────────────┬──────────────┬────────────┤ │ Total Users │ Pro Users │ Business │ Churn % │ │ 1,247 │ 89 │ 12 │ 2.3% │ ├──────────────┴──────────────┴──────────────┴────────────┤ │ MRR: 1,040 € ARR: 12,480 € ARPU: 9,74 € │ ├───────────────────────────────────────────────────────-─┤ │ Feature Adoption (last 30 days) │ │ ┌──────────────────────────────────────────────────┐ │ │ │ Auto-Tag ████████████████████░░░░░ 78% │ │ │ │ Semantic ████████████████░░░░░░░░░ 62% │ │ │ │ Reformulate ████████████░░░░░░░░░░░░░ 44% │ │ │ │ Chat ████████░░░░░░░░░░░░░░░░░░ 31% │ │ │ └──────────────────────────────────────────────────┘ │ └─────────────────────────────────────────────────────────┘ ``` ### 5.2 Tracking Événements ```typescript // lib/analytics/track.ts type EventName = | 'note_created' | 'note_edited' | 'ai_feature_used' | 'ai_feature_limit_hit' | 'upgrade_intent' | 'upgrade_completed' | 'downgrade_completed' | 'subscription_canceled' | 'trial_started' | 'trial_converted' | 'export_triggered' | 'brainstorm_created'; interface TrackPayload { userId: string; tier: SubscriptionTier; timestamp: Date; metadata?: Record; } // Usage: // track('ai_feature_used', { feature: 'auto_tag', tokensUsed: 150 }) ``` --- ## 6. Checklist de Déploiement ### Phase 1: Fondations (Week 1-2) - [ ] Ajouter `Subscription` model to Prisma schema - [ ] Ajouter `UsageLog` model to Prisma schema - [ ] Ajouter `FeatureFlag` model to Prisma schema - [ ] Créer `lib/entitlements.ts` — core entitlement logic - [ ] Créer `lib/usage-tracker.ts` — track AI feature usage - [ ] Créer migration Prisma - [ ] Ajouter `SubscriptionTier` field to User model - [ ] Mettre à jour `auth.ts` pour exposer le tier dans la session ### Phase 2: API Billing (Week 2-3) - [ ] Intégrer Stripe SDK - [ ] Créer `/api/billing/create-checkout` endpoint - [ ] Créer `/api/billing/portal` endpoint - [ ] Créer `/api/billing/webhook` handler (subscription events) - [ ] Tester Stripe webhook en local (stripe-cli) - [ ] Configurer produits/tarifs dans Stripe Dashboard ### Phase 3: UI Paywall & Quotas (Week 3-4) - [ ] Créer `` component - [ ] Créer `` component (progress bars) - [ ] Intégrer PaywallModal dans AI feature calls - [ ] Afficher remaining quotas dans sidebar/header - [ ] Ajouter "Upgrade" CTA sur les features Basic - [ ] Créer page `/settings/billing` (manage subscription) ### Phase 4: Analytics & Metrics (Week 4-5) - [ ] Créer `lib/analytics/track.ts` event tracking - [ ] Ajouter analytics calls dans les actions clés - [ ] Construire `` avec vrais metrics - [ ] Créer `` pour users (self-service) - [ ] Implémenter DAU/WAU/MAU queries - [ ] Dashboard churn tracking ### Phase 5: Trial & Conversion (Week 5-6) - [ ] Implémenter trial flow (14 jours Pro) - [ ] Email trial reminder (day 7, day 12) - [ ] Trial-to-paid conversion tracking - [ ] Optimiser upgrade flow (reduce friction) - [ ] A/B test upgrade prompts ### Phase 6: Multi-tenancy Preparation (Week 6-8) - [ ] Ajouter `Workspace` model - [ ] Créer workspace settings UI - [ ] Implémenter invite/collaboration pour Business - [ ] Workspace-level analytics - [ ] SSO/OAuth pour Business (future) --- ## 7. Risques & Mitigations | Risque | Probabilité | Impact | Mitigation | |--------|------------|--------|------------| | Stripe webhook failures | Moyenne | Élevé | Retry queue + email alerts | | AI provider costs explode | Haute | Élevé | Hard limits per tier + alerts | | Users sharing accounts | Moyenne | Moyen | Device tracking + limit concurrent sessions | | Churn élevé sur trial | Haute | Moyen | Better onboarding + email sequence | | Complexity explosion | Haute | Moyen | Phased rollout, v1.0 = scope minimal | --- ## 8. Estimation d'Effort | Phase | Complexity | Risk | Est. Time | |-------|------------|------|-----------| | Phase 1: Fondations | ⭐⭐ | 🟡 | 2-3 days | | Phase 2: Stripe | ⭐⭐⭐ | 🟡 | 2-3 days | | Phase 3: UI Paywall | ⭐⭐⭐ | 🟢 | 3-4 days | | Phase 4: Analytics | ⭐⭐⭐ | 🟡 | 2-3 days | | Phase 5: Trial | ⭐⭐ | 🟢 | 2 days | | Phase 6: Multi-tenancy | ⭐⭐⭐⭐ | 🔴 | 1-2 weeks | **Total estimate: 3-4 weeks full-time developer** --- ## Annexes ### A. Fichiers à Modifier ``` Modified Files: ├── prisma/schema.prisma # Add Subscription, UsageLog, Workspace ├── lib/entitlements.ts # NEW — entitlement logic ├── lib/usage-tracker.ts # NEW — usage tracking ├── lib/analytics/track.ts # NEW — event tracking ├── lib/stripe.ts # NEW — Stripe client ├── app/api/billing/ # NEW — billing endpoints ├── app/api/usage/current/route.ts # NEW ├── app/api/entitlements/route.ts # NEW ├── app/api/webhook/stripe/route.ts # NEW ├── auth.ts # Expose subscription tier in session ├── middleware.ts # Add tier-based route protection ├── components/paywall-modal.tsx # NEW ├── components/usage-meter.tsx # NEW ├── components/admin-dashboard.tsx # Upgrade for real metrics ├── app/(admin)/admin/page.tsx # Real metrics integration └── app/(settings)/billing/page.tsx # NEW — billing management ``` ### B. Variables d'Environnement Requises ```env # Stripe STRIPE_SECRET_KEY=sk_live_... STRIPE_WEBHOOK_SECRET=whsec_... STRIPE_PRICE_PRO_MONTHLY=price_... # €9.90/mois STRIPE_PRICE_PRO_ANNUAL=price_... # ~€99/an (2 mois gratuits) STRIPE_PRICE_BUSINESS_MONTHLY=price_... # €29.90/mois STRIPE_PRICE_BUSINESS_ANNUAL=price_... # ~€299/an (2 mois gratuits) STRIPE_PRICE_ENTERPRISE=price_... # Custom seat-based # Dual-currency: les prix USD sont définis dans Stripe Dashboard # et affichés selon la locale de l'utilisateur NEXT_PUBLIC_DEFAULT_CURRENCY=EUR NEXT_PUBLIC_SUPPORTED_CURRENCIES=EUR,USD # Feature flags NEXT_PUBLIC_FEATURE_TRIAL_ENABLED=true NEXT_PUBLIC_FEATURE_BILLING_ENABLED=true ``` ### D. Addons & Enterprise Tier #### Addon: Collaborateurs Supplémentaires (Business+) | Addon | Prix | |-------|------| | +5 collaborateurs | +4,90 €/mois | | +10 collaborateurs | +8,90 €/mois | *Les collaborateurs additionnels sont des "member seats" — ils ont leur propre compte Memento mais appartiennent au même workspace Business.* #### Enterprise Tier — Proposition Pour les équipes de **20+ utilisateurs** avec besoins avancés. | Élément | Détail | |---------|--------| | **Prix** | 49,90 €/mois fixes + 3,90 €/utilisateur/mois | | **Exemple** | 20 users = 49,90 + (20 × 3,90) = 127,90 €/mois | | **Minimum** | 20 users minimum facturés | | **Maximum** | Soft cap 500 users, au-delà = pricing custom | | Feature Enterprise | Détail | |-------------------|--------| | **Tout Business** | Inclus | | **SSO/SAML** | Okta, Azure AD, Google Workspace | | **Audit Logs** | Accès complet aux logs d'activité équipe | | **API illimitée** | Rate limit +10x Business | | **SLA** | 99.9% uptime + support dédié | | **Onboarding** | Session onboarding live incluse | | **Custom contracts** | Invoice net-30, BPA available | | **White-label** | Logo + couleurs custom (future) | | **Data residency** | EU-only data storage | **Migration path:** Business → Enterprise est seamless (ajustement facturation, pas de migration data). --- ### E. Tableau Comparatif Complet (4 Tiers) | Feature | Basic | Pro | Business | Enterprise | |---------|------:|----:|-----------|-----------:| | **Prix** | Gratuit | 9,90 €/mois | 29,90 €/mois | 49,90 + 3,90 €/user | | **Notes** | 100 max | Illimitées | Illimitées | Illimitées | | **Notebooks** | 3 | Illimités | Illimités | Illimités | | **Collaborateurs** | 0 | 0 | 10 | 20+ | | **AI Titres auto** | ❌ | 200/mois | 1000/mois | 5000/mois | | **AI Tags auto** | ❌ | 200/mois | 1000/mois | 5000/mois | | **Recherche sémantique** | ❌ | 100/mois | 1000/mois | Illimitée | | **Reformulation** | ❌ | 50/mois | 500/mois | 2000/mois | | **Résumé notebook** | ❌ | ❌ | 50/mois | 200/mois | | **Echo Memory** | ❌ | ❌ | 100/mois | Illimité | | **Agent Web Scraper** | ❌ | ❌ | 20 runs | 100 runs | | **Chat AI** | ❌ | 100/mois | 1000/mois | Illimité | | **Brainstorm collaboratif** | ❌ | ✅ | ✅ | ✅ | | **API Access** | ❌ | ❌ | ✅ | ✅ (illimitée) | | **Historique** | 7 jours | 30 jours | Illimité | Illimité | | **Export formats** | PDF | PDF+MD | PDF+MD+JSON | Tous + API | | **Support** | Commu | Email | Priority 24h | Dédié + SLA | | **SSO** | ❌ | ❌ | ❌ | ✅ | | **Audit Logs** | ❌ | ❌ | ❌ | ✅ | | **SLA** | ❌ | ❌ | ❌ | 99.9% | --- ## 9. V3 — Zéro Coût Additionnel (Staff Engineer Review) > **Relecture par Mary (Staff Engineer / Product Growth)** | _Optimisations PLG + B2B — Version Gratuite_ > > **Changements:** Redis self-hosted (pas Upstash) | Umami auto-hébergé (pas PostHog) | Option PostgreSQL UPSERT pour les quotas --- ### A. Nouvelle Stratégie Freemium — "AI Discovery Pack" **Problème V1:** Basic avec 0 IA = pas d'effet "Aha!". L'utilisateur ne peut jamais tester la magie du produit. **Solution V2:** "Starter Pack" — un quota de découverte à vie pour initier les utilisateurs à la valeur IA sans les forcer à payer. | Feature | Basic Starter | Pro | Business | Enterprise | |---------|:-------------:|----:|-----------|-----------:| | **Prix** | Gratuit | 9,90 €/mois | 29,90 €/mois | 49,90 + 3,90 €/user | | **Notes** | 100 max | Illimitées | Illimitées | Illimitées | | **Notebooks** | 3 | Illimités | Illimités | Illimités | | **Collaborateurs** | 0 | 0 | 10 | 20+ | | **AI Starter Pack** | **✅** | ✅ | ✅ | ✅ | | — Recherche sémantique | **30 req/mois (lifetime)** | 100/mois | 1000/mois | Illimitée | | — Tags auto | **20 req/mois (lifetime)** | 200/mois | 1000/mois | 5000/mois | | — Titres auto | **10 req/mois (lifetime)** | 200/mois | 1000/mois | 5000/mois | | **AI Reformulation** | ❌ | 50/mois | 500/mois | 2000/mois | | **AI Résumé notebook** | ❌ | ❌ | 50/mois | 200/mois | | **AI Echo Memory** | ❌ | ❌ | 100/mois | Illimité | | **AI Chat** | ❌ | 100/mois | 1000/mois | Illimité | | **Brainstorm collaboratif** | ❌ | ✅ | ✅ | ✅ | | **API Access** | ❌ | ❌ | ✅ | ✅ (illimitée) | | **Historique** | 7 jours | 30 jours | Illimité | Illimité | | **Support** | Commu | Email | Priority 24h | Dédié + SLA | | **SSO** | ❌ | ❌ | ❌ | ✅ | | **Audit Logs** | ❌ | ❌ | ❌ | ✅ | | **SLA** | ❌ | ❌ | ❌ | 99.9% | **Pourquoi ce changement:** - 30 requêtes sémantiques = ~1 semaine d'usage intensif ou 1 mois modéré — assez pour comprendre la valeur - L'effet "Aha!" arrive typiquement à la 3ème-5ème recherche sémantique (quand tu retrouves un vieux note enfoui) - Le "lifetime" supprime l'anxiété du quota mensuel — "je garde mes crédits même si je ne souscris pas" **Copy pour l'onboarding:** > "Vous avez accès à 30 recherches sémantiques gratuites — à vie. Découvrez comment l'IA retrouve vos idées enfouies." **Conversion trigger:** Une fois le Starter Pack épuisé, l'utilisateur a **vu la valeur** et comprend ce qu'il perd. Le paywall devient une évidence, pas une frustration. --- ### B. Architecture Quotas Haute Performance — Redis + PostgreSQL Hybride **Problème V1:** Écrire dans `UsageLog` à chaque requête IA = écriture DB intensive. À 10 000 utilisateurs actifs × 100 req/jour = 1M writes/jour sur PostgreSQL. Ça fond le serveur en pic. #### B.1 Vue d'Ensemble de l'Architecture ``` ┌─────────────────────────────────────────────────────────────────────┐ │ AI Request Flow │ │ │ │ User → Next.js API → canUseFeature() [Redis counter check] │ │ │ │ │ ├── [quota OK] → AI Service → trackUsage() │ │ │ │ │ │ │ Redis INCRBY │ │ │ (async, fire-and- │ │ │ forget) │ │ │ │ │ └── [quota EXCEEDED] → 429 + PaywallModal │ │ │ │ ─── CRON toutes les 5 min ─────────────────────────────────────── │ │ │ │ │ │ SyncWorker: Lis les compteurs Redis │ │ │ → Écrit les增量 (delta) dans PostgreSQL UsageLog │ │ │ → Réinitialise les compteurs Redis si nouveau mois │ │ │ │ │ ───────────────────────────────────────────────────────────────── │ └─────────────────────────────────────────────────────────────────────┘ ``` #### B.2 Redis Schema (Self-Hosted) ```typescript // Clés Redis par user et par feature // Format: usage:{userId}:{feature}:{YYYY-MM} // Option: PostgreSQL UPSERT (plus simple, $0, voir Section B.3) // Exemples: // usage:user123:semantic_search:2026-05 → Counter (INCRBY) // usage:user123:auto_tag:2026-05 → Counter (INCRBY) // usage:user123:auto_title:2026-05 → Counter (INCRBY) // Workspace-level pour Business: // workspace:{workspaceId}:semantic_search:2026-05 → Counter // TTL: 90 jours (auto-cleanup, covers grace period) ``` #### B.3 Service d'Entitlement Hybride ```typescript // lib/entitlements.ts import { Redis } from '@upstash/redis'; // Redis client (côté server, pas Next.js route) const redis = new Redis({ url: process.env.UPSTASH_REDIS_REST_URL!, token: process.env.UPSTASH_REDIS_REST_TOKEN!, }); const TIER_LIMITS = { BASIC: { maxNotes: 100, maxNotebooks: 3, collaborators: 0, aiStarterPack: { semanticSearch: 30, autoTag: 20, autoTitle: 10 }, historyDays: 7, }, PRO: { maxNotes: Infinity, maxNotebooks: Infinity, collaborators: 0, aiRequestsPerMonth: { autoTitle: 200, autoTag: 200, semanticSearch: 100, reformulate: 50, chat: 100 }, historyDays: 30, }, BUSINESS: { maxNotes: Infinity, maxNotebooks: Infinity, collaborators: 10, aiRequestsPerMonth: { autoTitle: 1000, autoTag: 1000, semanticSearch: 1000, reformulate: 500, notebookSummary: 50, memoryEcho: 100, webScraperAgent: 20, chat: 1000 }, historyDays: Infinity, }, ENTERPRISE: { maxNotes: Infinity, maxNotebooks: Infinity, collaborators: Infinity, aiRequestsPerMonth: { autoTitle: 5000, autoTag: 5000, semanticSearch: Infinity, reformulate: 2000, notebookSummary: 200, memoryEcho: Infinity, webScraperAgent: 100, chat: Infinity }, historyDays: Infinity, }, } as const; // READ: fast path — Redis only export async function canUseFeature(userId: string, feature: string): Promise<{ allowed: boolean; remaining: number; limit: number }> { const user = await getUserSubscription(userId); const tier = user.subscription?.tier ?? 'BASIC'; const limit = getLimit(tier, feature); const key = `usage:${userId}:${feature}:${getCurrentPeriodKey()}`; const [currentStr, maxStr] = await Promise.all([ redis.get(key), limit === Infinity ? Promise.resolve('Infinity') : Promise.resolve(String(limit)), ]); const current = parseInt(currentStr ?? '0'); const max = maxStr === 'Infinity' ? Infinity : parseInt(maxStr); if (max === Infinity) return { allowed: true, remaining: Infinity, limit: Infinity }; return { allowed: current < max, remaining: Math.max(0, max - current), limit: max, }; } // WRITE: fire-and-forget, ne bloque pas la requête IA export async function trackFeatureUsage( userId: string, feature: string, tokensUsed: number, metadata?: Record ): Promise { const key = `usage:${userId}:${feature}:${getCurrentPeriodKey()}`; // Piped transaction — atomic increment sans read-modify-write redis.pipeline() .incrby(key, 1) .incrbyfloat(`${key}:tokens`, tokensUsed) .expire(key, 90 * 24 * 60 * 60) // 90 days TTL .exec(); } function getCurrentPeriodKey(): string { return new Date().toISOString().slice(0, 7); // "2026-05" } function getLimit(tier: SubscriptionTier, feature: string): number { const tierLimits = TIER_LIMITS[tier]; if ('aiStarterPack' in tierLimits && feature in tierLimits.aiStarterPack) { return tierLimits.aiStarterPack[feature as keyof typeof tierLimits.aiStarterPack]; } if ('aiRequestsPerMonth' in tierLimits && feature in tierLimits.aiRequestsPerMonth) { return tierLimits.aiRequestsPerMonth[feature as keyof typeof tierLimits.aiRequestsPerMonth]; } return 0; } ``` #### B.4 Sync Worker — PostgreSQL Un CRON job (ou serverless function trigger toutes les 5 min) synchronise les compteurs Redis → PostgreSQL pour facturation et historique. ```typescript // app/api/cron/sync-usage/route.ts // Trigger: every 5 minutes via Vercel Cron ou node-cron export async function POST() { const keys = await redis.keys('usage:*:2026-05'); // Tous les compteurs du mois // Batch processing par 1000 pour éviter de tuer Redis for (const key of keys) { const [parts, counter, tokens] = await Promise.all([ key.split(':'), // [usage, userId, feature, period] redis.get(key), redis.get(`${key}:tokens`), ]); const [, userId, feature, period] = parts; await prisma.usageLog.upsert({ where: { userId_feature_periodStart: { userId, feature, periodStart: new Date(period + '-01'), }, }, create: { userId, feature, periodStart: new Date(period + '-01'), periodEnd: endOfMonth(new Date(period + '-01')), requestsCount: counter ?? 0, tokensUsed: tokens ?? 0, }, update: { requestsCount: counter ?? 0, tokensUsed: tokens ?? 0, metadata: { lastSyncedAt: new Date() }, }, }); } return NextResponse.json({ synced: keys.length }); } ``` #### B.5 Schéma Prisma Updated (V2) ```prisma // ===== SUBSCRIPTION MODELS ===== enum SubscriptionTier { BASIC // Free with Starter Pack PRO // 9,90 €/mois BUSINESS // 29,90 €/mois ENTERPRISE // 49,90 + 3,90 €/user } enum SubscriptionStatus { ACTIVE PAST_DUE CANCELED TRIALING INACTIVE } enum WorkspaceRole { OWNER // Full control, billing ADMIN // Can manage members, can't delete workspace MEMBER // Can create/edit notes VIEWER // Read-only access } model Subscription { id String @id @default(cuid()) userId String @unique tier SubscriptionTier @default(BASIC) status SubscriptionStatus @default(ACTIVE) stripeCustomerId String? @unique stripeSubscriptionId String? @unique stripePriceId String? trialEndsAt DateTime? currentPeriodStart DateTime currentPeriodEnd DateTime canceledAt DateTime? cancelAtPeriodEnd Boolean @default(false) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt user User @relation(fields: [userId], references: [id], onDelete: Cascade) } // UsageLog devient un journal de sync, pas un counter temps-réel model UsageLog { id String @id @default(cuid()) userId String feature String // semantic_search, auto_tag, etc. periodStart DateTime // 1er jour du mois periodEnd DateTime // Dernier jour du mois requestsCount Int @default(0) tokensUsed Int @default(0) // Flag de sync (Redis → PG) syncedAt DateTime? metadata Json? createdAt DateTime @default(now()) user User @relation(fields: [userId], references: [id], onDelete: Cascade) @@unique([userId, feature, periodStart]) @@index([userId, periodStart]) @@index([periodStart]) } model FeatureFlag { id String @id @default(cuid()) key String @unique enabled Boolean @default(false) tiers SubscriptionTier[] metadata Json? updatedAt DateTime @updatedAt } // ===== WORKSPACE MODEL ===== model Workspace { id String @id @default(cuid()) name String plan SubscriptionTier @default(BASIC) slug String @unique // URL: app.memento.io/acme // Workspace-level quotas (Business/Enterprise) aiQuotaOverrides Json? // Override user quotas if workspace-wide createdAt DateTime @default(now()) updatedAt DateTime @updatedAt members WorkspaceMember[] invitations WorkspaceInvitation[] // Pour le billing Enterprise seatCount Int @default(1) maxSeats Int @default(20) } model WorkspaceMember { id String @id @default(cuid()) userId String workspaceId String role WorkspaceRole @default(MEMBER) joinedAt DateTime @default(now()) user User @relation(fields: [userId], references: [id], onDelete: Cascade) workspace Workspace @relation(fields: [workspaceId], references: [id], onDelete: Cascade) @@unique([userId, workspaceId]) @@index([workspaceId]) } model WorkspaceInvitation { id String @id @default(cuid()) workspaceId String email String role WorkspaceRole @default(MEMBER) token String @unique // Magic link token expiresAt DateTime acceptedAt DateTime? workspace Workspace @relation(fields: [workspaceId], references: [id], onDelete: Cascade) @@unique([workspaceId, email]) } // ===== USER MODEL UPDATED ===== model User { // ... existing 42 relations ... subscription Subscription? workspaceMembers WorkspaceMember[] usageLogs UsageLog[] // Nouveaux champs aiLanguage String @default("en") locale String @default("en") // Workspace invitation (si en attente) pendingInvitation WorkspaceInvitation? } // Note: Workspace field removed from User model (many-to-many via WorkspaceMember) ``` --- ### C. Modèle B2B & RBAC #### C.1Hiérarchie des Permissions ``` Workspace (Organisation) │ ├── OWNER (Propriétaire) │ ├── Full control │ ├── Billing management │ ├── Can delete workspace │ └── Only one per workspace (the creator) │ ├── ADMIN │ ├── Manage members (invite, remove, change roles) │ ├── Manage workspace settings │ └── Cannot delete workspace or change billing │ ├── MEMBER │ ├── Create/edit/delete own notes │ ├── Create/edit/delete own notebooks │ ├── Use AI features (consomme du workspace quota) │ └── Participate in brainstorm sessions │ └── VIEWER ├── Read-only access to all notes └── Cannot use AI features ``` #### C.2 API Entitlements — Résolution Multi-Niveaux ```typescript // lib/entitlements.ts export interface EntitlementContext { userId: string; workspaceId?: string; // Si dans un workspace B2B tier: SubscriptionTier; workspaceRole?: WorkspaceRole; } export async function canUseFeature(ctx: EntitlementContext, feature: string): Promise { // 1. Workspace-level quota check (B2B) if (ctx.workspaceId) { const workspaceQuota = await getWorkspaceQuota(ctx.workspaceId, feature); if (workspaceQuota.exhausted) { return { allowed: false, reason: 'WORKSPACE_QUOTA_EXHAUSTED', message: 'Le quota IA de votre équipe est épuisé. Demandez à votre admin.', workspaceId: ctx.workspaceId, }; } return { allowed: true, remaining: workspaceQuota.remaining }; } // 2. User-level quota check (B2C) const userQuota = await getUserQuotaFromRedis(ctx.userId, feature); if (!userQuota.allowed) { return { allowed: false, reason: 'USER_QUOTA_EXHAUSTED', message: `Votre quota ${feature} est épuisé. ${upsellMessage(ctx.tier)}`, upgradeTier: ctx.tier === 'BASIC' ? 'PRO' : undefined, }; } return { allowed: true, remaining: userQuota.remaining }; } // Exemple de réponse quand workspace quota épuisé interface EntitlementResult { allowed: boolean; remaining: number; reason?: 'WORKSPACE_QUOTA_EXHAUSTED' | 'USER_QUOTA_EXHAUSTED' | 'TIER_LIMITED'; message?: string; upgradeTier?: 'PRO' | 'BUSINESS'; // Pour afficher le bon CTA workspaceId?: string; } ``` #### C.3 Communication Quota Épuisé aux Membres ``` Scénario: Workspace Business a dépasé son quota de recherche sémantique 1. Admin recoit un email: "Votre quota IA équipe est à 80%" 2. Les membres VIEWER/MEMBER voient un warning banner: "Le quota IA de votre équipe sera bientôt épuisé" (pas de bouton upgrade, juste une information) 3. Si le quota est vraiment épuisé: - Les appels IA retours 429 avec { reason: "WORKSPACE_QUOTA_EXHAUSTED" } - Le banner devient rouge avec CTA "Contacter l'admin" - L'admin peut Upgrade le workspace ou acheter des credits additionnels ``` #### C.4 Endpoints API pour Workspaces | Endpoint | Méthode | Purpose | Rôle Requis | |---------|---------|---------|-------------| | `/api/workspaces` | POST | Créer workspace | Any authenticated | | `/api/workspaces` | GET | Lister mes workspaces | Any authenticated | | `/api/workspaces/[id]` | GET | Détail workspace | ADMIN+ | | `/api/workspaces/[id]/members` | GET | Lister membres | ADMIN+ | | `/api/workspaces/[id]/members/invite` | POST | Inviter membre | ADMIN+ | | `/api/workspaces/[id]/members/[userId]` | PATCH | Changer rôle | OWNER | | `/api/workspaces/[id]/members/[userId]` | DELETE | Retirer membre | ADMIN+ | | `/api/workspaces/[id]/quota` | GET | Quotas workspace | ADMIN+ | | `/api/workspaces/[id]/invitations/[token]` | POST | Accepter invitation | Any authenticated (email match) | --- ### D. Stack Analytics Découplée — PostHog Integration **Problème V1:** Requêter DAU/MAU directement sur PostgreSQL = tables verrouillées pendant les heures de pic. **Solution V2:** PostHog (ou Plausible/Umami pour une alternative moins chère) comme event store central. PostgreSQL ne sert que pour la facturation. #### D.1Pourquoi PostHog? | Critère | PostgreSQL Direct | PostHog | |---------|-----------------|---------| | Cost at 100K DAU | ~$200/mo (RDS large) | ~$85/mo (PostHog Cloud) | | Query performance | Dégradé en pic | Constante | | Session replay | Non | ✅ | | Feature flags | DIY | ✅ Built-in | | Funnel analysis | Complex SQL | ✅ 1-click | | GDPR compliance | Full control | EU cloud available | #### D.2 Event Payload Structure ```typescript // lib/analytics/posthog.ts import { PostHog } from 'posthog-node'; const posthog = new PostHog(process.env.NEXT_PUBLIC_POSTHOG_API_KEY!, { host: process.env.NEXT_PUBLIC_POSTHOG_HOST, flushAt: 20, // Batch de 20 events pour réduire les appels flushInterval: 10, // ou toutes les 10 secondes }); // Deux types d'événements: client et server type EventSource = 'client' | 'server'; interface MementoEvent { event: string; properties: { // Core dimensions (toujours présents) userId: string; tier: SubscriptionTier; workspaceId?: string; source: EventSource; // Feature-specific feature?: string; noteId?: string; notebookId?: string; // AI-specific aiProvider?: string; tokensUsed?: number; requestDurationMs?: number; // Billing-specific plan?: string; // 'monthly' | 'annual' currency?: string; // 'EUR' | 'USD' amountCents?: number; // UX-specific screenName?: string; elementId?: string; interactionType?: 'click' | 'focus' | 'submit' | 'scroll'; // Meta appVersion: string; platform: 'web' | 'ios' | 'android'; }; timestamp?: Date; } // Événements SERVER (Stripe, billing, usage) async function trackServerEvent(event: string, properties: Omit) { await posthog.capture({ event, properties: { ...properties, source: 'server' }, }); } // Événements CLIENT (UX, interaction) async function trackClientEvent(event: string, properties: Omit) { await posthog.capture({ event, properties: { ...properties, source: 'client' }, }); } // ===== Server Events (Stripe & Billing) ===== // Ces événements sont critiques pour la facturation et ne doivent PAS dépendre du client trackServerEvent('stripe_checkout_started', { userId, tier: 'PRO', plan: 'monthly', currency: 'EUR', amountCents: 990, }); trackServerEvent('stripe_subscription_created', { userId, tier: 'PRO', plan: 'annual', currency: 'EUR', amountCents: 9900, }); trackServerEvent('stripe_webhook_received', { userId, eventType: 'invoice.paid', // Pas de userId si le webhook est avant authentication }); trackServerEvent('ai_request_completed', { userId, tier: 'PRO', feature: 'semantic_search', aiProvider: 'openai', tokensUsed: 1500, requestDurationMs: 340, }); // ===== Client Events (UX) ===== // Ces événements viennent du browser via posthog.capture() automatique trackClientEvent('page_viewed', { userId, tier: 'PRO', screenName: 'note-editor', }); trackClientEvent('feature_discovered', { userId, tier: 'BASIC', feature: 'semantic_search', }); trackClientEvent('upgrade_intent_clicked', { userId, tier: 'BASIC', currentFeature: 'semantic_search', screenName: 'search-results', }); trackClientEvent('starter_pack_exhausted', { userId, tier: 'BASIC', lastFeature: 'semantic_search', }); ``` #### D.3 PostHog Feature Flags (Pour A/B Tests) ```typescript // lib/analytics/feature-flags.ts // Gate le pricing display par locale const showUSD = posthog.isFeatureEnabled('pricing-display-usd', { userId }); // Gate le trial offer const showTrial = posthog.isFeatureEnabled('trial-offer-14days', { userId }); // Gate le paywall style const paywallStyle = posthog.isFeatureEnabled('paywall-v2', { userId }) ? 'v2' : 'v1'; // Gate le AI starter pack const hasStarterPack = posthog.isFeatureEnabled('ai-starter-pack', { userId }); ``` #### D.4 Dashboard PostHog — Vues Clés | Dashboard | Query | Use Case | |-----------|-------|----------| | **PLG Funnel** | Signup → Create note → Use AI → Upgrade | Mesure activation | | **Feature Adoption** | `% users with event 'ai_feature_used' in 7d` | Priorité roadmap | | **Retention** | Week 1/2/4/8 cohort retention | Réduce churn | | **Usage by Tier** | Group by properties.tier | Validate pricing | | **Revenue** | Sum of stripe events, filter by eventType | MRR/ARR | #### D.5 GDPR & Data Residency ```typescript // Pour EU-only data const posthog = new PostHog(process.env.POSTHOG_API_KEY!, { host: 'https://eu.i.posthog.com', // EU datacenter flushAt: 20, flushInterval: 10, }); // Option: anonymiser les IPs posthog.capture('page_viewed', { properties: { // PostHog enregistre $ip si non spécifié — on le met à 0 '$ip': '0.0.0.0', // Anonymisé }, }); // Option:拒绝 session recording sur pages sensibles // Dans le composant React: // ``` --- ### E. Résumé des Changements V1 → V3 | Section | V1 | V2 | |--------|----|----| | **Freemium** | Basic = 0 IA | Basic = "Starter Pack" (30 req sémantique lifetime) | | **Architecture quotas** | PostgreSQL direct (1M writes/jour) | Redis (Upstash) + sync CRON PostgreSQL | | **RBAC** | Simple user.role | Workspace + WorkspaceRole enum (OWNER/ADMIN/MEMBER/VIEWER) | | **Analytics** | PostgreSQL queries | PostHog (event store) + PostgreSQL (facturation) | | **Conversion trigger** | Inline upgrade prompt | Starter Pack exhaust trigger (plus fort car valeur prouvée) | | **B2B quotas** | Per-user only | Workspace-level quotas partages | --- ### F. Fichiers à Modifier / Ajouter (V3) ``` NEW FILES: ├── docker-compose.yml # Redis + Umami (VPS) ├── lib/entitlements.ts # PostgreSQL UPSERT ou Redis self-hosted ├── lib/redis.ts # Redis client (si Option B) ├── lib/analytics/umami.ts # Umami tracker ├── app/api/cron/sync-usage/route.ts # CRON sync (si Redis) ├── app/api/workspaces/ # Workspace management APIs ├── app/api/billing/webhook/route.ts # Stripe webhook UPDATED FILES: ├── prisma/schema.prisma # UsageCounter, Workspace, WorkspaceMember, WorkspaceInvitation, WorkspaceRole enum ├── auth.ts # Expose tier + workspaceRole in session ├── middleware.ts # Workspace slug routing └── components/paywall-modal.tsx # Add workspace quota exhausted state ``` --- ### G. Fichiers à Modifier / Ajouter (V3) ``` NEW FILES: ├── docker-compose.yml # Redis + Umami (VPS) ├── lib/entitlements.ts # PostgreSQL UPSERT ou Redis selon choix ├── lib/redis.ts # Redis client (si Option B) ├── lib/analytics/umami.ts # Umami tracker ├── app/api/cron/sync-usage/route.ts # CRON sync (si Redis) ├── app/api/workspaces/ # Workspace management APIs ├── app/api/billing/webhook/route.ts # Stripe webhook UPDATED FILES: ├── prisma/schema.prisma # UsageCounter, Workspace, WorkspaceMember, WorkspaceInvitation, WorkspaceRole enum ├── auth.ts # Expose tier + workspaceRole in session ├── middleware.ts # Workspace slug routing └── components/paywall-modal.tsx # Add workspace quota exhausted state ``` --- ### H. Enterprise Billing — Pourquoi Complexe (et Alternative Simple) **Pourquoi complexe:** Stripe Licensing API pour seat-based billing demande: 1. Webhook pour prorata (ajout/retrait de seats en cours de mois) 2. Minimum 20 seats enforced en code 3. Addons +5 seats + Licensing API = double complexité **Alternative simple pour V1:** | Approche | Complexité | Pour qui | |----------|------------|---------| | **Manuel (dashboard admin)** | $0 | < 10 clients Enterprise | | **Subscription + quantity** | Faible | Tu changes le quantity manuellement dans Stripe Dashboard | | **Full Licensing API** | Haute | 10+ clients Enterprise (plus tard) | **Ma recommandation:** Enterprise = **subscription simple** à $49.90 + $3.90 × seats. Tu gères le seat count manuellement dans `/admin`. Licensing API plus tard si > 10 clients Enterprise. --- ### I. Docker Compose Prêt à Déployer ```yaml # docker-compose.yml — Déploiement sur VPS (Ubuntu 22.04) version: '3.8' services: redis: image: redis:7-alpine container_name: memento-redis command: > redis-server --requirepass ${REDIS_PASSWORD} --appendonly yes --maxmemory 256mb --maxmemory-policy allkeys-lru volumes: - redis_data:/data ports: - "127.0.0.1:6379:6379" restart: unless-stopped umami: image: umami-software/umami:postgresql-latest container_name: memento-umami ports: - "127.0.0.1:3001:3000" environment: DATABASE_URL: postgresql://${PG_USER}:${PG_PASSWORD}@postgres:5432/${PG_DB}?schema=umami DATABASE_TYPE: postgresql APP_SECRET: ${UMAMI_APP_SECRET} volumes: - umami_data:/app/data restart: unless-stopped volumes: redis_data: umami_data: ``` ```bash # Commandes de déploiement docker-compose up -d docker-compose ps # Vérifier que tout tourne docker-compose logs -f redis # Logs si besoin ``` --- ### J. Résumé — Coût Total $0 | Composant | Option Coût Zero |替代方案 payante | |-----------|-----------------|-----------------| | **Analytics** | Umami auto-hébergé | PostHog Cloud ($85/mois) | | **Cache/Compteurs** | Redis self-hosted ou PostgreSQL UPSERT | Upstash ($10/mois) | | **Email transactionnel** | Resend (tu l'as) | SendGrid, Postmark | | **Serveur** | Ton VPS existant | Vercel, Railway | | **Total additionnel** | **$0** | ~$95/mois |