Startups & ScalingJérémy Marquer

Scaler une startup tech : De 10 à 100 utilisateurs sans exploser

Guide pratique pour scaler infrastructure, équipe et processus de 10 à 100 utilisateurs. Architecture, monitoring, dette technique, budget. Évitez les pièges.

Scaler une startup tech : De 10 à 100 utilisateurs sans exploser
#Scaling#Infrastructure#Architecture#Performance#Startup

Scaler une startup tech : De 10 à 100 utilisateurs sans exploser

80% des startups crashent entre 50 et 200 utilisateurs. Cause : infrastructure non préparée. Voici le plan étape par étape pour scaler de 10 à 100 users sans tout casser.

Les 3 seuils critiques

Seuil 1 : 10 → 50 utilisateurs (MVP → Early traction)

Symptômes de croissance :

  • ⚠️ App ralentit aux heures de pointe
  • ⚠️ Bugs remontés quotidiennement
  • ⚠️ Support client prend 4h/jour
  • ⚠️ Déploiements = stress (downtime possible)

Problèmes techniques :

  • Database queries non optimisées (N+1)
  • Pas de cache
  • Logs en console.log
  • Pas de monitoring
  • Déploiement manuel

Budget tech : 500-1K€/mois Temps setup : 2-3 semaines

Seuil 2 : 50 → 100 utilisateurs (Product-market fit)

Symptômes de croissance :

  • 🔥 Site down 2-3x/semaine
  • 🔥 Database timeout fréquents
  • 🔥 Features prennent 2x plus de temps
  • 🔥 Onboarding nouveau dev = 2 semaines

Problèmes techniques :

  • Architecture monolithique
  • Pas de tests automatisés
  • Dette technique = 30% du temps dev
  • Pas de CI/CD
  • Database migrations = panique

Budget tech : 2-5K€/mois Temps setup : 4-6 semaines

Seuil 3 : 100+ utilisateurs (Scale)

Symptômes de croissance :

  • 💥 Load balancing nécessaire
  • 💥 Multi-région envisagée
  • 💥 Équipe 5+ devs
  • 💥 Compliance (RGPD, SOC2)

Non couvert ici : Cet article focus 10 → 100 users

Phase 1 : 10 → 50 users (4 semaines)

Semaine 1 : Setup monitoring (CRITICAL)

Objectif : Voir avant que ça pète

Tools à installer :

OutilUsagePrix/moisSetup
SentryError tracking0-26€30min
Uptime RobotSite monitoring0€10min
Vercel AnalyticsPerformance0-20€5min
PostgreSQL statsDB slow queries0€1h

Configuration Sentry :

// next.config.js
const { withSentryConfig } = require('@sentry/nextjs');

module.exports = withSentryConfig({
  // Config Next.js
}, {
  org: "your-org",
  project: "your-project",
  silent: true,
});

// pages/_app.tsx
import * as Sentry from "@sentry/nextjs";

Sentry.init({
  dsn: process.env.NEXT_PUBLIC_SENTRY_DSN,
  tracesSampleRate: 0.1, // 10% des requêtes
  environment: process.env.NODE_ENV,
});

Alertes à configurer :

  • Error rate >5% → Email immédiat
  • Site down >1min → SMS
  • API latency >2s → Slack
  • Database CPU >80% → Email

Budget : 0-50€/mois

Semaine 2 : Optimiser database (80% impact)

Audit queries lentes :

-- PostgreSQL : top 10 queries lentes
SELECT 
  query,
  mean_exec_time,
  calls
FROM pg_stat_statements
ORDER BY mean_exec_time DESC
LIMIT 10;

Problèmes classiques :

1. N+1 queries

Avant (50 queries) :

// ❌ Mauvais
const users = await prisma.user.findMany();
for (const user of users) {
  user.posts = await prisma.post.findMany({ 
    where: { userId: user.id } 
  });
}

Après (2 queries) :

// ✅ Bon
const users = await prisma.user.findMany({
  include: { posts: true }
});

Gain : -96% requêtes, -80% latency

2. Indexes manquants

Avant (3s query) :

-- Scan full table (100K rows)
SELECT * FROM posts WHERE user_id = 123;

Après (20ms query) :

-- Créer index
CREATE INDEX idx_posts_user_id ON posts(user_id);

-- Même query = 150x plus rapide
SELECT * FROM posts WHERE user_id = 123;

