All checks were successful
Deploy to Production / Build and Deploy (push) Successful in 7s
- 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
1498 lines
52 KiB
Markdown
1498 lines
52 KiB
Markdown
# 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 | |