Bernardo Knoblauch

VamosMarcar

SaaS multi-tenant de prise de rendez-vous pour barbiers et salons. API en Node.js, Express et Prisma ; web en Next.js.

Produit personnel · SaaS · Node.js

Période
2026–
Stack
Node.js, Express, TypeScript, Prisma, PostgreSQL, Zod, Next.js, React, Luxon, Cloudflare

Images

Contexte

VamosMarcar est mon SaaS de prise de rendez-vous en ligne pour barbiers et salons de coiffure. Chaque commerce dispose d’une page de liens, pensée pour la bio Instagram, et d’un parcours de réservation sur /{slug}/agendar. Le gérant pilote tout depuis un tableau de bord ; j’administre la plateforme depuis un espace d’administration séparé.

Je l’ai développé seul, d’avril à août 2026 : 111 commits dans un monorepo pnpm réunissant l’API, le web et des paquets de contrats partagés.

Problème

Pour ce public, la prise de rendez-vous se fait par messages WhatsApp, sans vue d’agenda, sans historique clients et sans suivi des absences.

Un outil pour ce marché doit coûter presque rien par client, bien fonctionner sur mobile et ne pas demander au client final de créer un compte.

Ce que j’ai fait et pourquoi

Architecture

  • API en Node.js 20, Express et TypeScript, organisée par domaine : 11 modules (auth, rendez-vous, disponibilités, prestations, équipe, métriques…), chacun avec controller, service et repository, et 55 routes sous /api/v1.
  • PostgreSQL avec Prisma : 8 modèles et 12 migrations. Multi-tenant par businessId sur chaque ligne de tenant, avec des invariants vérifiés dans l’application et des entrées validées avec Zod.
  • Web en Next.js 15 et React 19. Le navigateur ne parle qu’à Next (un BFF en Route Handlers), qui appelle l’API : pas de CORS, et la page publique de réservation est rendue côté serveur avec cache.
  • Contrats partagés entre API et web dans les paquets @sweeney/types et @sweeney/api-client.

Réservation

  • Modèle demande puis confirmation : la demande arrive en pending, le gérant la confirme ou l’annule, puis la marque terminée ou no-show. Pending et confirmed bloquent tous deux le créneau.
  • Créneaux libres calculés avec Luxon dans le fuseau du commerce : plages hebdomadaires, rendez-vous existants, blocages d’une journée ou d’une plage horaire et date limite globale pour les nouveaux créneaux. Une réservation accepte plusieurs prestations, en additionnant les durées.
  • Vérification des chevauchements et insertion du rendez-vous dans la même transaction.
  • Annulation par le client avec le lien reçu et le même numéro de téléphone, dans une fenêtre configurable avant le rendez-vous.

Sécurité et exploitation

  • JWT en cookie portant l’instant du dernier changement de mot de passe (pwdAt) : changer de mot de passe invalide les anciennes sessions. Cookies et routes séparés pour l’administrateur de la plateforme.
  • Rate limiting maison sur les routes d’authentification, sans faire confiance à X-Forwarded-For quand le proxy n’est pas de confiance.
  • CI sur GitHub Actions et Bitbucket Pipelines avec typecheck, lint et build. Déploiement préparé pour Railway, avec Cloudflare en bordure (DNS, SSL, WAF) et Cloudflare Images pour les photos des commerces.

Pourquoi WhatsApp reste manuel

Les messages partent par des liens wa.me envoyés par le gérant ou le client, sans Twilio ni WhatsApp Business API. J’ai comparé les canaux selon le coût fixe, le coût par message et l’effort d’exploitation : pour le pilote, wa.me utilise le canal que ce public a déjà, à coût nul par message. L’envoi de SMS via Twilio existe dans le code et reste désactivé tant qu’il n’y a pas d’identifiants.

Résultat

  • En ligne sur vamosmarcar.com.
  • Tableau de bord avec agenda jour, semaine et mois, clients regroupés par numéro de téléphone, métriques par période avec export CSV, et équipe avec rôles de gérant et d’employé.
  • Coût par message envoyé : zéro.

Commerces actifs et rendez-vous : [PREENCHER]

Prochaines étapes

  • Tests automatisés. Aujourd’hui, le filet de sécurité de la CI est typecheck, lint et build.
  • Alerte automatique au gérant quand une nouvelle demande arrive.

Contact