Indexes critiques :

-- Foreign keys
CREATE INDEX idx_posts_user_id ON posts(user_id);
CREATE INDEX idx_comments_post_id ON comments(post_id);

-- Colonnes filtrées souvent
CREATE INDEX idx_users_email ON users(email);
CREATE INDEX idx_posts_created_at ON posts(created_at);

-- Recherche texte
CREATE INDEX idx_posts_title_trgm ON posts USING gin(title gin_trgm_ops);

3. Pas de pagination

Avant (10s, 50MB transferred) :

// ❌ Load 10K posts
const posts = await prisma.post.findMany();

Après (200ms, 500KB transferred) :

// ✅ Load 20 posts
const posts = await prisma.post.findMany({
  take: 20,
  skip: page * 20,
  orderBy: { createdAt: 'desc' }
});

Gain : -95% latency, -99% bandwidth

Semaine 3 : Ajouter cache stratégique

Caching levels :

Browser Cache (3600s)
    ↓
CDN Cache (Vercel, 86400s)
    ↓
App Cache (Redis, 300s)
    ↓
Database

Redis setup (Upstash gratuit) :

// lib/redis.ts
import { Redis } from '@upstash/redis';

export const redis = new Redis({
  url: process.env.UPSTASH_REDIS_URL!,
  token: process.env.UPSTASH_REDIS_TOKEN!,
});

// Wrapper avec TTL
export async function getCached<T>(
  key: string,
  fetcher: () => Promise<T>,
  ttl = 300 // 5min par défaut
): Promise<T> {
  // Check cache
  const cached = await redis.get<T>(key);
  if (cached) return cached;

  // Fetch fresh
  const fresh = await fetcher();
  await redis.setex(key, ttl, fresh);
  return fresh;
}

Usage :

// Sans cache : 500ms/requête
const user = await prisma.user.findUnique({ where: { id } });

// Avec cache : 20ms/requête
const user = await getCached(
  `user:${id}`,
  () => prisma.user.findUnique({ where: { id } }),
  3600 // 1h
);

Ce qu'il faut cacher :

  • ✅ User profiles (1h TTL)
  • ✅ Config app (24h TTL)
  • ✅ Listes paginated (5min TTL)
  • ❌ Real-time data (messages, notifs)
  • ❌ User-specific data

Budget : 0€ (Upstash free tier = 10K reqs/day)

Semaine 4 : CI/CD basique

Objectif : Déployer sans stress

GitHub Actions setup :

# .github/workflows/deploy.yml
name: Deploy

on:
  push:
    branches: [main]

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - uses: actions/setup-node@v3
        with:
          node-version: 20
      - run: npm ci
      - run: npm test
      - run: npm run lint

  deploy:
    needs: test
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - uses: amondnet/vercel-action@v20
        with:
          vercel-token: ${{ secrets.VERCEL_TOKEN }}
          vercel-org-id: ${{ secrets.ORG_ID }}
          vercel-project-id: ${{ secrets.PROJECT_ID }}
          vercel-args: '--prod'

Bénéfices :

  • ✅ Tests auto avant déploiement
  • ✅ Déploiement 1-click (git push)
  • ✅ Rollback facile (revert commit)
  • ✅ Preview branches (Vercel)

Budget : 0€ (GitHub Actions gratuit <2000 min/mois)

Résultat Phase 1 :

  • ✅ Monitoring en place
  • ✅ Database optimisée (-80% latency)
  • ✅ Cache stratégique (-90% load DB)
  • ✅ Déploiement automatisé
  • 💰 Budget : 50-100€/mois
  • ⏱️ Setup : 40-60h dev

Phase 2 : 50 → 100 users (6 semaines)

Semaine 1-2 : Architecture modulaire

Problème : Monolithe 20K lignes = impossible à maintenir

Solution : Découper en modules

Avant :

src/
├── pages/
│   ├── api/
│   │   ├── users.ts (500 lignes)
│   │   ├── posts.ts (800 lignes)
│   │   └── comments.ts (300 lignes)

Après :

src/
├── modules/
│   ├── users/
│   │   ├── user.service.ts
│   │   ├── user.repository.ts
│   │   └── user.types.ts
│   ├── posts/
│   │   ├── post.service.ts
│   │   ├── post.repository.ts
│   │   └── post.types.ts
├── pages/
│   └── api/
│       ├── users/[id].ts (50 lignes)
│       └── posts/[id].ts (50 lignes)

