Amine Benamrouche · Proyectos
← Volver a los proyectos

Klinica: SaaS multi-tenant de código abierto para clínicas médicas

Resumen

Klinica es un SaaS multi-tenant para clínicas médicas: un subdominio por clínica, una página de inicio pública personalizable, reservas en línea y un panel de administración completo. La facturación, los periodos de prueba y las restricciones de plan se gestionan de extremo a extremo con Paddle.

En vivo: klinica.space · Código fuente: github.com/AmineZCS/klinica

Lo construí como proyecto independiente, para armar desde cero y de extremo a extremo una arquitectura SaaS multi-tenant completa. Está desplegado y funcionando en una VM de AWS Lightsail, con Cloudflare gestionando el DNS y el SSL del dominio raíz y de cada subdominio de tenant, SMTP real para los correos de recordatorio y la facturación de Paddle en modo sandbox.

Es una versión deliberadamente más pequeña de la plataforma de salud multi-tenant en la que trabajé profesionalmente en HealthHub, y traslada los mismos conceptos centrales (aislamiento de tenants, suscripciones, programación de citas) a mi propia arquitectura.

Arquitectura

Infraestructura de Klinica: Cloudflare y Caddy delante de la aplicación Laravel, enrutamiento por subdominio hacia bases de datos de tenant aisladas, llamadas síncronas y salientes a Paddle, almacenamiento local de medios por tenant, una tarea programada que lee la base central y las de los tenants para los recordatorios de reservas, y una cola respaldada por Redis que alimenta un worker para el aprovisionamiento de tenants y el correo

Las solicitudes llegan a través de Cloudflare y Caddy a un único proceso de la aplicación Laravel. A partir de ahí, el subdominio decide adónde va la solicitud: el tráfico de cada clínica se enruta a su propia base de datos de tenant aislada, mientras que el panel de super-administración y los webhooks entrantes de Paddle escriben ambos en la base de datos central. La aplicación también llama directamente a Paddle para el checkout, los cambios de plan y las cancelaciones. La ruta de los webhooks es síncrona y no pasa por una cola, pero está verificada con HMAC y es idempotente. Los recursos de marca de los tenants (logotipos, imágenes) se guardan en un volumen de disco local, separado por tenant.

Una tarea programada ejecuta directamente el flujo de recordatorios de reservas: lee los tenants elegibles de la base de datos central, cambia después a la base de datos de cada tenant para obtener las próximas reservas y encola los correos de recordatorio. Lo hace de forma secuencial, un tenant cada vez, lo que está bien a la escala actual pero no está dividido en lotes para un número de tenants mucho mayor. La cola en sí (Redis y un worker) solo se encarga de dos cosas: aprovisionar la base de datos de un nuevo tenant al registrarse y enviar realmente el correo encolado.

Funciones

  • Multi-tenancy: subdominio por tenant con bases de datos aisladas, usando stancl/tenancy. El registro aprovisiona un nuevo tenant de forma asíncrona: la solicitud responde de inmediato y la base de datos se crea en un job encolado.
  • Facturación y suscripciones: integración con Paddle con periodos de prueba, suscripciones renovables, cambios de plan, cancelaciones y sincronización de estado dirigida por webhooks.
  • Restricciones de plan: límites de Starter y Pro (pacientes, empleados, reservas mensuales, almacenamiento, recordatorios por correo) aplicados mediante middleware.
  • Reservas públicas: cada clínica tiene una página de inicio con su marca y un flujo de reserva de citas de autoservicio.
  • Páginas de inicio personalizables: los tenants editan la marca, los colores, los datos de contacto y los textos desde su panel de ajustes, con vista previa en vivo antes de guardar.
  • Dos paneles de Filament: un panel central de super-administración (tenants, suscripciones, analítica) y un panel de clínica por tenant (reservas, pacientes, servicios, personal, calendario).
  • Roles y permisos: RBAC por tenant con spatie/laravel-permission.
  • Multilingüe: inglés, francés, alemán, español.
  • SEO: sitemaps por tenant, llms.txt, envío a IndexNow y noindex para los tenants que aún no están listos para ser públicos.
  • Registro de auditoría: seguimiento de la actividad de los tenants con spatie/laravel-activitylog.
  • RGPD: exportación de datos por tenant.

Producción y despliegue

Klinica funciona detrás de FrankenPHP + Laravel Octane en una única VM de AWS Lightsail. Toda la pila (aplicación, worker de cola, planificador, MySQL, Redis y Caddy como proxy de borde) está definida en un archivo Docker Compose de producción. Cloudflare gestiona el DNS y el SSL del dominio raíz y de cada subdominio de tenant, y Caddy termina el TLS delante de la aplicación.

Los despliegues pasan por un pipeline de GitHub Actions: lint y pruebas, construcción de la imagen, ejecución de migraciones (incluidas las migraciones por tenant), cambio de versión y, después, pruebas de humo, con reversión automática a la imagen anterior si algo falla. La suite de pruebas (Pest 4) cubre las rutas críticas: facturación, verificación de la firma de los webhooks e idempotencia, y aprovisionamiento de tenants. El pipeline de despliegue completo y la configuración del borde están en el repositorio.

Stack técnico

ComponenteTecnologías
BackendPHP 8.5, Laravel 13
Paneles de admin / tenantFilament 5
FrontendLivewire 4, Alpine.js, Tailwind CSS 4
Base de datosMySQL 8, Redis
Multi-tenancystancl/tenancy v3 (base de datos por tenant)
FacturaciónPaddle
Runtime de producciónFrankenPHP, Laravel Octane 2
Borde / proxy inversoCaddy
Desarrollo localLaravel Sail
PruebasPest 4
DespliegueDocker, GitHub Actions
InfraestructuraAWS Lightsail, Cloudflare (DNS/SSL)