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

1498 lines
52 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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<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)
```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<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
```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<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.
```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<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)
```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<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
```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<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)
```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:
// <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
```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 |