Pattern : Service → Repository → Database

// modules/users/user.repository.ts
export class UserRepository {
  async findById(id: string) {
    return prisma.user.findUnique({ where: { id } });
  }
}

// modules/users/user.service.ts
export class UserService {
  constructor(private repo: UserRepository) {}

  async getUser(id: string) {
    const user = await getCached(
      `user:${id}`,
      () => this.repo.findById(id),
      3600
    );
    if (!user) throw new NotFoundError();
    return user;
  }
}

// pages/api/users/[id].ts (50 lignes)
export default async function handler(req, res) {
  const service = new UserService(new UserRepository());
  const user = await service.getUser(req.query.id);
  res.json(user);
}

Bénéfices :

  • ✅ Code testable (mock repository)
  • ✅ Réutilisable (service partagé)
  • ✅ Maintenable (1 fichier = 1 responsabilité)

Semaine 3 : Tests automatisés (critiques)

Objectif : Déployer sans peur de tout casser

Coverage cible :

  • 80% units tests (fonctions business)
  • 20% integration tests (API endpoints)
  • 0% E2E (trop lent pour early stage)

Setup Vitest :

// vitest.config.ts
import { defineConfig } from 'vitest/config';

export default defineConfig({
  test: {
    coverage: {
      provider: 'v8',
      reporter: ['text', 'html'],
      thresholds: {
        lines: 80,
        functions: 80,
        branches: 70,
      }
    }
  }
});

Test exemple :

// modules/users/user.service.test.ts
import { describe, it, expect, vi } from 'vitest';
import { UserService } from './user.service';

describe('UserService', () => {
  it('should get user from cache', async () => {
    const mockRepo = {
      findById: vi.fn().mockResolvedValue({ id: '1', name: 'John' })
    };
    const service = new UserService(mockRepo);

    const user = await service.getUser('1');

    expect(user.name).toBe('John');
    expect(mockRepo.findById).toHaveBeenCalledOnce();
  });

  it('should throw if user not found', async () => {
    const mockRepo = {
      findById: vi.fn().mockResolvedValue(null)
    };
    const service = new UserService(mockRepo);

    await expect(service.getUser('999')).rejects.toThrow();
  });
});

Run tests :

npm test          # Run all
npm test -- --watch  # Watch mode
npm test -- --coverage  # Coverage report

CI integration (GitHub Actions) :

- run: npm test -- --coverage
- uses: codecov/codecov-action@v3  # Upload coverage

Budget : 0€

Semaine 4 : Database migrations sécurisées

Problème : ALTER TABLE = downtime

Solution : Migrations zero-downtime

Prisma migrations :

# Créer migration
npx prisma migrate dev --name add_user_role

# Review SQL généré
cat prisma/migrations/XXX_add_user_role/migration.sql

Checklist zero-downtime :

  • Additive only : Ajouter colonnes (pas supprimer)
  • Default values : Toujours un défaut
  • Nullable first : Rendre nullable → populate → NOT NULL
  • Indexes online : CONCURRENT (Postgres)
  • Backward compatible : App v1 doit fonctionner avec schema v2

Exemple migration sécurisée :

-- ❌ MAUVAIS (downtime)
ALTER TABLE users ADD COLUMN role TEXT NOT NULL;

-- ✅ BON (zero downtime)
-- Step 1 : Ajouter nullable avec défaut
ALTER TABLE users ADD COLUMN role TEXT DEFAULT 'user';

-- Step 2 : Populate existant (background job)
UPDATE users SET role = 'admin' WHERE email LIKE '%@company.com';

-- Step 3 : Rendre NOT NULL (après deploy)
ALTER TABLE users ALTER COLUMN role SET NOT NULL;

Rollback strategy :

-- Toujours avoir un DOWN migration
-- migration_down.sql
ALTER TABLE users DROP COLUMN role;

Semaine 5 : Performance budget

Objectif : Garantir perfs

Core Web Vitals cibles :

MétriqueSeuilCurrentAction
LCP (Largest Contentful Paint)<2.5s3.2s❌ Image lazy load
FID (First Input Delay)<100ms50ms✅ OK
CLS (Cumulative Layout Shift)<0.10.15❌ Reserve space images

Lighthouse CI :

