Overview
Klinica is a multi-tenant SaaS for medical clinics — subdomain-per-clinic, a customizable public landing page, online booking, and a full admin panel. Billing, trials, and plan restrictions are handled end-to-end through Paddle.
Live: klinica.space · Source: github.com/AmineZCS/klinica
I built it as an independent project, to put together a complete multi-tenant SaaS architecture from scratch, end to end. It’s deployed and running on an AWS Lightsail VM, with Cloudflare handling DNS and SSL for the root domain and every tenant subdomain, real SMTP for email reminders, and Paddle billing running in sandbox mode.
It’s a deliberately smaller version of the multi-tenant healthcare platform I worked on professionally at HealthHub, translating the same core concepts — tenant isolation, subscriptions, scheduling — into my own architecture.
Architecture
Requests come in through Cloudflare and Caddy to a single Laravel app process. From there, the subdomain decides where the request goes: each clinic’s traffic routes to its own isolated tenant database, while the super-admin panel and Paddle’s incoming webhooks both write to the central database — the app also calls out to Paddle directly for checkout, plan swaps, and cancellations. The webhook path is synchronous, not queued, but it’s HMAC-verified and idempotent. Tenant branding assets (logos, images) sit on a local disk volume, scoped per tenant.
A scheduled job runs the booking-reminder flow directly: it reads eligible tenants from the central database, then switches into each tenant’s own database to pull upcoming bookings, and queues the reminder emails — sequentially, one tenant at a time, which is fine at the current scale but not chunked for a much larger tenant count. The queue itself (Redis + a worker) only handles two things: provisioning a new tenant’s database on signup, and actually sending the queued mail.
Features
- Multi-tenancy: Subdomain-per-tenant with isolated databases, using
stancl/tenancy. Signup provisions a new tenant asynchronously — the request returns immediately, the database spins up on a queued job. - Billing and subscriptions: Paddle integration — trials, renewable subscriptions, plan swaps, cancellations, and webhook-driven state sync.
- Plan restrictions: Starter vs. Pro limits (patients, employees, monthly bookings, storage, email reminders) enforced via middleware.
- Public booking: Each clinic gets a branded landing page and a self-service appointment-booking flow.
- Customizable landing pages: Tenants edit branding, colors, contact info, and copy from their settings panel, with a live preview before saving.
- Two Filament panels: A central super-admin panel (tenants, subscriptions, analytics) and a per-tenant clinic panel (bookings, patients, services, staff, calendar).
- Roles and permissions: Per-tenant RBAC using
spatie/laravel-permission. - Multi-language: English, French, German, Spanish.
- SEO: Per-tenant sitemaps,
llms.txt, IndexNow submission, andnoindexfor tenants that aren’t ready to be public yet. - Audit log: Tenant activity tracking via
spatie/laravel-activitylog. - GDPR: Per-tenant data export.
Production & Deployment
Klinica runs behind FrankenPHP + Laravel Octane on a single AWS Lightsail VM, with the full stack — app, queue worker, scheduler, MySQL, Redis, and Caddy as the edge proxy — defined in a production Docker Compose file. Cloudflare handles DNS and SSL for the root domain and every tenant subdomain, with Caddy terminating TLS in front of the app.
Deploys run through a GitHub Actions pipeline: lint and test, build the image, run migrations (including per-tenant migrations), cut over, then smoke-test — with automatic rollback to the previous image if anything fails. The test suite (Pest 4) covers the critical paths: billing, webhook signature verification and idempotency, and tenant provisioning. The full deploy pipeline and edge configuration are in the repository.
Technical Stack
| Component | Technologies |
|---|---|
| Backend | PHP 8.5, Laravel 13 |
| Admin / tenant panels | Filament 5 |
| Frontend | Livewire 4, Alpine.js, Tailwind CSS 4 |
| Database | MySQL 8, Redis |
| Multi-tenancy | stancl/tenancy v3 (database-per-tenant) |
| Billing | Paddle |
| Production runtime | FrankenPHP, Laravel Octane 2 |
| Edge / reverse proxy | Caddy |
| Local development | Laravel Sail |
| Testing | Pest 4 |
| Deployment | Docker, GitHub Actions |
| Infrastructure | AWS Lightsail, Cloudflare (DNS/SSL) |