Ce projet a été initialisé avec Next.js 15, TypeScript, Drizzle ORM, Better-auth, et toutes les dépendances nécessaires pour créer une plateforme SaaS B2B de gestion de flotte de location de véhicules.
carblyV3/
├── app/
│ ├── (auth)/
│ │ ├── login/
│ │ └── signup/
│ ├── (dashboard)/
│ │ ├── dashboard/
│ │ ├── vehicles/
│ │ ├── reservations/
│ │ ├── customers/
│ │ └── settings/
│ ├── (public)/
│ │ └── reservation/
│ ├── admin/
│ ├── api/
│ │ ├── auth/
│ │ ├── webhooks/
│ │ │ ├── stripe/
│ │ │ └── yousign/
│ │ ├── cron/
│ │ └── upload/
│ ├── globals.css
│ ├── layout.tsx
│ └── page.tsx (Landing Page)
├── components/
│ ├── ui/ (Button, Card, Input)
│ ├── dashboard/
│ ├── vehicles/
│ ├── reservations/
│ └── emails/
├── drizzle/
│ └── schema.ts (Schema complet)
├── lib/
│ ├── auth.ts (Better-auth config)
│ ├── db.ts (Drizzle client)
│ ├── stripe.ts (Configuration Stripe)
│ ├── r2.ts (Cloudflare R2)
│ ├── resend.ts (Email templates)
│ ├── twilio.ts (SMS)
│ └── utils.ts (Helpers)
├── package.json
├── tsconfig.json
├── tailwind.config.ts
├── next.config.js
├── drizzle.config.ts
├── .env.example
└── .gitignore
✅ Tables créées :
organizations- Comptes facturables Stripeteams- Agences de location (1 par plan)users- Employés des agencesteamMembers- Relation users <-> teamsvehicles- Véhicules de la flottecustomers- Clients finauxreservations- Locationscontracts- Contrats signés Yousignpayments- Paiements Stripe
✅ Enums définis :
plan: starter, pro, businessvehicle_status: available, rented, maintenance, out_of_servicereservation_status: draft, pending_payment, paid, confirmed, in_progress, completed, cancelledpayment_type: deposit, total, caution, insurancepayment_status: pending, succeeded, failed, refunded
✅ Relations configurées avec Drizzle ORM
✅ Stripe (lib/stripe.ts) :
- Configuration client Stripe
- Helpers pour créer des checkouts d'abonnement
- Helpers pour paiements de réservation
- Pré-autorisation caution (Pro+)
- Stripe Identity (Pro+)
- Plans définis (Starter €49, Pro €99, Business €199)
✅ Cloudflare R2 (lib/r2.ts) :
- Upload de fichiers (images, PDFs)
- Génération de presigned URLs
- Download et suppression
✅ Resend (lib/resend.ts) :
- Templates emails pour :
- Lien de paiement réservation
- Confirmation paiement
- Contrat signé
- Rappel restitution (J-1)
✅ Twilio (lib/twilio.ts) :
- Envoi SMS pour notifications critiques
- Templates pour paiement confirmé, contrat signé, rappel restitution
✅ Better-auth (lib/auth.ts) :
- Configuration authentification session-based
- Multi-tenant avec
currentTeamId - Support SuperAdmin
✅ Landing Page (app/page.tsx) :
- Hero section
- Features
- Pricing (avec les 3 plans)
- CTA
- Footer
- Mobile-first, responsive
✅ Pages Auth :
app/(auth)/login/page.tsx- Connexionapp/(auth)/signup/page.tsx- Inscription- Formulaires avec validation
- Gestion erreurs
- Redirection selon rôle (SuperAdmin → /admin, User → /dashboard ou /onboarding)
✅ Composants créés :
components/ui/button.tsx- Bouton avec variantscomponents/ui/card.tsx- Card avec Header, Content, Footercomponents/ui/input.tsx- Input avec styles
✅ lib/utils.ts :
cn()- Merge classes TailwindformatDate()- Formater dates (français)formatCurrency()- Formater montants (EUR)generateRandomToken()- Tokens aléatoirescalculateDays()- Calcul jours entre datescalculateRentalPrice()- Calcul prix location
✅ Fichiers de configuration :
package.json- Scripts npm (dev, build, db:push, etc.)tsconfig.json- TypeScript strict modetailwind.config.ts- Thème personnalisé (bleu #3B82F6)next.config.js- Images remote patterns (R2)drizzle.config.ts- Configuration Drizzlepostcss.config.js- PostCSS.env.example- Variables d'environnement documentées.gitignore- Fichiers à ignorer
Fichiers à créer :
app/api/auth/signup/route.tsapp/api/auth/login/route.tsapp/api/auth/logout/route.ts
Instructions :
// app/api/auth/signup/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { db } from '@/lib/db';
import { users } from '@/drizzle/schema';
import bcrypt from 'bcryptjs';
export async function POST(req: NextRequest) {
try {
const { name, email, password } = await req.json();
// Check if user exists
const existingUser = await db.query.users.findFirst({
where: (users, { eq }) => eq(users.email, email),
});
if (existingUser) {
return NextResponse.json(
{ error: 'Email already exists' },
{ status: 400 }
);
}
// Hash password
const passwordHash = await bcrypt.hash(password, 10);
// Create user
const [newUser] = await db.insert(users).values({
name,
email,
passwordHash,
}).returning();
// TODO: Create session with Better-auth
return NextResponse.json({ user: newUser });
} catch (error) {
return NextResponse.json(
{ error: 'Signup failed' },
{ status: 500 }
);
}
}Répéter pour login et logout.
Page à créer : app/onboarding/page.tsx
Étapes :
- Créer organization (nom)
- Créer première team (nom agence)
- Choisir plan (Starter/Pro/Business)
- Setup paiement Stripe (SEPA ou carte)
- Confirmation → Redirect /dashboard
Instructions :
- Utiliser un state machine (étapes 1-5)
- Server Actions pour créer org, team
- Stripe Checkout pour paiement
- Stocker
stripeSubscriptionIddansteams
Fichier à créer : middleware.ts
Instructions :
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
// import { getSession } from '@/lib/auth';
export async function middleware(req: NextRequest) {
// const session = await getSession(req);
// Routes publiques
if (req.nextUrl.pathname.startsWith('/reservation/')) {
return NextResponse.next();
}
// SuperAdmin routes
if (req.nextUrl.pathname.startsWith('/admin')) {
// if (!session?.user?.isSuperAdmin) {
// return NextResponse.redirect(new URL('/dashboard', req.url));
// }
}
// Protected routes
// if (!session && !req.nextUrl.pathname.startsWith('/login')) {
// return NextResponse.redirect(new URL('/login', req.url));
// }
return NextResponse.next();
}
export const config = {
matcher: ['/((?!api|_next/static|_next/image|favicon.ico).*)'],
};Fichier à créer : app/(dashboard)/dashboard/page.tsx
Server Actions à créer : app/(dashboard)/dashboard/actions.ts
KPIs à afficher :
- CA du mois (€)
- Réservations en cours (nombre)
- Taux d'occupation (%)
- Véhicules disponibles
Graphiques :
- CA des 6 derniers mois (line chart avec recharts)
- Réservations par véhicule (bar chart)
Instructions :
// actions.ts
'use server';
import { db } from '@/lib/db';
import { reservations, payments, vehicles } from '@/drizzle/schema';
import { sql, and, eq, gte } from 'drizzle-orm';
export async function getDashboardStats(teamId: string) {
const now = new Date();
const startOfMonth = new Date(now.getFullYear(), now.getMonth(), 1);
// CA du mois
const revenue = await db
.select({ total: sql<number>`SUM(${payments.amount})` })
.from(payments)
.innerJoin(reservations, eq(payments.reservationId, reservations.id))
.where(
and(
eq(reservations.teamId, teamId),
gte(payments.paidAt, startOfMonth),
eq(payments.status, 'succeeded')
)
);
// ... autres KPIs
return { revenue: revenue[0]?.total || 0, ... };
}Fichiers à créer :
app/(dashboard)/vehicles/page.tsx- Liste véhiculesapp/(dashboard)/vehicles/new/page.tsx- Ajouter véhiculeapp/(dashboard)/vehicles/[id]/page.tsx- Éditer véhiculeapp/(dashboard)/vehicles/actions.ts- Server Actions
Features :
- Liste/Grid avec filtres (statut, marque)
- Form ajout : Marque, Modèle, Année, Immat, VIN, Prix, etc.
- Upload images vers R2 (4 angles + intérieur + carte grise)
- Édition, suppression
Instructions upload images :
- Client obtient presigned URL via Server Action
- Client upload direct vers R2 avec fetch()
- Client envoie URL finale au serveur
- Serveur stocke dans
vehicle.images(jsonb)
Fichiers à créer :
app/(dashboard)/reservations/page.tsx- Liste réservationsapp/(dashboard)/reservations/new/page.tsx- Créer réservation (5 steps)app/(dashboard)/reservations/[id]/page.tsx- Détails réservationapp/(dashboard)/reservations/actions.ts- Server Actions
Flow création réservation :
- Choisir véhicule (+ vérifier dispo)
- Dates (start/end) + calcul prix auto
- Client (search ou créer nouveau)
- Options (acompte, assurance, caution si Pro+)
- Confirmation → Créer résa + envoyer email avec magic link
Server Action :
'use server';
export async function createReservation(data) {
// 1. Vérifier conflit dates
// 2. Créer customer si nouveau
// 3. Générer magicLinkToken
// 4. Créer reservation (status: pending_payment)
// 5. Envoyer email via Resend
}Fichier à créer : app/(public)/reservation/[token]/page.tsx
Flow :
- Récupérer résa via token
- Afficher détails (véhicule, dates, montant)
- Si client nouveau + plan ≥ Pro : Bouton "Vérifier identité" (Stripe Identity)
- Stripe Checkout Session (montant + fee 0.99€)
- Si caution activée : Créer payment_intent manual capture
- Après paiement : Générer contrat PDF + envoyer à Yousign
Instructions Stripe Checkout :
const session = await stripe.checkout.sessions.create({
mode: 'payment',
payment_method_types: ['card', 'sepa_debit'],
line_items: [
{ price_data: { ... }, quantity: 1 }, // Montant résa
{ price_data: { ... }, quantity: 1 }, // Fee 0.99€
],
metadata: { reservationId, teamId },
success_url: `${process.env.NEXT_PUBLIC_URL}/reservation/${token}/success`,
cancel_url: `${process.env.NEXT_PUBLIC_URL}/reservation/${token}`,
});Fichier à créer : lib/pdf/contract.tsx
Instructions :
import { Document, Page, Text, View, StyleSheet } from '@react-pdf/renderer';
export const ContractPDF = ({ reservation, vehicle, customer, team }) => (
<Document>
<Page size="A4" style={styles.page}>
<View style={styles.header}>
<Text>CONTRAT DE LOCATION</Text>
<Text>{team.name}</Text>
</View>
{/* Parties, dates, montants, conditions */}
</Page>
</Document>
);
// Server Action
export async function generateContract(reservationId: string) {
const data = await fetchReservationData(reservationId);
const pdfBlob = await pdf(<ContractPDF {...data} />).toBlob();
const pdfBuffer = Buffer.from(await pdfBlob.arrayBuffer());
const pdfUrl = await uploadToR2({ file: pdfBuffer, path: `contracts/${reservationId}.pdf`, ... });
return pdfUrl;
}Fichier à créer : lib/yousign.ts
Instructions :
// npm install @yousign/yousign-api
import { Yousign } from '@yousign/yousign-api';
const yousign = new Yousign(process.env.YOUSIGN_API_KEY);
export async function createSignatureRequest(contractPdfUrl, customer) {
const signatureRequest = await yousign.signatureRequests.create({
name: `Contrat location`,
delivery_mode: 'email',
documents: [{ url: contractPdfUrl }],
signers: [{
info: {
first_name: customer.firstName,
last_name: customer.lastName,
email: customer.email,
},
signature_level: 'electronic_signature',
signature_authentication_mode: 'otp_sms',
phone_number: customer.phone,
}],
});
return signatureRequest;
}Ajouts dans : app/(dashboard)/reservations/[id]/page.tsx
Features :
- Bouton "Check-in" (si status = paid)
- Modal avec upload photos (R2), kilométrage, carburant
- Update reservation : checkinAt, checkinPhotos, checkinMileage, checkinFuelLevel
- Status → in_progress
Similaire pour Check-out.
Fichier à créer : app/api/webhooks/stripe/route.ts
Événements à écouter :
checkout.session.completed→ Update résa status → Générer contratpayment_intent.succeeded→ Confirmationcustomer.subscription.created/updated/deleted→ Gérer abonnements teams
Instructions :
export async function POST(req: Request) {
const body = await req.text();
const sig = req.headers.get('stripe-signature')!;
let event;
try {
event = stripe.webhooks.constructEvent(body, sig, process.env.STRIPE_WEBHOOK_SECRET!);
} catch (err) {
return new Response('Webhook signature verification failed', { status: 400 });
}
switch (event.type) {
case 'checkout.session.completed':
// Handle payment success
break;
// ...
}
return new Response('OK');
}Fichier à créer : app/api/webhooks/yousign/route.ts
Événement : signature_request.done
- Télécharger PDF signé depuis Yousign
- Upload sur R2
- Update contract (signedAt, signedPdfUrl)
- Update reservation status → confirmed
- Envoyer email confirmation
Fichier : app/(dashboard)/customers/page.tsx
- Liste clients (email, nom, nb locations)
- Filtres (verified, loyalty points)
- Fiche client détaillée
Fichier : app/(dashboard)/settings/page.tsx
Sections :
- Mon compte (email, password)
- Mon agence (nom, logo, plan)
- Facturation (Stripe Portal)
- Notifications (toggle email/SMS)
Fichiers : app/admin/*
- Dashboard global (nb orgs, teams, CA total)
- Liste organizations
- Liste teams avec détails
- Analytics (MRR, churn)
Fichier : app/api/cron/reminders/route.ts
- Rappel restitution J-1 (email + SMS)
- Run quotidien via Vercel Cron
À créer dans components/ui/ :
dialog.tsx- Modalsdropdown-menu.tsx- Dropdownsselect.tsx- Select inputstextarea.tsx- Textareasbadge.tsx- Badges de statutcalendar.tsx- Date pickertable.tsx- Tableaux
Fichier : app/(dashboard)/layout.tsx
- Sidebar avec navigation
- Header avec user menu
- Badge plan actuel
- Mobile : bottom nav + hamburger
- Créer un compte sur Neon
- Créer un projet
- Copier
DATABASE_URLdans.env - Exécuter migrations :
npm run db:push- Créer compte Stripe
- Mode test : Copier clés API dans
.env - Créer 3 Products dans Dashboard :
- Starter (€49/mois)
- Pro (€99/mois)
- Business (€199/mois)
- Copier Price IDs dans
.env - Configurer webhooks :
- URL :
https://votre-domaine.com/api/webhooks/stripe - Événements :
checkout.session.completed,payment_intent.succeeded,customer.subscription.*
- URL :
- Créer compte Cloudflare
- Créer bucket R2 "carbly-uploads"
- Générer API tokens (R2 Read & Write)
- Copier credentials dans
.env
- Créer compte Resend
- Ajouter domaine (ex: carbly.com)
- Configurer DNS (SPF, DKIM)
- Copier API key dans
.env
- Créer compte Twilio
- Acheter numéro français
- Copier Account SID, Auth Token dans
.env
- Créer compte Yousign
- Mode sandbox : Copier API key dans
.env - Configurer webhook :
https://votre-domaine.com/api/webhooks/yousign
Créer .env à partir de .env.example :
cp .env.example .envRemplir toutes les variables.
# Installer dépendances
npm install
# Appliquer migrations DB
npm run db:push
# Lancer dev server
npm run devOuvrir http://localhost:3000
- Push code sur GitHub
- Importer projet sur Vercel
- Configurer variables d'environnement
- Déployer
Vercel Cron :
Créer vercel.json :
{
"crons": [{
"path": "/api/cron/reminders",
"schedule": "0 10 * * *"
}]
}- Primary :
#3B82F6(bleu) - Success :
#10B981(vert) - Destructive :
#EF4444(rouge)
- Font : Inter (Geist possible)
- Tailles : text-sm, text-base, text-lg, text-xl, text-2xl
- Mobile-first
- Container : max-w-7xl
- Padding : p-4 (mobile), p-6 (desktop)
- Toujours
'use server';en haut - Vérifier session au début
- Filter par
teamId(RLS) - Gestion erreurs try/catch
- Seulement si interactivité nécessaire
'use client';en haut- Loading states
- Error boundaries
- Components : PascalCase
- Files : kebab-case
- Variables : camelCase
- Constants : UPPER_SNAKE_CASE
console.log('[DEBUG]', variable);npm run db:studioOuvrir https://local.drizzle.studio
stripe listen --forward-to localhost:3000/api/webhooks/stripe- Configurer toutes les variables d'environnement
- Tester flow complet (signup → onboarding → dashboard → créer réservation → paiement → signature)
- Vérifier webhooks Stripe et Yousign
- Configurer domaine custom
- Activer SSL
- Setup Sentry pour monitoring erreurs
- Activer mode production Stripe
- RGPD : Mentions légales, politique confidentialité
- Limiter rate limiting (Vercel ou Upstash)
- Backup BDD (Neon)
- Auth complète (API routes + middleware) ← CRITIQUE
- Onboarding (org → team → plan → Stripe) ← CRITIQUE
- Dashboard (KPIs) ← IMPORTANT
- CRUD Véhicules ← IMPORTANT
- Créer réservation (agence) ← IMPORTANT
- Lien magique client + paiement ← IMPORTANT
- Génération contrat PDF ← IMPORTANT
- Yousign signature ← IMPORTANT
- Webhooks Stripe/Yousign ← IMPORTANT
- Check-in/out ← MOYEN
- Page Clients ← MOYEN
- Page Paramètres ← MOYEN
- SuperAdmin ← BAS
- Cron jobs ← BAS
- Tester au fur et à mesure : Ne pas attendre d'avoir tout fini pour tester
- Commencer simple : MVP d'abord, features avancées ensuite
- Logs partout : console.log est votre ami
- Stripe test mode : Utiliser cartes de test (4242 4242 4242 4242)
- Webhooks locaux : Utiliser Stripe CLI ou ngrok
- DB migrations : Toujours sauvegarder avant de modifier le schema
- Git commit souvent : Petits commits fréquents
Pour toute question sur l'implémentation :
- Vérifier la documentation officielle
- Chercher sur Stack Overflow
- Tester avec des données de test
- Utiliser Drizzle Studio pour inspecter la DB
Bon courage pour l'implémentation ! 🚀
Ce projet est ambitieux mais la structure de base est solide. Suivez l'ordre de priorité et testez chaque feature avant de passer à la suivante.