# IliaCloud — Plan de monetisation

> Strategie de monetisation SaaS avec 3 niveaux d'abonnement.
> Integration Stripe pour les paiements recurrents.

---

## Plans et tarifs

| | **Free** | **Pro** | **Business** |
|---|---|---|---|
| **Prix mensuel** | 0€ | 12€/mois | 35€/mois |
| **Prix annuel** | — | 119€/an (2 mois offerts) | 349€/an (2 mois offerts) |
| **Cible** | Developpeur solo, test | Freelance, petite equipe | Agence, startup, infra critique |

---

## Limites par plan

### Infrastructure

| Ressource | Free | Pro | Business |
|---|---|---|---|
| Serveurs SSH | 1 | 5 | Illimite |
| Connexions SSH simultanees | 1 | 5 | Illimite |

### Monitoring

| Ressource | Free | Pro | Business |
|---|---|---|---|
| Monitors uptime | 3 | 20 | Illimite |
| Intervalle min uptime | 5 min | 1 min | 30 sec |
| Certificats SSL surveilles | 3 | 20 | Illimite |
| Historique metriques | 24h | 30 jours | 90 jours |
| Historique uptime | 7 jours | 30 jours | 90 jours |

### Alertes & Notifications

| Ressource | Free | Pro | Business |
|---|---|---|---|
| Webhooks | 1 | 5 | Illimite |
| Push notifications | Oui | Oui | Oui |
| Alertes serveur (CPU/RAM/Disque) | 2 regles | 20 regles | Illimite |
| Alertes HTTP 5xx | Non | Oui | Oui |
| Alertes SSL expiration | Non | Oui | Oui |

### Docker

| Ressource | Free | Pro | Business |
|---|---|---|---|
| Liste containers | Oui | Oui | Oui |
| Start/Stop/Restart | Oui | Oui | Oui |
| Stats temps reel | Non | Oui | Oui |
| Logs streaming (temps reel) | Non | Oui | Oui |
| Stacks (deploy/rollback) | Non | Oui | Oui |
| Prune containers/images | Non | Oui | Oui |

### Gestion serveur

| Ressource | Free | Pro | Business |
|---|---|---|---|
| Terminal SSH web | Non | Oui | Oui |
| Editeur de fichiers | Lecture seule | Lecture + ecriture | Lecture + ecriture |
| Actions rapides | 3 | 20 | Illimite |
| Cron manager | Lecture seule | CRUD complet | CRUD complet |
| Logs configurables | 3 | 20 | Illimite |

### Backups

| Ressource | Free | Pro | Business |
|---|---|---|---|
| Backup manuel | 1/jour | Illimite | Illimite |
| Schedules automatiques | 1 | 10 | Illimite |
| Retention max | 3 jours | 30 jours | 90 jours |
| Download backup | Non | Oui | Oui |
| Restauration | Non | Oui | Oui |

### IA / Chat

| Ressource | Free | Pro | Business |
|---|---|---|---|
| Messages chat / jour | 20 | 200 | Illimite |
| Providers disponibles | Anthropic uniquement | Tous (6 providers) | Tous (6 providers) |
| Tool use (bash, read, write) | Non | Oui | Oui |
| Sessions chat | 3 | Illimite | Illimite |

### Administration

| Ressource | Free | Pro | Business |
|---|---|---|---|
| Audit log | 7 jours | 30 jours | 90 jours |
| Export CSV audit | Non | Oui | Oui |
| Export config (chiffre) | Non | Oui | Oui |
| Import config | Non | Oui | Oui |
| Status pages publiques | 0 | 2 | Illimite |

### Futur (multi-user)

| Ressource | Free | Pro | Business |
|---|---|---|---|
| Utilisateurs | 1 | 1 | 5 inclus (+5€/user) |
| Roles et permissions | Non | Non | Oui |
| Audit par utilisateur | Non | Non | Oui |

---

## Implementation technique

### 1. Base de donnees

#### Modifier la table users

```sql
-- Ajouter 'business' comme plan valide
ALTER TABLE users DROP CONSTRAINT IF EXISTS users_plan_check;
ALTER TABLE users ADD CONSTRAINT users_plan_check
  CHECK (plan IN ('free', 'pro', 'business'));
```

#### Nouvelle table subscriptions

