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
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 ynoindexpara 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
| Componente | Tecnologías |
|---|---|
| Backend | PHP 8.5, Laravel 13 |
| Paneles de admin / tenant | Filament 5 |
| Frontend | Livewire 4, Alpine.js, Tailwind CSS 4 |
| Base de datos | MySQL 8, Redis |
| Multi-tenancy | stancl/tenancy v3 (base de datos por tenant) |
| Facturación | Paddle |
| Runtime de producción | FrankenPHP, Laravel Octane 2 |
| Borde / proxy inverso | Caddy |
| Desarrollo local | Laravel Sail |
| Pruebas | Pest 4 |
| Despliegue | Docker, GitHub Actions |
| Infraestructura | AWS Lightsail, Cloudflare (DNS/SSL) |