Amine Benamrouche · Projets
← Retour aux projets

Klinica : SaaS multi-tenant open source pour cliniques médicales

Aperçu

Klinica est un SaaS multi-tenant pour cliniques médicales : un sous-domaine par clinique, une page d’accueil publique personnalisable, la réservation en ligne et un panneau d’administration complet. La facturation, les périodes d’essai et les restrictions de forfait sont gérées de bout en bout via Paddle.

En ligne : klinica.space · Code source : github.com/AmineZCS/klinica

Je l’ai construit comme projet indépendant, pour assembler de A à Z une architecture SaaS multi-tenant complète. Il est déployé et fonctionne sur une VM AWS Lightsail, avec Cloudflare pour le DNS et le SSL du domaine racine et de chaque sous-domaine de tenant, un vrai SMTP pour les e-mails de rappel et la facturation Paddle en mode sandbox.

C’est une version volontairement plus petite de la plateforme de santé multi-tenant sur laquelle j’ai travaillé professionnellement chez HealthHub, qui reprend les mêmes concepts de base (isolation des tenants, abonnements, planification) dans ma propre architecture.

Architecture

Infrastructure de Klinica : Cloudflare et Caddy devant l'application Laravel, routage par sous-domaine vers des bases de données de tenant isolées, appels Paddle synchrones et sortants, stockage local des médias par tenant, une tâche planifiée qui lit les bases centrale et des tenants pour les rappels de réservation, et une file d'attente adossée à Redis alimentant un worker pour le provisionnement des tenants et l'envoi d'e-mails

Les requêtes arrivent via Cloudflare et Caddy vers un seul processus d’application Laravel. Ensuite, le sous-domaine décide où va la requête : le trafic de chaque clinique est routé vers sa propre base de données de tenant isolée, tandis que le panneau super-admin et les webhooks entrants de Paddle écrivent tous deux dans la base centrale. L’application appelle aussi Paddle directement pour le paiement, les changements de formule et les résiliations. Le chemin des webhooks est synchrone et non mis en file d’attente, mais il est vérifié par HMAC et idempotent. Les ressources de personnalisation des tenants (logos, images) sont stockées sur un volume de disque local, cloisonné par tenant.

Une tâche planifiée exécute directement le flux des rappels de réservation : elle lit les tenants éligibles dans la base centrale, bascule ensuite dans la base de chaque tenant pour récupérer les réservations à venir et met en file d’attente les e-mails de rappel. Cela se fait de manière séquentielle, un tenant à la fois, ce qui convient à l’échelle actuelle mais n’est pas découpé en lots pour un nombre de tenants bien plus élevé. La file d’attente elle-même (Redis et un worker) ne gère que deux choses : le provisionnement de la base de données d’un nouveau tenant à l’inscription et l’envoi effectif des e-mails mis en file.

Fonctionnalités

  • Multi-tenancy : un sous-domaine par tenant avec des bases de données isolées, grâce à stancl/tenancy. L’inscription provisionne un nouveau tenant de façon asynchrone : la requête répond immédiatement, la base de données est créée par un job en file d’attente.
  • Facturation et abonnements : intégration de Paddle avec périodes d’essai, abonnements renouvelables, changements de formule, résiliations et synchronisation d’état pilotée par les webhooks.
  • Restrictions de forfait : limites Starter et Pro (patients, employés, réservations mensuelles, stockage, rappels par e-mail) appliquées via des middlewares.
  • Réservation publique : chaque clinique dispose d’une page d’accueil à son image et d’un parcours de prise de rendez-vous en libre-service.
  • Pages d’accueil personnalisables : les tenants modifient l’identité visuelle, les couleurs, les coordonnées et les textes depuis leur panneau de paramètres, avec un aperçu en direct avant l’enregistrement.
  • Deux panneaux Filament : un panneau super-admin central (tenants, abonnements, analytique) et un panneau de clinique par tenant (réservations, patients, services, personnel, calendrier).
  • Rôles et permissions : RBAC par tenant avec spatie/laravel-permission.
  • Multilingue : anglais, français, allemand, espagnol.
  • SEO : sitemaps par tenant, llms.txt, soumission IndexNow et noindex pour les tenants qui ne sont pas encore prêts à être publics.
  • Journal d’audit : suivi de l’activité des tenants avec spatie/laravel-activitylog.
  • RGPD : export des données par tenant.

Production et déploiement

Klinica fonctionne derrière FrankenPHP + Laravel Octane sur une seule VM AWS Lightsail. Toute la pile (application, worker de file d’attente, planificateur, MySQL, Redis et Caddy comme proxy en périphérie) est définie dans un fichier Docker Compose de production. Cloudflare gère le DNS et le SSL du domaine racine et de chaque sous-domaine de tenant, Caddy terminant le TLS devant l’application.

Les déploiements passent par un pipeline GitHub Actions : lint et tests, construction de l’image, exécution des migrations (y compris les migrations par tenant), bascule, puis tests de fumée, avec retour automatique à l’image précédente en cas d’échec. La suite de tests (Pest 4) couvre les chemins critiques : facturation, vérification de la signature des webhooks et idempotence, et provisionnement des tenants. Le pipeline de déploiement complet et la configuration en périphérie se trouvent dans le dépôt.

Stack technique

ComposantTechnologies
BackendPHP 8.5, Laravel 13
Panneaux admin / tenantFilament 5
FrontendLivewire 4, Alpine.js, Tailwind CSS 4
Base de donnéesMySQL 8, Redis
Multi-tenancystancl/tenancy v3 (une base de données par tenant)
FacturationPaddle
Runtime de productionFrankenPHP, Laravel Octane 2
Périphérie / reverse proxyCaddy
Développement localLaravel Sail
TestsPest 4
DéploiementDocker, GitHub Actions
InfrastructureAWS Lightsail, Cloudflare (DNS/SSL)