```sql
CREATE TABLE IF NOT EXISTS subscriptions (
  id                      UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  user_id                 UUID NOT NULL UNIQUE REFERENCES users(id) ON DELETE CASCADE,
  stripe_customer_id      TEXT UNIQUE,
  stripe_subscription_id  TEXT UNIQUE,
  stripe_price_id         TEXT,
  plan                    TEXT NOT NULL DEFAULT 'free',
  status                  TEXT NOT NULL DEFAULT 'active'
                          CHECK (status IN ('active','canceled','past_due','trialing','incomplete')),
  current_period_start    TIMESTAMPTZ,
  current_period_end      TIMESTAMPTZ,
  cancel_at_period_end    BOOLEAN NOT NULL DEFAULT FALSE,
  created_at              TIMESTAMPTZ NOT NULL DEFAULT NOW(),
  updated_at              TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX IF NOT EXISTS idx_subscriptions_user ON subscriptions(user_id);
CREATE INDEX IF NOT EXISTS idx_subscriptions_stripe ON subscriptions(stripe_customer_id);
```

### 2. Configuration des limites

```javascript
// backend/src/config/plans.js

const PLAN_LIMITS = {
  free: {
    servers: 1,
    uptime_monitors: 3,
    uptime_interval_min: 300,       // 5 min en secondes
    ssl_certificates: 3,
    webhooks: 1,
    alert_rules: 2,
    quick_actions: 3,
    log_paths: 3,
    backup_schedules: 1,
    backup_manual_per_day: 1,
    backup_retention_days: 3,
    chat_messages_per_day: 20,
    chat_sessions: 3,
    chat_providers: ['anthropic'],
    status_pages: 0,
    metrics_retention_hours: 24,
    audit_retention_days: 7,
    // Features booleennes
    terminal_ssh: false,
    file_editor_write: false,
    docker_stats: false,
    docker_logs_streaming: false,
    docker_stacks: false,
    docker_prune: false,
    cron_write: false,
    backup_download: false,
    backup_restore: false,
    chat_tool_use: false,
    export_config: false,
    export_csv_audit: false,
    http_error_alerts: false,
    ssl_alerts: false,
  },
  pro: {
    servers: 5,
    uptime_monitors: 20,
    uptime_interval_min: 60,        // 1 min
    ssl_certificates: 20,
    webhooks: 5,
    alert_rules: 20,
    quick_actions: 20,
    log_paths: 20,
    backup_schedules: 10,
    backup_manual_per_day: Infinity,
    backup_retention_days: 30,
    chat_messages_per_day: 200,
    chat_sessions: Infinity,
    chat_providers: ['anthropic','openai','google','deepseek','mistral','xai'],
    status_pages: 2,
    metrics_retention_hours: 720,   // 30 jours
    audit_retention_days: 30,
    terminal_ssh: true,
    file_editor_write: true,
    docker_stats: true,
    docker_logs_streaming: true,
    docker_stacks: true,
    docker_prune: true,
    cron_write: true,
    backup_download: true,
    backup_restore: true,
    chat_tool_use: true,
    export_config: true,
    export_csv_audit: true,
    http_error_alerts: true,
    ssl_alerts: true,
  },
  business: {
    servers: Infinity,
    uptime_monitors: Infinity,
    uptime_interval_min: 30,        // 30 sec
    ssl_certificates: Infinity,
    webhooks: Infinity,
    alert_rules: Infinity,
    quick_actions: Infinity,
    log_paths: Infinity,
    backup_schedules: Infinity,
    backup_manual_per_day: Infinity,
    backup_retention_days: 90,
    chat_messages_per_day: Infinity,
    chat_sessions: Infinity,
    chat_providers: ['anthropic','openai','google','deepseek','mistral','xai'],
    status_pages: Infinity,
    metrics_retention_hours: 2160,  // 90 jours
    audit_retention_days: 90,
    terminal_ssh: true,
    file_editor_write: true,
    docker_stats: true,
    docker_logs_streaming: true,
    docker_stacks: true,
    docker_prune: true,
    cron_write: true,
    backup_download: true,
    backup_restore: true,
    chat_tool_use: true,
    export_config: true,
    export_csv_audit: true,
    http_error_alerts: true,
    ssl_alerts: true,
  },
};
```

### 3. Middleware de verification des limites

