🔧 BIBLE CTO — IMPLÉMENTATION PAR TAILLE DE PROJET
Petit · Moyen · Grand
Code: [NOVA:CTO-PMG-V1-20260219]
Stack imposée Dream Nova : Next.js App Router, Python async, Supabase, Tailwind CSS 4, Vercel, TypeScript strict.
JAMAIS changer la stack sans raison technique impérative. La cohérence est une arme compétitive.
TABLE DES MATIÈRES
- Classification Technique
- GOD-BOILERPLATE — Template Universel DreamNova
- PARTIE A — Petit Projet (< 24h)
- A1. Pipeline Voice → Image ([Client] Case Study)
- A2. Landing Page Rapide (< 30 min)
- A3. Script Python Automation Template
- B1. Architecture Type
- B2. Schema Supabase Standard
- B3. Stripe Integration Standard
- B4. Performance Checklist Lighthouse >90
- B5. Deployment Checklist (20 items)
- C1. Architecture Multi-Services
- C2. CI/CD Pipeline Complet
- C3. Monitoring & Auto-Repair
- C4. Database Migration Strategy
- C5. Security Grand Projet
CLASSIFICATION TECHNIQUE
Le premier acte du CTO est de classifier le projet avant d'écrire une seule ligne de code. Une mauvaise classification = surengineering ou dette technique immédiate.
| Critère | PETIT | MOYEN | GRAND |
|---|---|---|---|
| Durée | < 24h | 2–7 jours | > 1 semaine |
| Revenu cible | 0–2 000 ₪/mois | 2 000–15 000 ₪/mois | 15 000 ₪/mois+ |
| Frontend | HTML/CSS ou Next.js simple | Next.js App Router complet | Next.js + Mobile React Native |
| Backend | API route ou Python script | Next.js API routes + Supabase | Node.js/Python séparé + Supabase |
| Auth | Aucune ou Supabase Magic Link | Supabase Auth complet | Supabase Auth + RLS policies exhaustives |
| Paiement | Stripe Payment Link (zéro code) | Stripe Checkout Session | Stripe Subscriptions + LemonSqueezy |
| DB | Aucune ou Supabase simple | Supabase PostgreSQL versionné | Supabase + migrations versionnées + seeds |
| Deploy | Vercel en 1 commande | Vercel + env vars + domaine | Vercel + GitHub Actions CI/CD complet |
| Tests | Manuel seul | Playwright QA basique | Playwright E2E complet + Sentry + alertes |
| Monitoring | Aucun | Vercel Analytics | Sentry + Upstash + dashboards |
| Exemple DreamNova | Script [Client], Landing Ariel | Keren Rabbi Yisrael, Esther Ifrah | DreamNova Platform, DreamOS |
Règle de Classification Cash-First
Avant TOUT projet, évaluer silencieusement :
SI feature → paiement < 7 jours → code production-grade, max effort
SINON → MVP <15 lignes, log l'idée, revenir au revenue work
Cette règle est intégrée dans l'identité CTO DreamNova. Elle n'est pas optionnelle.
GOD-BOILERPLATE
Le Template Universel DreamNova
Tout projet DreamNova démarre depuis ce template. Le dupliquer, jamais le réécrire.
Structure de dossiers exacte
project-name/
├── app/
│ ├── (marketing)/ # Pages publiques : /, /about, /pricing, /contact
│ │ ├── page.tsx # Homepage
│ │ ├── pricing/page.tsx # Page tarifs
│ │ └── contact/page.tsx # Formulaire contact
│ ├── (shop)/ # E-commerce
│ │ ├── checkout/page.tsx # Checkout Stripe
│ │ └── success/page.tsx # Confirmation commande
│ ├── (auth)/ # Authentification
│ │ ├── login/page.tsx
│ │ └── register/page.tsx
│ ├── (dashboard)/ # Zone connectée
│ │ ├── layout.tsx # Layout avec sidebar
│ │ ├── overview/page.tsx # Dashboard principal
│ │ ├── orders/page.tsx # Commandes
│ │ └── settings/page.tsx # Paramètres
│ ├── api/
│ │ ├── stripe/
│ │ │ ├── checkout/route.ts # POST → session Stripe
│ │ │ └── webhook/route.ts # POST → events Stripe
│ │ ├── contact/route.ts # POST → email Resend
│ │ └── waitlist/route.ts # POST → inscription waitlist
│ ├── globals.css # Variables CSS globales + Tailwind
│ └── layout.tsx # Root layout (fonts, metadata)
├── components/
│ ├── ui/ # Composants atomiques (Shadcn/ui base)
│ │ ├── button.tsx
│ │ ├── card.tsx
│ │ └── input.tsx
│ ├── sections/ # Sections de pages réutilisables
│ │ ├── Hero.tsx
│ │ ├── Features.tsx
│ │ ├── Testimonials.tsx
│ │ └── PricingTable.tsx
│ └── layout/ # Composants layout
│ ├── Header.tsx
│ ├── Footer.tsx
│ └── Sidebar.tsx
├── lib/
│ ├── supabase/
│ │ ├── client.ts # createBrowserClient()
│ │ ├── server.ts # createServerClient() (RSC/API)
│ │ └── middleware.ts # Session refresh middleware
│ ├── stripe.ts # Instance Stripe configurée
│ ├── resend.ts # Instance Resend configurée
│ ├── validations.ts # Schemas Zod partagés
│ └── utils.ts # cn(), formatPrice(), etc.
├── public/
│ ├── images/ # Images optimisées WebP
│ ├── icons/ # SVG icons
│ └── og-image.png # Open Graph image (1200x630)
├── supabase/
│ ├── migrations/ # Migrations SQL numérotées
│ │ └── 001_initial_schema.sql
│ └── seed.sql # Données de test
├── .env.local.example # Template variables d'env
├── .env.local # Variables réelles (JAMAIS commitées)
├── next.config.js # Config Next.js + security headers
├── tailwind.config.ts # Tokens design DreamNova
├── tsconfig.json # TypeScript strict
├── middleware.ts # Supabase session + rate limiting
└── package.json
Packages inclus par défaut
{
"dependencies": {
"next": "^15.0.0",
"@supabase/supabase-js": "^2.0.0",
"@supabase/ssr": "^0.5.0",
"stripe": "^17.0.0",
"resend": "^4.0.0",
"framer-motion": "^11.0.0",
"zod": "^3.0.0",
"clsx": "^2.0.0",
"tailwind-merge": "^2.0.0"
},
"devDependencies": {
"typescript": "^5.0.0",
"@types/node": "^20.0.0",
"tailwindcss": "^4.0.0",
"@playwright/test": "^1.40.0"
}
}
Variables .env.local requises
# Supabase
NEXT_PUBLIC_SUPABASE_URL=https://XXXX.supabase.co
NEXT_PUBLIC_SUPABASE_ANON_KEY=eyJ...
SUPABASE_SERVICE_ROLE_KEY=eyJ...
# Stripe
STRIPE_SECRET_KEY=sk_live_...
STRIPE_WEBHOOK_SECRET=whsec_...
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_live_...
# Resend
RESEND_API_KEY=re_...
# Site
NEXT_PUBLIC_SITE_URL=https://projet.vercel.app
# Gemini (pour scripts Python)
GEMINI_API_KEY=AIzaSyDZ29PmIKVBj_YS86MsX5FJ0jaXyQyebts
# Optionnel: Sentry (grands projets)
SENTRY_DSN=https://...@sentry.io/...
Commande setup alias
# Ajouter dans ~/.zshrc
alias dn-init='function _dn_init() {
git clone https://github.com/CodeNoLimits/dreamnova-boilerplate "$1"
cd "$1"
npm install
cp .env.local.example .env.local
echo "Projet $1 pret. Edit .env.local puis npm run dev"
}; _dn_init'
# Premier deploy (token permanent DreamNova)
alias dn-deploy='vercel --prod --token=VCP_TOKEN_REDACTED'
next.config.js obligatoire (headers sécurité)
/** @type {import('next').NextConfig} */
const nextConfig = {
images: {
formats: ['image/webp', 'image/avif'],
minimumCacheTTL: 31536000,
},
async headers() {
return [{
source: '/(.*)',
headers: [
{ key: 'X-Frame-Options', value: 'DENY' },
{ key: 'X-Content-Type-Options', value: 'nosniff' },
{ key: 'Referrer-Policy', value: 'strict-origin-when-cross-origin' },
{ key: 'Permissions-Policy', value: 'camera=(), microphone=(), geolocation=()' },
{
key: 'Content-Security-Policy',
value: "default-src 'self'; script-src 'self' 'unsafe-eval' 'unsafe-inline' *.stripe.com; frame-src *.stripe.com;"
}
]
}]
}
}
module.exports = nextConfig
PARTIE A — Petit Projet (< 24h)
A1. Pipeline Voice → Image ([Client] Case Study — Template Réutilisable)
Contexte réel : Client [Client] envoie un message vocal en hébreu avec des instructions de retouche photo pour ses produits bijoux. Pipeline complet : audio → texte → prompt structuré → image éditée → validation auto.
Durée d'implémentation : 4 heures. Valeur client : 200-500 ₪ par session.
# ~/scripts/workflows/voice_to_image_pipeline.py
"""
Pipeline Voice -> Image Pipeline (DreamNova Template)
Usage: python3 voice_to_image_pipeline.py audio.ogg photo1.jpg photo2.jpg
"""
import os
import sys
import json
import time
import logging
from pathlib import Path
from concurrent.futures import ThreadPoolExecutor, as_completed
from typing import Optional
import requests
from groq import Groq
import google.generativeai as genai
from PIL import Image
# Configuration
GROQ_KEY = os.environ.get("GROQ_API_KEY", "")
NANO_BANANA_KEY = os.environ.get("NANO_BANANA_PRO_KEY", "")
GEMINI_KEY = os.environ.get("GEMINI_API_KEY", "AIzaSyDZ29PmIKVBj_YS86MsX5FJ0jaXyQyebts")
TELEGRAM_TOKEN = os.environ.get("TELEGRAM_BOT_TOKEN", "")
TELEGRAM_CHAT_ID = "7269582214" # David @nerostats
# Logging standard DreamNova
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s [%(levelname)s] %(message)s",
handlers=[
logging.FileHandler(f"/tmp/voice_to_image_{int(time.time())}.log"),
logging.StreamHandler()
]
)
log = logging.getLogger(__name__)
def send_telegram_alert(message: str) -> None:
"""Alerte Telegram David en cas d'erreur critique."""
if not TELEGRAM_TOKEN:
return
try:
requests.post(
f"https://api.telegram.org/bot{TELEGRAM_TOKEN}/sendMessage",
json={"chat_id": TELEGRAM_CHAT_ID, "text": f"[VoiceImage] {message}"},
timeout=5
)
except Exception:
pass
def transcribe_audio(audio_path: str, language: str = "he") -> str:
"""
Etape 1: Groq Whisper large-v3-turbo -> texte exact.
Supporte: OGG, MP3, MP4, WAV, M4A, WebM.
"""
log.info(f"Transcription de {audio_path} (langue: {language})")
client = Groq(api_key=GROQ_KEY)
with open(audio_path, "rb") as f:
transcription = client.audio.transcriptions.create(
file=(Path(audio_path).name, f),
model="whisper-large-v3-turbo",
language=language,
response_format="text"
)
log.info(f"Transcription: {transcription[:200]}...")
return transcription
def parse_instructions_to_prompts(raw_text: str, image_paths: list[str]) -> list[dict]:
"""
Etape 2: Gemini parse les instructions vocales en prompts structurés.
Retourne: [{image_path, prompt_english, style_notes, priority}]
"""
genai.configure(api_key=GEMINI_KEY)
model = genai.GenerativeModel("gemini-2.0-flash")
system_prompt = f"""
Tu es un expert retouche photo bijouterie luxe.
Instructions vocales du client (hébreu/français/anglais melange):
"{raw_text}"
Images disponibles: {[Path(p).name for p in image_paths]}
Genere un JSON valide avec cette structure exacte:
[
{{
"image_filename": "nom_exact_du_fichier.jpg",
"prompt_english": "Professional product photography: [instruction en anglais ultra-precis]",
"style_notes": "luxury jewelry, white background, golden ratio, 8K",
"priority": 1
}}
]
Regles:
- prompt_english doit etre en anglais uniquement (les APIs image n'acceptent que l'anglais)
- Sois TRES specifique: couleurs, eclairage, angles, corrections demandees
- Si une instruction concerne plusieurs photos, duplique l'entree
- Trie par priorite (1 = plus important)
"""
response = model.generate_content(system_prompt)
# Extraire le JSON de la reponse
text = response.text.strip()
start = text.find("[")
end = text.rfind("]") + 1
parsed = json.loads(text[start:end])
# Mapper nom fichier -> chemin complet
name_to_path = {Path(p).name: p for p in image_paths}
for item in parsed:
item["image_path"] = name_to_path.get(item.get("image_filename", ""), image_paths[0])
log.info(f"Parsed {len(parsed)} instructions depuis le vocal")
return parsed
def edit_image_nanobananaPro(image_path: str, prompt: str, output_dir: str = "/tmp") -> Optional[str]:
"""
Etape 3: Nano Banana Pro API -> image editee.
Retourne: chemin local du fichier resultat.
"""
log.info(f"Edition image: {Path(image_path).name} | Prompt: {prompt[:80]}...")
output_path = Path(output_dir) / f"edited_{Path(image_path).stem}_{int(time.time())}.webp"
try:
with open(image_path, "rb") as img:
resp = requests.post(
"https://api.nanobananapro.com/v1/edit",
headers={"Authorization": f"Bearer {NANO_BANANA_KEY}"},
files={"image": (Path(image_path).name, img, "image/jpeg")},
data={
"prompt": prompt,
"output_format": "webp",
"quality": 95,
"style": "professional product photography"
},
timeout=120
)
resp.raise_for_status()
result = resp.json()
# Telecharger l'image resultante
img_url = result.get("output_url") or result.get("url")
if img_url:
img_data = requests.get(img_url, timeout=30).content
output_path.write_bytes(img_data)
log.info(f"Image sauvegardee: {output_path}")
return str(output_path)
except requests.exceptions.RequestException as e:
log.error(f"Erreur API Nano Banana: {e}")
send_telegram_alert(f"Erreur edition image: {e}")
return None
def auto_evaluate_gemini_vision(image_path: str, original_prompt: str) -> tuple[bool, str]:
"""
Etape 3.5: Gemini Vision verifie PASS/FAIL et donne raison.
Retourne: (bool passed, str reason)
"""
genai.configure(api_key=GEMINI_KEY)
model = genai.GenerativeModel("gemini-2.0-flash")
img = Image.open(image_path)
response = model.generate_content([
img,
f"""Analyse cette image de produit bijouterie.
Prompt demande: "{original_prompt}"
Verifie:
1. L'image correspond-elle exactement au prompt?
2. Qualite professionnelle bijouterie luxe?
3. Problemes visibles (flou, artefacts, deformations)?
Reponds UNIQUEMENT: PASS ou FAIL, puis une ligne d'explication."""
])
text = response.text.strip()
passed = text.upper().startswith("PASS")
log.info(f"Evaluation: {'PASS' if passed else 'FAIL'} | {text[:100]}")
return passed, text
def process_single_image(instruction: dict, output_dir: str, max_retries: int = 3) -> Optional[str]:
"""
Traitement d'une image avec retry automatique et prompt refinement.
"""
prompt = instruction["prompt_english"]
image_path = instruction["image_path"]
style_notes = instruction.get("style_notes", "professional product photography, 8K quality")
full_prompt = f"{prompt}, {style_notes}"
for attempt in range(max_retries):
result_path = edit_image_nanobananaPro(image_path, full_prompt, output_dir)
if not result_path:
continue
passed, reason = auto_evaluate_gemini_vision(result_path, prompt)
if passed:
log.info(f"Image validee en {attempt + 1} tentative(s)")
return result_path
# Raffiner le prompt a chaque echec
refinements = [
f"{prompt}, ultra-sharp details, perfect lighting, no artifacts",
f"{prompt}, studio lighting, clean background, professional retouching, photorealistic",
f"High-end jewelry photography: {prompt}, masterpiece quality, award-winning composition"
]
full_prompt = f"{refinements[min(attempt, len(refinements)-1)]}, {style_notes}"
log.warning(f"Tentative {attempt + 1} echouee: {reason[:80]}. Nouveau prompt applique.")
time.sleep(2)
log.error(f"Echec apres {max_retries} tentatives pour {Path(image_path).name}")
return None
def process_batch(audio_path: str, image_paths: list[str], output_dir: str = "/tmp/voice_image_output") -> list[str]:
"""
Pipeline complet: audio -> texte -> prompts -> images editees (batch parallele).
"""
Path(output_dir).mkdir(parents=True, exist_ok=True)
# Etape 1: Transcription
raw_text = transcribe_audio(audio_path)
# Etape 2: Parse instructions
instructions = parse_instructions_to_prompts(raw_text, image_paths)
log.info(f"Pipeline: {len(instructions)} images a traiter en parallele")
# Etape 3 + 3.5: Edition + validation (4 workers max)
results = []
with ThreadPoolExecutor(max_workers=4) as executor:
futures = {
executor.submit(process_single_image, inst, output_dir): inst
for inst in sorted(instructions, key=lambda x: x.get("priority", 99))
}
for future in as_completed(futures):
inst = futures[future]
result_path = future.result()
if result_path:
results.append(result_path)
log.info(f"Traite: {Path(inst['image_path']).name} -> {result_path}")
else:
log.error(f"Echec: {Path(inst['image_path']).name}")
log.info(f"Pipeline termine: {len(results)}/{len(instructions)} images reussies")
send_telegram_alert(f"Pipeline termine: {len(results)}/{len(instructions)} images. Output: {output_dir}")
return results
if __name__ == "__main__":
if len(sys.argv) < 3:
print("Usage: python3 voice_to_image_pipeline.py audio.ogg photo1.jpg [photo2.jpg ...]")
sys.exit(1)
audio = sys.argv[1]
photos = sys.argv[2:]
if not Path(audio).exists():
log.error(f"Fichier audio introuvable: {audio}")
sys.exit(1)
results = process_batch(audio, photos)
print(f"Resultats ({len(results)} images):")
for r in results:
print(f" -> {r}")
A2. Landing Page Rapide (< 30 minutes)
Template Next.js minimal, Lighthouse >90 garanti, CTA Stripe Payment Link sans auth.
// app/(marketing)/page.tsx
import { Metadata } from 'next'
export const metadata: Metadata = {
title: '[Nom Service] — [Promesse principale]',
description: '[Bénéfice en 155 caractères max. Pas de blabla.]',
openGraph: { images: ['/og-image.png'] }
}
// Design tokens: adapter selon projet
const DESIGN = {
primary: '#D4AF37', // Luxury
bg: '#000000',
text: '#FFFFFF',
cta: 'https://buy.stripe.com/XXXXXX' // Payment Link Stripe
}
const FEATURES = [
{ icon: '⚡', title: 'Rapide', desc: 'Livraison en 24h, pas en 2 semaines.' },
{ icon: '🎯', title: 'Précis', desc: 'Résultat exact selon vos instructions.' },
{ icon: '🔒', title: 'Garanti', desc: 'Satisfait ou remboursé sans question.' }
]
export default function LandingPage() {
return (
<main className="min-h-screen bg-black text-white font-sans">
{/* Hero */}
<section className="flex flex-col items-center justify-center min-h-screen px-4 text-center">
<h1 className="text-4xl md:text-6xl font-bold mb-6 leading-tight"
style={{ color: DESIGN.primary }}>
[Problème Client<br/>En Gras Ici]
</h1>
<p className="text-xl md:text-2xl text-gray-300 mb-8 max-w-2xl">
[Solution en 1-2 phrases. Concret. Pas de jargon.]
</p>
<a
href={DESIGN.cta}
className="px-8 py-4 rounded-xl text-black text-xl font-bold
hover:scale-105 transition-transform duration-200"
style={{ backgroundColor: DESIGN.primary }}
>
Commencer — [Prix] ₪
</a>
<p className="mt-4 text-sm text-gray-500">Paiement sécurisé · Satisfait ou remboursé</p>
</section>
{/* Features */}
<section className="py-20 px-4">
<div className="max-w-5xl mx-auto grid grid-cols-1 md:grid-cols-3 gap-8">
{FEATURES.map((f) => (
<div key={f.title}
className="p-6 rounded-2xl border border-gray-800 bg-gray-950
hover:border-yellow-600 transition-colors duration-200">
<div className="text-4xl mb-4">{f.icon}</div>
<h3 className="text-xl font-bold mb-2" style={{ color: DESIGN.primary }}>
{f.title}
</h3>
<p className="text-gray-400">{f.desc}</p>
</div>
))}
</div>
</section>
{/* Testimonial */}
<section className="py-16 px-4 bg-gray-950">
<blockquote className="max-w-2xl mx-auto text-center">
<p className="text-2xl italic text-gray-200 mb-4">
"[Résultat concret obtenu par un vrai client. Chiffres si possible.]"
</p>
<cite className="text-gray-500">— [Prénom], [Ville]</cite>
</blockquote>
</section>
{/* CTA Final */}
<section className="py-20 px-4 text-center">
<h2 className="text-3xl font-bold mb-6">Prêt à [résultat désiré] ?</h2>
<a
href={DESIGN.cta}
className="inline-block px-10 py-5 rounded-xl text-black text-xl font-bold
hover:scale-105 transition-transform duration-200"
style={{ backgroundColor: DESIGN.primary }}
>
Oui, je veux [résultat] — [Prix] ₪
</a>
</section>
</main>
)
}
A3. Script Python Automation Template
Template standard réutilisable pour TOUT script d'automatisation ponctuel DreamNova.
#!/usr/bin/env python3
"""
[Nom du script] — [Description en une ligne]
Usage: python3 nom_script.py [args]
Auteur: DreamNova CTO (David Amor)
"""
import os
import sys
import time
import logging
import json
import traceback
from pathlib import Path
from functools import wraps
from typing import Any, Optional
import requests
from dotenv import load_dotenv
load_dotenv(Path.home() / ".env.local")
# === CONFIGURATION ===
TELEGRAM_BOT_TOKEN = os.environ.get("TELEGRAM_BOT_TOKEN", "")
TELEGRAM_CHAT_ID = "7269582214"
MAX_RETRIES = 3
RETRY_DELAY = 2 # secondes
# === LOGGING STANDARD ===
log_file = Path("/tmp") / f"{Path(__file__).stem}_{int(time.time())}.log"
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s [%(levelname)s] %(funcName)s: %(message)s",
handlers=[logging.FileHandler(log_file), logging.StreamHandler()]
)
log = logging.getLogger(__name__)
# === DECORATEUR RETRY ===
def retry(max_attempts: int = MAX_RETRIES, delay: float = RETRY_DELAY):
"""Retry automatique avec backoff exponentiel."""
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
for attempt in range(max_attempts):
try:
return func(*args, **kwargs)
except Exception as e:
if attempt == max_attempts - 1:
raise
wait = delay * (2 ** attempt)
log.warning(f"Tentative {attempt+1}/{max_attempts} echouee: {e}. Retry dans {wait}s")
time.sleep(wait)
return wrapper
return decorator
# === ALERTES TELEGRAM ===
def alert(message: str, level: str = "INFO") -> None:
"""Envoie alerte Telegram. Utiliser pour erreurs critiques uniquement."""
if not TELEGRAM_BOT_TOKEN:
return
emoji = {"INFO": "", "WARNING": "[!]", "ERROR": "[ERREUR]", "SUCCESS": "[OK]"}.get(level, "")
try:
requests.post(
f"https://api.telegram.org/bot{TELEGRAM_BOT_TOKEN}/sendMessage",
json={"chat_id": TELEGRAM_CHAT_ID, "text": f"{emoji} [{Path(__file__).stem}] {message}"},
timeout=5
)
except Exception:
pass
# === OUTPUT VERS FICHIER ===
def save_output(data: Any, filename: Optional[str] = None) -> Path:
"""Sauvegarde output JSON + retourne chemin."""
output_dir = Path("/tmp/dreamnova_outputs")
output_dir.mkdir(exist_ok=True)
outfile = output_dir / (filename or f"{Path(__file__).stem}_output_{int(time.time())}.json")
outfile.write_text(json.dumps(data, ensure_ascii=False, indent=2))
log.info(f"Output sauvegarde: {outfile}")
return outfile
# === FONCTIONS METIER ===
@retry()
def fetch_data(url: str) -> dict:
"""Exemple: fetch avec retry automatique."""
response = requests.get(url, timeout=30)
response.raise_for_status()
return response.json()
def process_item(item: dict) -> dict:
"""Traitement unitaire. Isoler chaque transformation ici."""
# Logique metier ici
return item
def run(args: list[str]) -> int:
"""Point d'entree principal. Retourne 0=succes, 1=erreur."""
log.info(f"Demarrage {Path(__file__).name} avec args: {args}")
start = time.time()
try:
# 1. Validation des args
if not args:
log.error("Aucun argument fourni")
print("Usage: python3 nom_script.py [input]")
return 1
# 2. Traitement principal
results = []
for arg in args:
result = process_item({"input": arg})
results.append(result)
log.info(f"Traite: {arg}")
# 3. Sauvegarder output
outfile = save_output(results)
# 4. Rapport final
duration = time.time() - start
summary = f"{len(results)} elements traites en {duration:.1f}s. Output: {outfile}"
log.info(summary)
alert(summary, "SUCCESS")
return 0
except KeyboardInterrupt:
log.info("Interrompu par utilisateur")
return 0
except Exception as e:
error_msg = f"Erreur fatale: {e}\n{traceback.format_exc()}"
log.error(error_msg)
alert(f"ERREUR CRITIQUE: {e}", "ERROR")
return 1
if __name__ == "__main__":
sys.exit(run(sys.argv[1:]))
PARTIE B — Moyen Projet (2-7 jours)
B1. Architecture Type Moyen Projet
La structure App Router est NON-NÉGOCIABLE pour tous les projets moyens DreamNova. Route groups = séparation claire des responsabilités = CI/CD plus simple.
Règle clé : Chaque route group (nom) peut avoir son propre layout.tsx. Le dashboard a un layout différent du marketing. Ne jamais mélanger.
project-name/
├── app/
│ ├── (marketing)/ # Pages publiques - pas d'auth
│ ├── (shop)/ # E-commerce - auth optionnelle
│ ├── (auth)/ # Login/Register - redirect si déjà connecté
│ ├── (dashboard)/ # Zone protégée - middleware vérifie session
│ └── api/ # API routes - JAMAIS de logique UI ici
├── components/ # Réutilisables UNIQUEMENT si utilisés 2+ fois
├── lib/ # Utilitaires purs (pas de JSX)
├── public/ # Assets statiques WebP/SVG uniquement
└── supabase/migrations/ # UN fichier par migration, numéroté
B2. Schema Supabase Standard
Ce schema couvre 80% des projets moyens DreamNova. L'adapter, jamais repartir de zéro.
-- ============================================================
-- SCHEMA STANDARD DREAMNOVA v2.0
-- Testé sur: Keren Rabbi Yisrael, Esther Ifrah, Baroukh Sagit
-- ============================================================
-- Extension UUID (activée par défaut sur Supabase)
create extension if not exists "uuid-ossp";
-- ============================================================
-- TABLE: profiles (extension de auth.users)
-- ============================================================
create table public.profiles (
id uuid references auth.users on delete cascade not null,
email text unique not null,
full_name text,
phone text,
role text default 'customer' check (role in ('customer', 'admin', 'staff')),
locale text default 'fr' check (locale in ('fr', 'he', 'en', 'es', 'ru', 'ar')),
created_at timestamptz default now() not null,
updated_at timestamptz default now() not null,
primary key (id)
);
create index idx_profiles_email on public.profiles(email);
-- ============================================================
-- TABLE: products
-- ============================================================
create table public.products (
id uuid default gen_random_uuid() primary key,
slug text unique not null,
name_fr text not null,
name_he text,
name_en text,
description text,
price integer not null, -- en agorot (₪ × 100)
compare_price integer, -- prix barre
stock integer default 0,
images text[] default '{}'::text[],
category text,
tags text[] default '{}'::text[],
is_active boolean default true,
metadata jsonb default '{}'::jsonb,
created_at timestamptz default now(),
updated_at timestamptz default now()
);
create index idx_products_slug on public.products(slug);
create index idx_products_category on public.products(category);
create index idx_products_active on public.products(is_active);
-- ============================================================
-- TABLE: orders
-- ============================================================
create table public.orders (
id uuid default gen_random_uuid() primary key,
user_id uuid references public.profiles(id),
-- user_id NULL = commande invité (email stocké dans shipping_address)
stripe_payment_intent_id text unique,
stripe_session_id text unique,
amount integer not null, -- total en agorot
currency text default 'ILS',
status text default 'pending'
check (status in ('pending', 'paid', 'failed', 'refunded', 'shipped', 'delivered')),
shipping_address jsonb, -- {name, email, line1, city, country, zip}
metadata jsonb default '{}'::jsonb,
created_at timestamptz default now(),
updated_at timestamptz default now()
);
create index idx_orders_user on public.orders(user_id);
create index idx_orders_status on public.orders(status);
create index idx_orders_stripe on public.orders(stripe_payment_intent_id);
-- ============================================================
-- TABLE: order_items
-- ============================================================
create table public.order_items (
id uuid default gen_random_uuid() primary key,
order_id uuid references public.orders(id) on delete cascade not null,
product_id uuid references public.products(id),
quantity integer not null default 1,
unit_price integer not null, -- prix au moment de l'achat (snapshot)
total_price integer not null,
metadata jsonb default '{}'::jsonb
);
create index idx_order_items_order on public.order_items(order_id);
-- ============================================================
-- ROW LEVEL SECURITY (RLS) — OBLIGATOIRE sur TOUTES les tables
-- ============================================================
alter table public.profiles enable row level security;
alter table public.products enable row level security;
alter table public.orders enable row level security;
alter table public.order_items enable row level security;
-- Profiles: chaque user voit/modifie uniquement le sien
create policy "profiles_select_own" on public.profiles
for select using (auth.uid() = id);
create policy "profiles_update_own" on public.profiles
for update using (auth.uid() = id);
create policy "profiles_insert_own" on public.profiles
for insert with check (auth.uid() = id);
-- Admins voient tout
create policy "profiles_admin_all" on public.profiles
using (exists (
select 1 from public.profiles p
where p.id = auth.uid() and p.role = 'admin'
));
-- Products: lecture publique, écriture admin uniquement
create policy "products_select_all" on public.products
for select using (is_active = true);
create policy "products_admin_write" on public.products
for all using (exists (
select 1 from public.profiles p
where p.id = auth.uid() and p.role = 'admin'
));
-- Orders: user voit ses commandes, admin voit tout
create policy "orders_select_own" on public.orders
for select using (auth.uid() = user_id);
create policy "orders_insert_authenticated" on public.orders
for insert with check (auth.uid() = user_id or user_id is null);
create policy "orders_admin_all" on public.orders
for all using (exists (
select 1 from public.profiles p
where p.id = auth.uid() and p.role = 'admin'
));
-- ============================================================
-- TRIGGER: updated_at automatique
-- ============================================================
create or replace function public.handle_updated_at()
returns trigger language plpgsql as $$
begin
new.updated_at = now();
return new;
end;
$$;
create trigger profiles_updated_at before update on public.profiles
for each row execute procedure public.handle_updated_at();
create trigger products_updated_at before update on public.products
for each row execute procedure public.handle_updated_at();
create trigger orders_updated_at before update on public.orders
for each row execute procedure public.handle_updated_at();
-- ============================================================
-- TRIGGER: créer profile automatiquement à l'inscription
-- ============================================================
create or replace function public.handle_new_user()
returns trigger language plpgsql security definer set search_path = public as $$
begin
insert into public.profiles (id, email, full_name)
values (
new.id,
new.email,
coalesce(new.raw_user_meta_data->'full_name', '')::text
);
return new;
end;
$$;
create trigger on_auth_user_created
after insert on auth.users
for each row execute procedure public.handle_new_user();
B3. Stripe Integration Standard
// app/api/stripe/checkout/route.ts
import { NextRequest, NextResponse } from 'next'
import Stripe from 'stripe'
import { createServerClient } from '@supabase/ssr'
import { cookies } from 'next/headers'
import { z } from 'zod'
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, {
apiVersion: '2026-01-28.clover'
})
const CheckoutSchema = z.object({
items: z.array(z.object({
productId: z.string().uuid(),
quantity: z.number().int().positive()
})).min(1),
successUrl: z.string().url().optional(),
cancelUrl: z.string().url().optional()
})
export async function POST(request: NextRequest) {
const cookieStore = cookies()
const supabase = createServerClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.SUPABASE_SERVICE_ROLE_KEY!,
{ cookies: { get: (name) => cookieStore.get(name)?.value } }
)
try {
const body = await request.json()
const { items, successUrl, cancelUrl } = CheckoutSchema.parse(body)
// Récupérer les produits depuis Supabase
const productIds = items.map(i => i.productId)
const { data: products, error } = await supabase
.from('products')
.select('id, name_fr, price, images')
.in('id', productIds)
.eq('is_active', true)
if (error || !products?.length) {
return NextResponse.json({ error: 'Produits introuvables' }, { status: 404 })
}
// Construire les line_items Stripe
const line_items = items.map(item => {
const product = products.find(p => p.id === item.productId)!
return {
price_data: {
currency: 'ils',
product_data: {
name: product.name_fr,
images: product.images?.slice(0, 1) ?? []
},
unit_amount: product.price // déjà en agorot
},
quantity: item.quantity
}
})
const session = await stripe.checkout.sessions.create({
mode: 'payment',
line_items,
success_url: successUrl ?? `${process.env.NEXT_PUBLIC_SITE_URL}/success?session_id={CHECKOUT_SESSION_ID}`,
cancel_url: cancelUrl ?? `${process.env.NEXT_PUBLIC_SITE_URL}/checkout`,
payment_method_types: ['card'],
metadata: { items: JSON.stringify(items) }
})
return NextResponse.json({ url: session.url })
} catch (err) {
if (err instanceof z.ZodError) {
return NextResponse.json({ error: 'Données invalides', details: err.errors }, { status: 400 })
}
console.error('Stripe checkout error:', err)
return NextResponse.json({ error: 'Erreur serveur' }, { status: 500 })
}
}
// app/api/stripe/webhook/route.ts
import { NextRequest, NextResponse } from 'next'
import Stripe from 'stripe'
import { createClient } from '@supabase/supabase-js'
import { Resend } from 'resend'
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, { apiVersion: '2026-01-28.clover' })
const supabase = createClient(process.env.NEXT_PUBLIC_SUPABASE_URL!, process.env.SUPABASE_SERVICE_ROLE_KEY!)
const resend = new Resend(process.env.RESEND_API_KEY!)
export async function POST(request: NextRequest) {
const body = await request.text()
const signature = request.headers.get('stripe-signature')!
let event: Stripe.Event
try {
event = stripe.webhooks.constructEvent(body, signature, process.env.STRIPE_WEBHOOK_SECRET!)
} catch {
return NextResponse.json({ error: 'Signature invalide' }, { status: 400 })
}
if (event.type === 'checkout.session.completed') {
const session = event.data.object as Stripe.Checkout.Session
// 1. Créer commande dans Supabase
const { data: order } = await supabase
.from('orders')
.insert({
stripe_session_id: session.id,
stripe_payment_intent_id: session.payment_intent as string,
amount: session.amount_total,
currency: session.currency?.toUpperCase() ?? 'ILS',
status: 'paid',
shipping_address: {
name: session.customer_details?.name,
email: session.customer_details?.email,
}
})
.select()
.single()
// 2. Email confirmation via Resend
if (session.customer_details?.email && order) {
await resend.emails.send({
from: 'DreamNova <noreply@dreamnova.vercel.app>',
to: session.customer_details.email,
subject: `Confirmation commande #${order.id.slice(0, 8).toUpperCase()}`,
html: `<h1>Merci pour votre commande!</h1><p>Montant: ${(session.amount_total! / 100).toFixed(2)} ₪</p>`
})
}
}
return NextResponse.json({ received: true })
}
B4. Performance Checklist CTO (Lighthouse >90 OBLIGATOIRE)
Cette checklist est intégrée dans le CI/CD. Un score < 90 BLOQUE le deploy.
IMAGES (impact: +15-20 pts)
[ ] next/image sur TOUTES les images (jamais <img> directe)
[ ] Format WebP/AVIF avec fallback (next.config.js: formats)
[ ] sizes= défini sur chaque Image (prevent layout shift)
[ ] priority= sur l'image hero above-the-fold uniquement
[ ] lazy loading implicite sur tout le reste (default Next.js)
[ ] max 800x800px pour produits, optimisé en build
[ ] SVG pour icons (jamais PNG/JPG pour icons)
FONTS (impact: +5-10 pts)
[ ] next/font/google ou next/font/local (jamais @import CSS)
[ ] display: 'swap' sur toutes les fonts
[ ] Preload uniquement la font primaire
[ ] Max 2 familles de fonts par projet
CODE SPLITTING (impact: +10-15 pts)
[ ] dynamic(() => import(...)) sur composants non-critiques
[ ] Suspense boundaries avec fallback skeleton
[ ] Route-based code splitting (automatique App Router)
[ ] Barrel exports supprimés (import direct)
CACHE & HEADERS (impact: +5-10 pts)
[ ] Cache-Control: public, max-age=31536000 sur /public/*
[ ] Revalidation Supabase: next: { revalidate: 3600 }
[ ] Static rendering sur toutes les pages sans auth
[ ] generateStaticParams sur pages produits
CSS (impact: +5 pts)
[ ] Tailwind CSS purge activé (automatique en prod)
[ ] Variables CSS pour tokens (pas de valeurs hardcodées)
[ ] No unused CSS classes en prod
[ ] Critical CSS inliné (automatique Next.js)
CORE WEB VITALS
[ ] LCP < 2.5s (image hero optimisée + preload)
[ ] CLS < 0.1 (width/height sur images, skeleton loaders)
[ ] FID/INP < 100ms (pas de calculs lourds sur main thread)
[ ] TTFB < 800ms (SSG ou ISR quand possible)
B5. Deployment Checklist Moyen Projet (20 items)
# Checklist à exécuter DANS CET ORDRE avant chaque deploy production
[ ] 01. TypeScript: npx tsc --noEmit → ZERO erreurs
[ ] 02. Build local: npm run build → ZERO warnings critiques
[ ] 03. Tests Playwright: npx playwright test → PASS
[ ] 04. Env vars Vercel: toutes les variables .env.local.example sont sur Vercel
[ ] 05. Supabase migrations: supabase db push (sur branch preview d'abord)
[ ] 06. RLS policies testées: chaque table a au moins 1 policy
[ ] 07. Stripe webhook URL mise à jour sur dashboard Stripe
[ ] 08. Test paiement: 1 commande test avec carte 4242 4242 4242 4242
[ ] 09. Test email: vérifier réception email confirmation (Resend logs)
[ ] 10. Screenshot mobile: iPhone 12 Chrome DevTools
[ ] 11. Screenshot desktop: 1440px
[ ] 12. Lighthouse: score >90 Performance + Accessibility + SEO
[ ] 13. Meta tags: title + description sur chaque page
[ ] 14. OG image: /public/og-image.png (1200×630)
[ ] 15. 404 page: /app/not-found.tsx personnalisée
[ ] 16. Error boundary: /app/error.tsx définie
[ ] 17. Loading states: /app/loading.tsx sur routes dynamiques
[ ] 18. Git tag: git tag v1.0.0-$(date +%Y%m%d) avant deploy
[ ] 19. Deploy: vercel --prod --token=VCP_TOKEN_REDACTED
[ ] 20. Vérification live: tester les 5 pages critiques en production
PARTIE C — Grand Projet (> 1 semaine)
C1. Architecture Multi-Services
Pour DreamOS, DreamNova Platform, et tout projet > 30K ₪/mois potentiel.
┌─────────────────────────────────────────────────────────┐
│ INTERNET / CDN │
│ Cloudflare (DNS + WAF) │
└────────────────────────┬────────────────────────────────┘
│
┌───────────────┴───────────────┐
│ │
┌────────▼────────┐ ┌────────▼────────┐
│ Next.js (Vercel)│ │ Static Assets │
│ Frontend + API │ │ (Cloudflare R2) │
│ dreamnova.app │ │ CDN worldwide │
└────────┬────────┘ └─────────────────┘
│ HTTP/REST
┌────────▼────────────────────────────────────────┐
│ Supabase (Database Layer) │
│ PostgreSQL + Auth + Storage + Realtime │
│ Row Level Security sur TOUTES les tables │
└────────┬────────────────────────────────────────┘
│
┌────────▼────────────────────────────────────────┐
│ VPS IONOS (66.179.240.214) │
│ Python FastAPI (workers asynchrones) │
│ - Job queue (BullMQ/n8n) │
│ - Batch processing (translations, images) │
│ - Webhooks handlers │
│ - Cron jobs (heartbeat, daily-scan) │
└─────────────────────────────────────────────────┘
│
┌────────▼────────────────────────────────────────┐
│ Monitoring Stack │
│ Sentry (erreurs) + Upstash Redis (rate limit) │
│ Telegram alerts (David @nerostats) │
└─────────────────────────────────────────────────┘
C2. CI/CD Pipeline Complet
# .github/workflows/deploy.yml
name: DreamNova Deploy Pipeline
on:
push:
branches: [main]
pull_request:
branches: [main]
env:
VERCEL_TOKEN: ${{ secrets.VERCEL_TOKEN }}
TELEGRAM_BOT_TOKEN: ${{ secrets.TELEGRAM_BOT_TOKEN }}
TELEGRAM_CHAT_ID: "7269582214"
jobs:
# ============================================================
# JOB 1: TypeScript + Lint
# ============================================================
typecheck:
name: TypeScript & Lint
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with: { node-version: '20', cache: 'npm' }
- run: npm ci
- name: TypeScript check (ZERO errors)
run: npx tsc --noEmit
- name: ESLint
run: npm run lint
# ============================================================
# JOB 2: Build
# ============================================================
build:
name: Next.js Build
needs: typecheck
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with: { node-version: '20', cache: 'npm' }
- run: npm ci
- name: Build
run: npm run build
env:
NEXT_PUBLIC_SUPABASE_URL: ${{ secrets.NEXT_PUBLIC_SUPABASE_URL }}
NEXT_PUBLIC_SUPABASE_ANON_KEY: ${{ secrets.NEXT_PUBLIC_SUPABASE_ANON_KEY }}
- name: Upload build artifacts
uses: actions/upload-artifact@v4
with:
name: build-output
path: .next/
# ============================================================
# JOB 3: Tests Playwright E2E
# ============================================================
e2e:
name: Playwright E2E Tests
needs: build
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with: { node-version: '20', cache: 'npm' }
- run: npm ci
- name: Install Playwright browsers
run: npx playwright install chromium --with-deps
- name: Run E2E tests
run: npx playwright test
env:
BASE_URL: 'http://localhost:3000'
TEST_STRIPE_KEY: ${{ secrets.STRIPE_TEST_KEY }}
- uses: actions/upload-artifact@v4
if: failure()
with:
name: playwright-report
path: playwright-report/
# ============================================================
# JOB 4: Lighthouse CI
# ============================================================
lighthouse:
name: Lighthouse Score >90
needs: build
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with: { node-version: '20', cache: 'npm' }
- run: npm ci && npm run build
- run: npm start &
- name: Lighthouse CI
uses: treosh/lighthouse-ci-action@v11
with:
urls: |
http://localhost:3000
http://localhost:3000/pricing
budgetPath: .github/lighthouse-budget.json
uploadArtifacts: true
# ============================================================
# JOB 5: Deploy to Vercel (main branch uniquement)
# ============================================================
deploy:
name: Deploy to Production
needs: [e2e, lighthouse]
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with: { node-version: '20' }
- run: npm install -g vercel
- name: Deploy to Vercel Production
run: vercel --prod --token=$VERCEL_TOKEN --yes
- name: Tag release
run: |
git config user.email "ci@dreamnova.app"
git config user.name "DreamNova CI"
git tag "v$(date +%Y%m%d-%H%M%S)"
git push --tags
# ============================================================
# JOB 6: Notification Telegram David
# ============================================================
notify:
name: Notify David
needs: deploy
runs-on: ubuntu-latest
if: always()
steps:
- name: Send Telegram notification
run: |
STATUS="${{ needs.deploy.result }}"
if [ "$STATUS" = "success" ]; then
MSG="[DEPLOY OK] $(git log -1 --pretty=format:\'%s\') deploye en production"
else
MSG="[DEPLOY FAIL] Echec deploy: ${{ github.sha }}"
fi
curl -s -X POST \
"https://api.telegram.org/bot${TELEGRAM_BOT_TOKEN}/sendMessage" \
-d "chat_id=${TELEGRAM_CHAT_ID}&text=${MSG}"
C3. Monitoring & Auto-Repair
# ~/scripts/monitoring/sentry_auto_repair.py
"""
Sentry Webhook -> Gemini analyse -> PR GitHub automatique
Principe: erreur detectee -> analyse automatique -> suggestion de fix -> PR pour David
"""
import os
import json
import hashlib
import requests
from fastapi import FastAPI, Request, HTTPException
from fastapi.responses import JSONResponse
import google.generativeai as genai
app = FastAPI()
GITHUB_TOKEN = os.environ["GITHUB_TOKEN"]
REPO = "CodeNoLimits/dreamnova-boilerplate"
GEMINI_KEY = os.environ.get("GEMINI_API_KEY", "AIzaSyDZ29PmIKVBj_YS86MsX5FJ0jaXyQyebts")
TELEGRAM_CHAT_ID = "7269582214"
TELEGRAM_TOKEN = os.environ["TELEGRAM_BOT_TOKEN"]
def notify_david(message: str) -> None:
requests.post(
f"https://api.telegram.org/bot{TELEGRAM_TOKEN}/sendMessage",
json={"chat_id": TELEGRAM_CHAT_ID, "text": message},
timeout=5
)
def analyze_error_with_gemini(error_data: dict) -> str:
"""Gemini analyse l'erreur Sentry et propose un fix."""
genai.configure(api_key=GEMINI_KEY)
model = genai.GenerativeModel("gemini-2.0-flash")
prompt = f"""
Erreur Sentry en production (Next.js/TypeScript/Supabase):
Message: {error_data.get('message', '')}
Type: {error_data.get('type', '')}
Stack trace:
{error_data.get('stacktrace', '')}
Contexte: App Next.js avec Supabase et Stripe.
Fournis:
1. Cause probable (1 phrase)
2. Fix exact (code TypeScript ready-to-paste)
3. Fichier a modifier
4. Test a faire pour valider le fix
"""
response = model.generate_content(prompt)
return response.text
def create_github_pr(fix_description: str, error_id: str) -> str:
"""Crée une PR GitHub avec le fix suggéré."""
headers = {
"Authorization": f"token {GITHUB_TOKEN}",
"Accept": "application/vnd.github.v3+json"
}
# Créer une branche
branch_name = f"fix/sentry-{error_id[:8]}"
base_sha = requests.get(
f"https://api.github.com/repos/{REPO}/git/refs/heads/main",
headers=headers
).json()["object"]["sha"]
requests.post(
f"https://api.github.com/repos/{REPO}/git/refs",
headers=headers,
json={"ref": f"refs/heads/{branch_name}", "sha": base_sha}
)
# Créer la PR
pr = requests.post(
f"https://api.github.com/repos/{REPO}/pulls",
headers=headers,
json={
"title": f"fix: auto-repair Sentry #{error_id[:8]}",
"body": f"## Fix automatique\n\n{fix_description}\n\n_Généré automatiquement par DreamNova Auto-Repair_",
"head": branch_name,
"base": "main"
}
).json()
return pr.get("html_url", "")
@app.post("/webhook/sentry")
async def sentry_webhook(request: Request):
"""Handler webhook Sentry."""
payload = await request.json()
event = payload.get("data", {}).get("event", {})
if not event:
raise HTTPException(status_code=400, detail="Payload invalide")
error_id = hashlib.md5(
f"{event.get('type', '')}{event.get('message', '')}".encode()
).hexdigest()
# Analyse Gemini
fix = analyze_error_with_gemini({
"message": event.get("message", ""),
"type": event.get("type", ""),
"stacktrace": str(event.get("exception", {}).get("values", [{}])[0].get("stacktrace", ""))
})
# Créer PR
pr_url = create_github_pr(fix, error_id)
# Notifier David
notify_david(
f"[AUTO-REPAIR] Erreur Sentry detectee\n"
f"Type: {event.get('type', '')}\n"
f"PR creee: {pr_url}\n"
f"Valider sur GitHub avant merge."
)
return JSONResponse({"status": "PR created", "url": pr_url})
C4. Database Migration Strategy
#!/bin/bash
# ~/scripts/db/safe_migration.sh
# Usage: ./safe_migration.sh 002_add_variants.sql "Add product variants table"
MIGRATION_FILE="$1"
DESCRIPTION="$2"
SUPABASE_PROJECT="rhwthxmphohqdawglhfde" # Adapter par projet
if [ -z "$MIGRATION_FILE" ] || [ -z "$DESCRIPTION" ]; then
echo "Usage: ./safe_migration.sh migration.sql 'Description'"
exit 1
fi
echo "[1/5] Backup avant migration..."
supabase db dump --project-ref "$SUPABASE_PROJECT" > "/tmp/backup_$(date +%Y%m%d_%H%M%S).sql"
echo "Backup OK: /tmp/backup_$(date +%Y%m%d_%H%M%S).sql"
echo "[2/5] Test sur branch Supabase preview..."
supabase db push --dry-run
echo "[3/5] Application migration..."
supabase db push
echo "[4/5] Regeneration types TypeScript..."
npx supabase gen types typescript --project-id "$SUPABASE_PROJECT" > lib/database.types.ts
echo "[5/5] Commit migration..."
git add supabase/migrations/ lib/database.types.ts
git commit -m "chore(db): $DESCRIPTION"
echo "Migration terminee. Tester les API routes critiques maintenant."
Règles absolues migrations :
- Jamais supprimer de colonne en phase 1 — marquer comme
deprecateden commentaire - Toujours additive — ajouter colonnes nullable ou avec DEFAULT
- Test sur preview Supabase avant de toucher la production
- Backup automatique avant chaque migration (script ci-dessus)
- Types TypeScript regénérés immédiatement après toute migration
C5. Security Grand Projet
// middleware.ts — Rate limiting + session Supabase
import { NextRequest, NextResponse } from 'next'
import { createServerClient } from '@supabase/ssr'
import { Ratelimit } from '@upstash/ratelimit'
import { Redis } from '@upstash/redis'
const ratelimit = new Ratelimit({
redis: Redis.fromEnv(),
limiter: Ratelimit.slidingWindow(100, '1 m'), // 100 req/min par IP
analytics: true
})
export async function middleware(request: NextRequest) {
// 1. Rate limiting sur routes API
if (request.nextUrl.pathname.startsWith('/api/')) {
const ip = request.ip ?? '127.0.0.1'
const { success, limit, remaining } = await ratelimit.limit(ip)
if (!success) {
return NextResponse.json(
{ error: 'Too many requests' },
{ status: 429, headers: { 'X-RateLimit-Limit': String(limit), 'X-RateLimit-Remaining': String(remaining) } }
)
}
}
// 2. Session Supabase refresh automatique
const response = NextResponse.next()
const supabase = createServerClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
{ cookies: { get: (name) => request.cookies.get(name)?.value, set: (name, value, options) => { response.cookies.set({ name, value, ...options }) } } }
)
await supabase.auth.getUser()
// 3. Protéger routes dashboard
if (request.nextUrl.pathname.startsWith('/(dashboard)')) {
const { data: { user } } = await supabase.auth.getUser()
if (!user) {
return NextResponse.redirect(new URL('/login', request.url))
}
}
return response
}
export const config = {
matcher: ['/api/:path*', '/(dashboard)/:path*']
}
# .github/hooks/pre-commit — Bloquer secrets dans les commits
#!/bin/bash
# Installer: cp .github/hooks/pre-commit .git/hooks/pre-commit && chmod +x .git/hooks/pre-commit
SECRETS_PATTERN="sk_live_|whsec_|eyJ|AIzaSy|vcp_|re_[A-Za-z0-9]"
if git diff --cached --name-only | xargs grep -l -E "$SECRETS_PATTERN" 2>/dev/null; then
echo "[ERREUR] Secrets potentiels detectes dans les fichiers staged!"
echo "Verifier: .env, .env.local, config files"
echo "Utiliser: git reset HEAD <file> pour unstager"
exit 1
fi
exit 0
RÈGLES D'OR CTO (10 Non-Négociables)
Ces règles sont gravées. Elles ne sont pas des suggestions. Chaque violation a coûté du temps, de l'argent, ou un client DreamNova.
1. Screenshot AVANT → Gemini Vision → Modification → Screenshot APRÈS
Pourquoi : Le site Keren a été détruit le 12 fév 2026 en travaillant sans validation visuelle.
Comment : openclaw browser screenshot avant + après. Gemini Vision compare. Si degradation → rollback immédiat.
2. TypeScript strict : ZERO any, ZERO @ts-ignore
Chaque any est une bombe à retardement. Exception unique : interface externe mal typée → créer un type wrapper documenté avec // REASON:.
3. Backup AVANT toute migration DB
supabase db dump --project-ref PROJECT_ID > backup_$(date +%Y%m%d_%H%M%S).sql
Pas de backup = pas de migration. La règle est absolue.
4. Git tag AVANT chaque deploy production
git tag "v$(date +%Y%m%d-%H%M%S)" && git push --tags
vercel --prod --token=VCP_TOKEN_REDACTED
Rollback en 30 secondes : vercel rollback --token=...
5. Docker sandbox pour tout agent avec accès shell
OpenClaw, Gemini Junior, tout agent autonome → jamais d'accès direct au filesystem de production. Sandbox Docker avec volume /output uniquement.
6. Lighthouse >90 BLOQUE le deploy
Intégré dans .github/workflows/deploy.yml. Pas de bypass. Score 89 = fix avant deploy, pas d'exception.
7. Variables d'env : JAMAIS hardcodées, JAMAIS commitées
# Vérification avant commit (hook pre-commit)
grep -r "sk_live_\|whsec_\|AIzaSy" src/ && echo "SECRET DETECTED" && exit 1
Le token Vercel permanent dans ce document est accepté uniquement en doc interne.
8. Test paiement Stripe mode Test AVANT prod
Carte test : 4242 4242 4242 4242, date future, CVC quelconque.
Tester : succès, échec, remboursement. Si un des 3 échoue → pas de deploy.
9. Mobile test iPhone 12 Chrome DevTools AVANT livraison client
Résolution : 390×844. Vérifier : navigation, CTA visibles, formulaires cliquables, images chargées, pas de scroll horizontal.
10. Rétro-engineering : chaque nouveau problème résolu → script réutilisable
Problème résolu → ~/scripts/workflows/NOM_DU_PROBLEME.py
Répertoire ~/scripts/workflows/ = mémoire technique DreamNova. Auditer mensuellement.
APPENDICE — Commandes Rapides CTO
# Setup nouveau projet
dn-init nom-projet
# Deploy production (token permanent)
vercel --prod --token=VCP_TOKEN_REDACTED
# Rollback deploy
vercel rollback --token=VCP_TOKEN_REDACTED
# Migration DB avec backup
~/scripts/db/safe_migration.sh supabase/migrations/002_feature.sql "Description"
# Regenerer types Supabase
npx supabase gen types typescript --project-id PROJECT_ID > lib/database.types.ts
# Lighthouse local
npx lighthouse http://localhost:3000 --output=html --output-path=/tmp/report.html
# Screenshot production via OpenClaw
openclaw browser screenshot --url https://projet.vercel.app --output /tmp/check.png
# Check TypeScript
npx tsc --noEmit
# Pipeline voice->image
python3 ~/scripts/workflows/voice_to_image_pipeline.py message.ogg photo.jpg
# Alert Telegram David
openclaw message send --channel telegram --target 7269582214 --message "Texte"
Document maintenu par DreamNova CTO — David Amor, Jérusalem
Dernière mise à jour: 2026-02-19
Code: [NOVA:CTO-PMG-V1-20260219]