# Deployment Guide

Multi-tenant clinic SaaS — production deployment notes.

## Stack

- PHP 8.3 + FPM
- MySQL 8.0
- Redis 7+ (cache, sessions, queue)
- Nginx with wildcard subdomain (`*.your-domain.com`)
- Supervisor (queue worker + scheduler)
- Node 20+ (Vite, build assets only)

## DNS / hosts

Wildcard DNS `*.your-domain.com → server-ip`. Example with `your-domain.com = clinicsuite.app`:

```
clinicsuite.app           → marketing site / super-admin login
admin.clinicsuite.app     → super-admin (also resolves via /admin)
demo.clinicsuite.app      → tenant clinic with slug "demo"
```

Set `CENTRAL_DOMAIN=clinicsuite.app` in `.env`. Anything not equal to that hostname is treated by `IdentifyTenant` as a tenant subdomain.

## First boot

```bash
git clone <repo> /var/www/clinic
cd /var/www/clinic
cp .env.example .env
# edit .env: APP_KEY, DB credentials, REDIS_*, MAIL_*, CENTRAL_DOMAIN
composer install --no-dev --optimize-autoloader
php artisan key:generate
php artisan storage:link
php artisan migrate --database=landlord --path=database/migrations/landlord --force
php artisan db:seed --class="Database\Seeders\Landlord\LandlordDatabaseSeeder"
php artisan config:cache route:cache view:cache
chown -R www-data:www-data storage bootstrap/cache
```

Provision a clinic:

```bash
php artisan tenant:create "عيادة الأمل" admin@hope.clinic --slug=hope
```

Outputs the login URL `https://hope.clinicsuite.app/login` and a one-time admin password.

## Nginx (host install, not Docker)

```nginx
server {
    listen 443 ssl http2;
    server_name *.clinicsuite.app clinicsuite.app;

    ssl_certificate     /etc/letsencrypt/live/clinicsuite.app/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/clinicsuite.app/privkey.pem;

    root /var/www/clinic/public;
    index index.php;
    client_max_body_size 25m;

    location / { try_files $uri $uri/ /index.php?$query_string; }

    location ~ \.php$ {
        fastcgi_pass unix:/run/php/php8.3-fpm.sock;
        fastcgi_index index.php;
        include fastcgi_params;
        fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;
    }

    location ~* \.(css|js|woff2?|svg|jpg|jpeg|png|gif|ico)$ {
        expires 30d;
        add_header Cache-Control "public, immutable";
    }
}

server {
    listen 80;
    server_name *.clinicsuite.app clinicsuite.app;
    return 301 https://$host$request_uri;
}
```

Get a wildcard certificate via Let's Encrypt DNS challenge (e.g. `certbot --dns-cloudflare`).

## Supervisor

`/etc/supervisor/conf.d/clinic.conf`:

```ini
[program:clinic-queue]
command=php /var/www/clinic/artisan queue:work --tries=3 --sleep=3 --max-time=3600
user=www-data
autostart=true
autorestart=true
numprocs=2
stdout_logfile=/var/log/clinic-queue.log

[program:clinic-scheduler]
command=/bin/bash -c "while true; do php /var/www/clinic/artisan schedule:run >> /dev/null 2>&1; sleep 60; done"
user=www-data
autostart=true
autorestart=true
```

Reload:

```bash
supervisorctl reread && supervisorctl update && supervisorctl start clinic-queue:* clinic-scheduler
```

## Cron (alternative to supervisor scheduler)

```cron
* * * * * www-data php /var/www/clinic/artisan schedule:run >> /dev/null 2>&1
```

## Backups

The simplest tenant-aware backup script — runs every clinic in the landlord registry:

```bash
#!/bin/bash
DATE=$(date +%F)
DEST=/var/backups/clinic/$DATE
mkdir -p $DEST
mysqldump clinic_landlord | gzip > $DEST/clinic_landlord.sql.gz

# All clinic DBs
mysql -e "SHOW DATABASES" | grep "^clinic_" | while read db; do
    mysqldump $db | gzip > $DEST/${db}.sql.gz
done

# Retention: keep 14 days
find /var/backups/clinic -mindepth 1 -maxdepth 1 -mtime +14 -exec rm -rf {} \;
```

Cron: `30 2 * * * /usr/local/bin/clinic-backup.sh`

## Health check

`GET /health` returns JSON with the status of `app`, `database`, `cache`, and `redis`. Wire this into your uptime monitor.

## Security checklist

- [x] HTTPS enforced (`URL::forceScheme('https')` in `AppServiceProvider::boot` when env=production)
- [x] CSRF on all forms (Laravel default `web` middleware)
- [x] Rate limiting on login (`LoginController::ensureIsNotRateLimited`, 5/min)
- [x] Output escaped (Blade default escapes; only intentional `{!! !!}` in trusted reports)
- [x] SQL via Eloquent / parameter binding only
- [x] File uploads validated by mime + size in `StorePatientRequest`, `LabTestController`
- [x] Sensitive columns encrypted at rest (`Clinic::db_password`, `User::two_factor_secret`)
- [x] Tenant isolation: every tenant model pins `$connection = 'tenant'`; central models pin `$connection = 'landlord'`. There is no shared DB. Verify by inspecting `app/Models/Tenant/*` and `app/Models/Landlord/*`.
- [x] Permissions checked with `@can` in views and `abort_unless` in controllers
- [x] Sessions stored in landlord DB (`SESSION_CONNECTION=landlord`) so cross-tenant fixation is harder
- [ ] Consider signing tenant_id into the session and rejecting mismatched requests at the middleware level (production hardening)

## Performance

- `composer dump-autoload --optimize` and `config:cache route:cache view:cache` after each deploy
- OPcache + JIT enabled in `docker/php.ini`
- DataTables go through `serverSide: true` so no rows are sent that aren't displayed
- Dashboard widgets cached for 60s in `DashboardService`
- Heavy reports should be cached for 5–15 minutes (TODO: add `Cache::remember` to `ReportController` once usage patterns are clearer)

## Adding a real SMS provider

```php
// app/Services/Sms/TwilioGateway.php
class TwilioGateway implements \App\Contracts\SmsGateway { ... }

// AppServiceProvider::register()
$this->app->bind(\App\Contracts\SmsGateway::class, TwilioGateway::class);
```

`AppointmentReminder::via()` already includes `database` + optional `mail`. To add SMS, push a `toSms()` method that calls `app(SmsGateway::class)->send(...)`, and append `'sms'` to `via()` when the user has a phone.

## Adding a tenant manually

```bash
php artisan tenant:create "Clinic Name" owner@example.com --slug=mycln --password=secret123
```

Backfill a missing tenant DB:

```bash
php artisan tenant:migrate mycln --seed
```

Re-run all tenant migrations (e.g. after adding a new column):

```bash
php artisan tenant:migrate          # all clinics
php artisan tenant:migrate mycln    # one clinic
```