```javascript
// backend/src/middleware/planLimits.js

const PLAN_LIMITS = require('../config/plans');

/**
 * Verifie qu'une feature booleenne est disponible pour le plan.
 * Usage : router.use(requireFeature('terminal_ssh'))
 */
function requireFeature(feature) {
  return (req, res, next) => {
    const plan = req.user.plan || 'free';
    const limits = PLAN_LIMITS[plan];
    if (!limits[feature]) {
      return res.status(403).json({
        error: 'Cette fonctionnalite necessite un plan superieur.',
        feature,
        current_plan: plan,
        required_plan: feature in PLAN_LIMITS.pro && PLAN_LIMITS.pro[feature] ? 'pro' : 'business',
        upgrade_url: '/settings#billing',
      });
    }
    next();
  };
}

/**
 * Verifie qu'un quota n'est pas depasse.
 * Usage : router.post('/', checkQuota('servers', 'servers'), handler)
 *
 * @param {string} resource — Cle dans PLAN_LIMITS (ex: 'servers')
 * @param {string} table — Nom de la table SQL pour compter
 * @param {string} [countColumn='user_id'] — Colonne pour filtrer par user
 */
function checkQuota(resource, table, countColumn = 'user_id') {
  return async (req, res, next) => {
    const plan = req.user.plan || 'free';
    const limit = PLAN_LIMITS[plan][resource];
    if (limit === Infinity) return next();

    const { rows } = await db.query(
      `SELECT COUNT(*) FROM ${table} WHERE ${countColumn} = $1`,
      [req.user.id],
    );
    const count = parseInt(rows[0].count);

    if (count >= limit) {
      return res.status(403).json({
        error: `Limite atteinte : ${count}/${limit} ${resource} (plan ${plan}).`,
        current: count,
        limit,
        current_plan: plan,
        upgrade_url: '/settings#billing',
      });
    }
    next();
  };
}

/**
 * Verifie un quota journalier (ex: messages chat par jour).
 */
function checkDailyQuota(resource, table, countColumn = 'user_id') {
  return async (req, res, next) => {
    const plan = req.user.plan || 'free';
    const limit = PLAN_LIMITS[plan][resource];
    if (limit === Infinity) return next();

    const { rows } = await db.query(
      `SELECT COUNT(*) FROM ${table}
       WHERE ${countColumn} = $1 AND created_at > NOW() - INTERVAL '24 hours'`,
      [req.user.id],
    );
    const count = parseInt(rows[0].count);

    if (count >= limit) {
      return res.status(429).json({
        error: `Limite journaliere atteinte : ${count}/${limit} (plan ${plan}).`,
        current: count,
        limit,
        current_plan: plan,
        resets_in: 'Reinitialise dans 24h.',
        upgrade_url: '/settings#billing',
      });
    }
    next();
  };
}
```

### 4. Application aux routes existantes

```javascript
// Exemples d'application sur les routes existantes :

// servers.js
router.post('/', checkQuota('servers', 'servers'), async (req, res, next) => { ... });

// uptime.js
router.post('/monitors', checkQuota('uptime_monitors', 'uptime_monitors'), async (req, res, next) => { ... });

// webhooks.js
router.post('/', checkQuota('webhooks', 'webhooks'), async (req, res, next) => { ... });

// chat.js
router.post('/sessions/:id/messages', checkDailyQuota('chat_messages_per_day', 'chat_messages', 'session_id → user_id'), ...);

// docker.js — logs streaming
router.get('/stats', requireFeature('docker_stats'), ...);

// files.js — ecriture
router.post('/write', requireFeature('file_editor_write'), ...);

// Terminal WebSocket
if (msg.type === 'terminal-open') {
  if (!PLAN_LIMITS[userPlan].terminal_ssh) {
    ws.send(JSON.stringify({ type: 'error', message: 'Terminal SSH necessite le plan Pro.' }));
    return;
  }
}
```

### 5. Integration Stripe

#### Variables d'environnement

```env
STRIPE_SECRET_KEY=sk_live_...
STRIPE_WEBHOOK_SECRET=whsec_...
STRIPE_PRICE_PRO_MONTHLY=price_...
STRIPE_PRICE_PRO_YEARLY=price_...
STRIPE_PRICE_BUSINESS_MONTHLY=price_...
STRIPE_PRICE_BUSINESS_YEARLY=price_...
```

#### Routes billing

```
POST /billing/checkout          — Creer une session Stripe Checkout
POST /billing/portal            — Ouvrir le portail client Stripe
POST /billing/webhook           — Recevoir les evenements Stripe (sans auth)
GET  /billing/status            — Voir l'abonnement actuel
POST /billing/cancel            — Annuler l'abonnement (fin de periode)
```

#### Flux d'achat

```
1. Utilisateur clique "Passer au Pro" dans l'UI
   → POST /billing/checkout { plan: 'pro', interval: 'monthly' }

2. Backend cree une session Stripe Checkout
   → Stripe genere une URL de paiement
   → Redirect utilisateur vers Stripe

3. Utilisateur paie sur Stripe
   → Stripe envoie webhook "checkout.session.completed"
   → POST /billing/webhook (sans auth, verifie par signature Stripe)
   → Backend met a jour :
     - subscriptions.status = 'active'
     - subscriptions.plan = 'pro'
     - users.plan = 'pro'

4. Utilisateur est redirige vers /settings?billing=success
   → L'UI detecte le nouveau plan et debloque les features
```

#### Evenements Stripe a gerer

