Files
Momento/memento-note/docs/saas-deployment-prep.md
Antigravity 1fcea6ed7d
All checks were successful
Deploy to Production / Build and Deploy (push) Successful in 7s
feat: brainstorm sessions, PDF document Q&A, embedding fixes, and UI improvements
- Add brainstorm feature with collaborative canvas, AI idea generation, live cursors, playback, and export
- Add PDF upload/extraction/ingestion pipeline with pgvector document search (RAG)
- Add document Q&A overlay with streaming chat and PDF preview
- Add note attachments UI with status polling, grid layout, and auto-scroll
- Add task extraction AI tool and agent executor improvements
- Fix NoteEmbedding missing updatedAt column, re-index 66 notes with 1536-dim embeddings
- Fix brainstorm 'Create Note' button: add success toast and redirect to created note
- Fix memory echo notification infinite polling
- Fix chat route to always include document_search tool
- Add brainstorm i18n keys across all 14 locales
- Add socket server for real-time brainstorm collaboration
- Add hierarchical notebook selector and organize notebook dialog improvements
- Add sidebar brainstorm section with session management
- Update prisma schema with brainstorm tables, attachments, and document chunks
2026-05-14 17:43:21 +00:00

52 KiB
Raw Permalink Blame History

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)

// ===== 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

// 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<boolean> {
  // 1. Check subscription tier
  // 2. Check usage within limits
  // 3. Return boolean
}

export async function trackFeatureUsage(
  userId: string, 
  feature: string, 
  tokensUsed: number,
  metadata?: Record<string, any>
): Promise<void> {
  // 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)

// 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

// 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<string, any>;
}

// 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 <PaywallModal> component
  • Créer <UsageMeter> 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 <AdminDashboard> avec vrais metrics
  • Créer <UserAnalytics> 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

# 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)

// 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

// 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<string>(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<string, any>
): Promise<void> {
  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.

// 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<number>(key),
      redis.get<number>(`${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)

// ===== 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

// 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<EntitlementResult> {
  // 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

// 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<MementoEvent['properties'], 'source'>) {
  await posthog.capture({
    event,
    properties: { ...properties, source: 'server' },
  });
}

// Événements CLIENT (UX, interaction)
async function trackClientEvent(event: string, properties: Omit<MementoEvent['properties'], 'source'>) {
  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)

// 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

// 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:
// <PostHogProvider sessionRecording={{ blockBy: '#note-content' }} />

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

# 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:
# 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