# Deployment Guide

## 1. Minimum Server Requirements

| Component | Minimum | Recommended (production SaaS) |
|---|---|---|
| PHP | 8.3 | 8.3 with OPcache + JIT |
| MySQL | 8.0 | 8.0, InnoDB, 4GB+ buffer pool |
| Redis | Optional | Recommended for session/cache/queue at scale |
| Web server | Apache 2.4 / Nginx | Nginx + PHP-FPM |
| RAM | 2GB | 8GB+ |
| Storage | 20GB SSD | SSD, scaled to resume/document volume |
| Composer | 2.x | 2.x |

## 2. Recommended Stack

```
Nginx (reverse proxy, TLS termination)
  → PHP-FPM (CI4 app)
  → MySQL 8 (primary; read-replica optional at scale)
  → Redis (session, cache, rate-limit, queue)
  → S3-compatible object storage (resumes, verification docs, invoices) — optional, local disk acceptable for single-server start
```

## 3. Environment Setup

1. Clone/deploy codebase to `/var/www/job-portal/`
2. `composer install --no-dev --optimize-autoloader`
3. Copy `.env.example` → `.env`, or run the Installation Wizard (see `08-installation-workflow.md`) which generates it
4. Set `CI_ENVIRONMENT=production` in `.env`
5. `php spark migrate` (if not using the wizard) and `php spark db:seed MasterSeeder`
6. Set ownership: `writable/` and upload directories owned by the web server user (`www-data`), `750` permissions

## 4. Nginx Configuration (example)

```nginx
server {
    listen 443 ssl http2;
    server_name portal.example.com;

    root /var/www/job-portal/public;
    index index.php;

    ssl_certificate     /etc/letsencrypt/live/portal.example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/portal.example.com/privkey.pem;

    add_header Strict-Transport-Security "max-age=63072000" always;
    add_header X-Content-Type-Options nosniff always;
    add_header X-Frame-Options DENY always;

    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 $document_root$fastcgi_script_name;
    }

    location ~ /\.(?!well-known) { deny all; }
    location ~ ^/(writable|app|vendor)/ { deny all; }

    client_max_body_size 25M;   # resume/document uploads
}

server {
    listen 80;
    server_name portal.example.com;
    return 301 https://$host$request_uri;
}
```

## 5. Cron Setup

Add to the deployment user's crontab (`crontab -e`), paths adjusted to install location:

```
* * * * *     php /var/www/job-portal/spark tasks:run >> /var/log/job-portal-cron.log 2>&1
```

Using CI4's task scheduler (`spark tasks:run` invoked every minute) which internally dispatches to the actual jobs at their configured schedules:

| Task | Schedule | Purpose |
|---|---|---|
| `subscription:expiry-check` | Daily 00:15 | Expire lapsed subscriptions, send warnings |
| `credit:expiry-check` | Daily 00:30 | Expire time-limited credits |
| `notifications:digest` | Every 15 min | Send queued scheduled notifications |
| `interviews:reminders` | Every 30 min | Send upcoming-interview reminders |
| `cleanup:temp-files` | Daily 03:00 | Purge temp uploads, expired password reset tokens |
| `reports:generate` | Daily 02:00 | Refresh materialized report summary tables |

## 6. Queue Worker (if Redis enabled)

```
# systemd service: job-portal-queue.service
[Unit]
Description=Job Portal Queue Worker
After=network.target redis.service

[Service]
User=www-data
WorkingDirectory=/var/www/job-portal
ExecStart=/usr/bin/php spark queue:work --queue=default
Restart=always

[Install]
WantedBy=multi-user.target
```

If Redis is not configured, the app runs notifications/PDF generation synchronously in-request — no queue worker needed (per the "Redis optional" requirement).

## 7. Zero-Downtime Deployment Flow

1. `git pull` (or artifact deploy) to a new release directory (`releases/{timestamp}/`)
2. `composer install --no-dev --optimize-autoloader` in the new release
3. Symlink shared `.env` and `writable/uploads` into the new release
4. `php spark migrate` (migrations must be backward-compatible with the still-running old release during the brief cutover)
5. Atomically switch the `current` symlink to the new release directory
6. Reload PHP-FPM (`systemctl reload php8.3-fpm`) — graceful, no dropped requests
7. Keep last 3 releases for instant rollback (`current` symlink swap back)

## 8. Backups

- **Database:** nightly `mysqldump` (or `mysqlpg`/`xtrabackup` at scale) → gpg-encrypt → upload to offsite/S3 → 30-day retention, weekly snapshots kept 6 months.
- **File storage:** nightly incremental sync of `writable/uploads` / `storage/agencies/` to offsite storage.
- **Restore drill:** documented monthly restore-to-staging test (a backup nobody has restored isn't a backup).

## 9. Monitoring

- Application: error logging to `writable/logs/`, shipped to a log aggregator (e.g. self-hosted or managed) in production.
- Uptime: external HTTP(S) monitor hitting `/healthz` (lightweight endpoint checking DB + Redis connectivity, no auth).
- Queue depth (if Redis queue in use) and cron last-run timestamps exposed on an internal admin "System Health" dashboard.

## 10. Scaling Path

1. **Single server** (this guide) — fine up to a few thousand agencies/moderate traffic.
2. **Vertical scale** DB + app server RAM/CPU.
3. **Horizontal app tier**: multiple PHP-FPM app servers behind a load balancer; sessions move to Redis (already recommended above) so any server can serve any request.
4. **DB read replica** for search/reporting read traffic, primary reserved for writes.
5. **Object storage migration** (S3-compatible) for uploads once local disk / single-server storage becomes a bottleneck or multi-server consistency is needed.
6. **Tenancy re-evaluation**: if a handful of very large agencies emerge, those specific tenants can be migrated to dedicated DB schemas without changing application code (Repository layer already abstracts this — see System Architecture §4).