| Evenement | Action |
|---|---|
| `checkout.session.completed` | Activer le plan, creer la subscription en base |
| `invoice.paid` | Confirmer le renouvellement |
| `invoice.payment_failed` | Marquer `past_due`, envoyer un email/webhook |
| `customer.subscription.updated` | Mettre a jour le plan (upgrade/downgrade) |
| `customer.subscription.deleted` | Remettre le plan a `free` |

#### Securite Stripe

- Webhook verifie par signature (`stripe.webhooks.constructEvent`)
- Pas d'auth JWT sur la route webhook (Stripe n'envoie pas de cookie)
- `stripe_customer_id` unique par utilisateur
- Pas de stockage de carte bancaire (gere par Stripe)

### 6. Frontend

#### Page pricing publique (/pricing)

- 3 colonnes (Free / Pro / Business)
- Feature comparison detaillee
- Bouton CTA par plan
- Accessible sans login

#### Composant UpgradeBanner

- S'affiche quand une limite est atteinte
- Message contextuel ("3/3 monitors utilises")
- Bouton "Passer au Pro" qui redirige vers Stripe Checkout

#### Section billing dans Settings

- Plan actuel + badge
- Date de renouvellement
- Bouton "Gerer mon abonnement" (portail Stripe)
- Bouton "Changer de plan"
- Historique des factures (via portail Stripe)

#### Hook usePlan()

```javascript
// Expose le plan et les limites au frontend
const { plan, limits, canUse, isAtLimit, upgrade } = usePlan();

// Exemples d'utilisation :
if (!canUse('terminal_ssh')) {
  return <UpgradeBanner feature="Terminal SSH" requiredPlan="pro" />;
}

if (isAtLimit('servers')) {
  return <UpgradeBanner feature="Serveurs" current={1} limit={1} />;
}
```

---

## Metriques business a suivre

| Metrique | Description |
|---|---|
| **MRR** | Monthly Recurring Revenue |
| **Churn rate** | % d'utilisateurs qui annulent par mois |
| **Conversion free→pro** | % des free qui passent pro |
| **LTV** | Lifetime Value moyen par client |
| **ARPU** | Average Revenue Per User |

---

## Calendrier de mise en place

| Etape | Duree estimee | Description |
|---|---|---|
| 1. Config plans + middleware | 1 session | Fichier plans.js, middleware planLimits.js |
| 2. Appliquer les limites aux routes | 1 session | Modifier chaque route avec checkQuota/requireFeature |
| 3. Frontend UpgradeBanner + usePlan | 1 session | Hook + composant + griser les features |
| 4. Page pricing publique | 1 session | Design des 3 colonnes avec CTA |
| 5. Integration Stripe | 1 session | Routes billing, webhooks, checkout |
| 6. Section billing Settings | 1 session | Plan actuel, gestion abonnement |
| 7. Tests + deploiement | 1 session | Tests unitaires + E2E + production |

---

---

## Positionnement & argument de vente

> **Termius + UptimeRobot + Portainer + un agent IA = 25€/mois minimum en outils separes.**
> **IliaCloud = 9€, tout inclus.**

C'est la phrase qui doit apparaitre en haut de la page pricing, avant le tableau de comparaison.

Le differentiant c'est le **tout-en-un** : aucun concurrent ne combine SSH + Docker + IA + monitoring + backups dans la meme interface.

---

## Decisions techniques prises

| Sujet | Decision | Raison |
|---|---|---|
| **Multi-localisation** | Cloudflare Workers (3 regions, free tier 100k req/jour) | 50 lignes de code, zero infra, a faire rapidement |
| **Historique long** | 1 an pour Business (changer la retention SQL) | 10 min de dev, argument marketing immediat |
| **SMS / Twilio** | Reporter apres premiers clients payants | Telegram couvre le besoin, zero cout |
| **SOC2** | Ignorer pour l'instant | Hors cible (freelance/indie), 50k€ + 6 mois |

---

## Analyse des couts

| Poste | Cout | Detail |
|---|---|---|
| **VPS OVH** | ~30€/mois | Tient 100-150 users actifs sans upgrade |
| **Stockage metriques/logs** | Negligeable | Quelques MB/mois/user |
| **Uptime monitoring** | Negligeable | HTTP requests toutes les minutes |
| **Push notifications** | 0€ | VAPID gratuit |
| **IA / Chat** | 0€ | BYOK (l'utilisateur fournit sa cle API) |
| **Webhooks** | Negligeable | HTTP outbound |
| **Cout marginal/user** | **0,10 a 0,30€/mois** | |

**Seuil de rentabilite** : 3 users Pro (36€/mois) couvrent le VPS.
**A 50 users Pro** : 600€/mois MRR, marge ~95%.

---

*Derniere mise a jour : 2 avril 2026*