# .github/workflows/lighthouse.yml
- uses: treosh/lighthouse-ci-action@v9
  with:
    urls: |
      https://preview-${{ github.sha }}.vercel.app
    uploadArtifacts: true
    temporaryPublicStorage: true

Performance budget :

// lighthouserc.json
{
  "ci": {
    "assert": {
      "assertions": {
        "first-contentful-paint": ["error", {"maxNumericValue": 2000}],
        "interactive": ["error", {"maxNumericValue": 3000}],
        "total-byte-weight": ["error", {"maxNumericValue": 500000}]
      }
    }
  }
}

CI bloque si perf < seuil

Semaine 6 : Documentation technique

Objectif : Onboarder dev #2 en <2 jours

Docs critiques :

1. README.md

# Project Name

## Quick start
\`\`\`bash
git clone ...
npm install
cp .env.example .env
npm run dev
\`\`\`

## Architecture
- Frontend : Next.js 15 + React 19
- Backend : tRPC + Prisma
- Database : PostgreSQL (Supabase)
- Cache : Redis (Upstash)

## Deploy
\`\`\`bash
git push origin main  # Auto-deploy Vercel
\`\`\`

2. ARCHITECTURE.md

## System design

[Diagram here]

## Data flow
1. User → Next.js (Vercel)
2. Next.js → tRPC API
3. tRPC → Service Layer
4. Service → Repository
5. Repository → Prisma
6. Prisma → PostgreSQL

## Key decisions
- **Why Next.js?** : SEO + React
- **Why tRPC?** : Type-safe API
- **Why Supabase?** : Managed Postgres

3. CONTRIBUTING.md

## Workflow
1. Create branch `feat/feature-name`
2. Code + tests
3. Push → Auto-preview Vercel
4. PR → Code review
5. Merge → Auto-deploy prod

## Standards
- ESLint + Prettier
- Conventional commits
- Test coverage >80%

Budget : 8-16h rédaction

Résultat Phase 2 :

  • ✅ Architecture modulaire
  • ✅ Tests 80% coverage
  • ✅ Migrations zero-downtime
  • ✅ Performance budget
  • ✅ Documentation complète
  • 💰 Budget : 100-200€/mois
  • ⏱️ Setup : 120-150h dev

Infrastructure : coûts réels 10 → 100 users

Phase 1 : 10-50 users

ServiceUsagePrix/mois
Hébergement (Vercel Pro)Unlimited bandwidth20€
Database (Supabase Free)500MB, 2GB bandwidth0€
Cache (Upstash Free)10K requests/day0€
Monitoring (Sentry)5K errors/month0€
Analytics (Vercel)Unlimited0€
Email (Resend Free)3K emails/month0€
TOTAL20€/mois

Phase 2 : 50-100 users

ServiceUsagePrix/mois
Hébergement (Vercel Pro)Unlimited20€
Database (Supabase Pro)8GB, 50GB bandwidth25€
Cache (Upstash Pay-as-you-go)100K req/day10€
Monitoring (Sentry Team)50K errors/month26€
Analytics (Vercel)Unlimited0€
Email (Resend)10K emails/month20€
Storage (S3)10GB, 50GB transfer5€
TOTAL106€/mois

Projection 100+ users : 200-500€/mois

Checklist scaling readiness

Infrastructure ✅

  • Monitoring setup (Sentry + Uptime)
  • Database optimisée (indexes, N+1 fixed)
  • Cache layer (Redis)
  • CI/CD automatisé (GitHub Actions)
  • Backup database (daily)

Code ✅

  • Architecture modulaire (services + repos)
  • Tests coverage >80%
  • Documentation technique à jour
  • Performance budget configuré
  • Error handling standardisé

Process ✅

  • Migrations zero-downtime
  • Rollback strategy
  • Incident response plan (qui appeler ?)
  • Feature flags (déployer sans activer)

Conclusion

Scaler de 10 à 100 utilisateurs nécessite 10-12 semaines de travail technique et 120-200€/mois d'infrastructure.

ROI : Investir 15K€ dev maintenant évite 50-100K€ de refonte à 500 users.

Audit infrastructure scaling : Évaluez votre readiness et obtenez un plan d'action.


À propos : Jérémy Marquer a accompagné 20+ startups dans leur scaling. Zéro crash majeur à ce jour.

Partager cet article