Plateforme web belge de mise en relation particuliers ↔ artisans. Modèle pay-per-lead avec wallet rechargeable côté pro.
DevisRapide met en relation des particuliers cherchant un artisan en Belgique avec des professionnels validés par la plateforme. Le particulier soumet sa demande via un wizard en 3 étapes (projet → infos → coordonnées), et le système propose le lead à tous les artisans validés de la zone via un algorithme de matching géographique (Haversine SQL custom), le rayon s'élargissant progressivement par paliers. Un nombre limité et configurable d'entre eux peut l'acheter depuis son wallet rechargeable (Stripe Checkout) : plusieurs pour un lead partagé, un seul pour un lead exclusif.
Acteurs :
- Client (particulier) — pas de compte authentifié en V1, anonyme
- Pro (artisan) — compte auth, wallet, dashboard, leads acceptés / refusés
- Admin — panel /admin pour validation pros, lifecycle, wallet override, stats
| Couche | Technos |
|---|---|
| Framework | Next.js 16 (App Router, Turbopack, Server Components) + React 19 |
| Langage | TypeScript strict, Zod pour la validation runtime |
| Styling | Tailwind v4 (@theme inline), shadcn/ui primitives, Phosphor icons, Bricolage Grotesque |
| Base de données | PostgreSQL (Neon) + Prisma 6 |
| Auth | Auth.js v5 + Prisma adapter (Credentials provider, JWT strategy) |
| Paiement | Stripe Checkout one-time + webhook idempotent (StripeWebhookEvent.stripeEventId @unique) |
| Animation | CSS-only Reveal (IntersectionObserver) sur landing + framer-motion AnimatePresence sur wizards |
| Rate limit | Upstash Ratelimit (sliding window) |
| Hébergement | Vercel Pro + Vercel Cron |
| Monitoring | Sentry (server + client + edge), captureException sur les call-sites critiques (admin actions, cron, Stripe webhook) |
| Anti-bot | Cloudflare Turnstile (CAPTCHA invisible) sur /demande, /inscription-pro, /connexion |
| PWA | manifest.ts natif Next + service worker manuel + offline fallback + install prompt (Android natif + iOS instructions) |
| Push | web-push + VAPID, branchement 8 events (nouveau lead, wallet faible au franchissement, 4 lifecycle, lead offert, lead bientôt expiré, auto-accept declenche, lead pris par un autre) + master-switch notifyByPush |
Resend + React Email templates, master-switch notifyByEmail via helper deliver() requiresOptIn, emails essentials (recharge, lifecycle, lead-offert, no-match client) toujours envoyes |
|
| Tests | Vitest (logique métier pure : pricing, geo, stats) — 29 tests verts |
- Node 20+
- pnpm 10+
- PostgreSQL : compte Neon recommandé (free tier OK), ou Postgres local
# 1. Clone + install
git clone https://github.com/Namirop/devisrapide.git
cd devisrapide
pnpm install
# 2. Copier l'exemple d'env et compléter
cp .env.local.example .env.local
# Au minimum : DATABASE_URL, NEXTAUTH_SECRET, NEXTAUTH_URL,
# ADMIN_EMAIL, ADMIN_INITIAL_PASSWORD.
# Variables Stripe/Resend/Upstash/Sentry/Turnstile optionnelles en dev
# (les modules tombent gracefully en no-op si vars absentes — cf. section
# variables d'environnement).
# 3. Appliquer migrations + seed
pnpm db:deploy
pnpm db:seed
# 4. Lancer le dev server
pnpm dev
# → http://localhost:3000Le webhook /api/stripe/webhook doit recevoir les events Stripe pour créditer
le wallet. En local, utilise le CLI Stripe :
# Dans un terminal séparé (laisse tourner)
stripe listen --forward-to localhost:3000/api/stripe/webhook
# → affiche un webhook signing secret "whsec_..."Recopie ce whsec_... dans ton .env.local comme STRIPE_WEBHOOK_SECRET,
relance pnpm dev.
Carte de test : 4242 4242 4242 4242 / expiration future / CVC quelconque.
VAPID keys : Générez votre paire via :
pnpm dlx web-push generate-vapid-keysEt placez publicKey / privateKey dans .env.local (NEXT_PUBLIC_VAPID_PUBLIC_KEY + VAPID_PRIVATE_KEY). Sans ces variables, sendPushToProfile est gracefully no-op (les events déclenchent toujours le code mais aucun push n'est émis), et le composant client PushSubscriptionManager affiche une erreur si on tente d'activer.
Service worker en dev : par défaut désactivé pour ne pas interférer avec le HMR Turbopack. Pour tester l'enregistrement + flow push en local, set NEXT_PUBLIC_SW_DEV=1 dans .env.local. En vrai test de prod : pnpm build && pnpm start.
Icônes PWA : régénérables depuis le logo source via node scripts/generate-pwa-icons.mjs (sharp). Output : public/icons/icon-{192,256,384,512}.png + icon-maskable-512.png (safe-zone 80% pour Android).
Sentry : Sans NEXT_PUBLIC_SENTRY_DSN / SENTRY_DSN configurés, Sentry.init est gracefully no-op (pas de network, pas d'erreur). Les capture calls inline (Sentry.captureException dans withAuditLog, cron, webhook) ne font rien. Pour activer en dev, créer un projet Sentry et coller le DSN dans .env.local.
Cloudflare Turnstile : Sans NEXT_PUBLIC_TURNSTILE_SITE_KEY / TURNSTILE_SECRET_KEY, le widget client utilise la sitekey de test Cloudflare 1x00000000000000000000AA (toujours-pass) et verifyTurnstileToken server-side accepte tout token en dev (NODE_ENV !== "production"). En prod, les keys deviennent obligatoires (cf. src/lib/turnstile/verify.ts). Pour activer en dev : créer un site sur Cloudflare > Turnstile, copier les keys dans .env.local.
pnpm db:seed crée le catalogue + un admin. Pour ajouter des données de test
(pros, leads, wallet) utiles au test des espaces pro/admin :
pnpm db:seed:fakes (idempotent, cf. prisma/seed-fakes.ts).
| Variable | Requis | Description |
|---|---|---|
DATABASE_URL |
✅ | URL Postgres complète (Neon recommandé) |
NEXTAUTH_SECRET |
✅ | Secret signing JWT (openssl rand -base64 32) |
NEXTAUTH_URL |
✅ | URL publique (ex: http://localhost:3000 en dev) |
ADMIN_EMAIL |
✅ | Email admin seedé au premier db:seed |
ADMIN_INITIAL_PASSWORD |
✅ | Mot de passe admin initial (changeable depuis /admin/parametres) |
STRIPE_SECRET_KEY |
Clef secrète Stripe (sk_test_...) |
|
STRIPE_WEBHOOK_SECRET |
Secret webhook (whsec_..., généré par stripe listen) |
|
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY |
Clef publique Stripe (pk_test_...) |
|
RESEND_API_KEY |
Si absent : emails tombent en console.log |
|
RESEND_FROM_EMAIL |
Default onboarding@resend.dev |
|
UPSTASH_REDIS_REST_URL |
Si absent : rate limit no-op (utile dev) | |
UPSTASH_REDIS_REST_TOKEN |
idem | |
CRON_SECRET |
Bearer token cron Vercel (openssl rand -hex 32) |
|
SENTRY_DSN |
⚪ Monitoring | DSN Sentry (côté server) |
NEXT_PUBLIC_SENTRY_DSN |
⚪ Monitoring | DSN Sentry (côté client) |
NEXT_PUBLIC_VAPID_PUBLIC_KEY |
⚪ Push | VAPID public (push subscribe côté navigateur) |
VAPID_PRIVATE_KEY |
⚪ Push | VAPID privé (signature serveur, jamais exposé client) |
VAPID_SUBJECT |
⚪ Push | mailto:contact@… requis par la spec Web Push |
NEXT_PUBLIC_SW_DEV |
dev only | 1 pour activer le service worker en dev (default = prod-only pour ne pas casser le HMR) |
Voir .env.local.example pour la liste complète et commentée.
| Script | Action |
|---|---|
pnpm dev |
Dev server (Turbopack, hot reload) |
pnpm build |
Build prod (prisma migrate deploy + next build) |
pnpm start |
Sert le build prod |
pnpm lint |
ESLint |
pnpm db:migrate |
prisma migrate dev (nouvelle migration en dev) |
pnpm db:deploy |
prisma migrate deploy (applique migrations en prod / CI) |
pnpm db:seed |
Seed catalogue + admin |
pnpm db:seed:fakes |
Ajoute des données de test (pros, leads, wallet) |
pnpm db:studio |
Prisma Studio UI |
pnpm db:generate |
Régénère le client Prisma |
pnpm test |
Vitest run (tests unitaires logique métier) |
pnpm test:watch |
Vitest mode watch |
pnpm test:ui |
Vitest UI (debug visuel) |
App Router avec route groups : (public) / (legal) / (pro-public) /
(dashboard) / (admin). Server Components par défaut, 'use client'
placé le plus bas possible dans l'arbre. Server Actions pour les mutations
user-driven, Route Handlers pour les webhooks/cron.
Modèle métier : 3 niveaux de catalogue (Universe → Category → SubCategory),
Lead avec workflow PENDING_MATCH → ASSIGNED → ACCEPTED → COMPLETED,
LeadAssignment pivot avec snapshot prix et expiresAt, Wallet en Int
(centimes) + WalletTransaction log immuable, AuditLog systématique sur
toutes les actions admin.
Voir docs/architecture.md pour la doc complète
(modèle de données, flow matching, sécurité, RGPD, etc.).
- TypeScript strict, zéro
any/as anydouteux - Result type pattern sur toutes les Server Actions :
{ success: true; data } | { success: false; code; message } requireProSession()/requireAdminSession()au début de chaque action sensible- Tous les montants en
Intreprésentant des centimes (jamais Float) - Wallet : transaction Prisma
Serializable+SELECT ... FOR UPDATEsur tout débit - Webhook Stripe : signature vérifiée + body raw + idempotence par
stripeEventId @unique - Conventional commits (
feat:,fix:,refactor:, etc.)
Détail complet : docs/conventions.md.
docs/conventions.md— Conventions de code détaillées
- Clients particuliers anonymes — pas de login client en V1. Auth.js Email magic link prévue en V2 pour permettre au client de revenir voir ses devis.
- B2B / Copropriétés — section LP en mode "Bientôt", fonctionnalité V2.
- RGPD utilisateur — droits d'accès / effacement traités manuellement
en V1, endpoints
/dashboard/profil/donneesprévus V2. - Cookie banner CMP — V1 ne dépose que des cookies essentiels (auth, CSRF, Stripe Checkout), pas de CMP requis. À revoir si analytics V2.
- Cron Vercel —
vercel.jsonconfigure 2 crons (process-leadstoutes les 15min,check-no-match-leadsdaily 9h), actifs en prod sur plan Vercel Pro. En dev local : trigger manuel viacurl -H "Authorization: Bearer $CRON_SECRET". - Tests automatisés — Vitest sur la logique métier pure (pricing, geo, stats — 29 tests). Pas de Playwright en V1, couverture e2e envisagée post-launch.
Repo public à des fins de portfolio dev — utilisation, reproduction ou réutilisation du code soumise à autorisation préalable.
- Support produit :
contact@devisrapide.be - Dev (questions techniques code) : voir profil GitHub
@Namirop