# Deploying Ecclesia Ledger

> Moving to a real server? Follow the step-by-step **[GO-LIVE.md](GO-LIVE.md)** checklist.

Two supported setups. Both run the same code; only `.env` differs.

| | Small church (shared hosting / XAMPP / one VPS) | Platform / many churches (Docker) |
|---|---|---|
| Web | Apache or nginx + PHP 8.2 | `docker compose` (nginx + PHP-FPM in one image) |
| Database | MySQL 8 / MariaDB 10.4+ | MariaDB 10.11 container (or managed MySQL) |
| Queue (email, SMS, WhatsApp) | `database`, drained by the scheduler every minute | `redis`, 2 supervised workers |
| Cache / sessions | `database` | `redis` |
| Scheduler | cron / Windows Task Scheduler: `php artisan schedule:run` every minute | `schedule:work` inside the container |

## 1. Docker (recommended for the SaaS platform)

```bash
cp .env.example .env.docker
# Edit .env.docker: APP_URL, APP_KEY, BACKUP_KEY, DB_PASSWORD, DB_ROOT_PASSWORD, MAIL_*, BILLING_*.
docker compose --env-file .env.docker up -d --build
```

- Generate keys once and store them in a password manager **before** going live:
  `docker compose run --rm app php artisan key:generate --show` and `... php artisan backup:key --show`.
  Losing `APP_KEY` makes encrypted data (welfare notes, gateway keys, 2FA secrets) unreadable, even from backups.
- The container migrates on start (`RUN_MIGRATIONS=false` to skip), caches config/routes/views, and runs
  2 queue workers plus the scheduler. Health check: `GET /up`.
- Put HTTPS in front (Caddy, Traefik, nginx, or Cloudflare). Set `APP_URL=https://…`, `SESSION_SECURE_COOKIE=true`.
- Paystack/Flutterwave and WhatsApp webhooks need the public HTTPS URL.
- Scale reads first with a bigger DB; the app itself is stateless (sessions/cache/queue in Redis), so more `app`
  containers can sit behind a load balancer. Run the scheduler in only one of them (`onOneServer` is used for backups).

## 2. Single server without Docker (Ubuntu example)

```bash
sudo apt install nginx mariadb-server redis-server php8.2-fpm php8.2-{mysql,bcmath,intl,zip,gd,mbstring,xml,curl,redis} unzip
composer install --no-dev --optimize-autoloader
npm ci && npm run build
cp .env.example .env && php artisan key:generate && php artisan backup:key
php artisan migrate --force
php artisan config:cache && php artisan route:cache && php artisan view:cache
```

- Point the web root at `public/`. Use `docker/nginx.conf` as a starting point.
- Cron: `* * * * * cd /var/www/church && php artisan schedule:run >> /dev/null 2>&1`
- Optional real workers (systemd or supervisor, see `docker/supervisord.conf`) with `QUEUE_CONNECTION=redis`
  and `QUEUE_DRAIN_VIA_SCHEDULER=false`.

## 3. XAMPP on Windows (local / single church office)

Already set up on this machine. Notes:
- Several Laravel apps under one Apache share environment variables, so this app runs from cached config:
  run `php artisan config:cache` after any `.env` change. Cookies are scoped with `SESSION_PATH`.
- Windows Task Scheduler: every minute run `C:\xampp\php\php.exe C:\xampp\htdocs\church_acct_app\artisan schedule:run`.
- `BACKUP_COPY_TO` can point at a OneDrive/Google Drive synced folder or an external drive for the off-site copy.

## What the scheduler runs

| When | Command | What |
|---|---|---|
| every minute | `queue:work --stop-when-empty` | sends queued email/SMS/WhatsApp (non-Docker only) |
| hourly | `reports:send-scheduled` | emails due report schedules (07:00 Lagos) |
| 00:30 | `billing:run` | subscription invoices, grace, read-only |
| 01:00 | `giving:post-online` | posts yesterday's online giving |
| 05:00 | `bank-feeds:sync` | brings in bank-feed transactions for reconciliation |
| every 15 min | `einvoice:submit` | transmits paid platform invoices (only when e-invoicing is enabled) |
| 02:00 | `backup:run` | encrypted backup + off-site copy + retention |
| Sun 03:00 | `backup:verify` | decrypts the newest backup and checks every checksum |

## Updating

```bash
php artisan backup:run            # always back up first
git pull && composer install --no-dev -o && npm ci && npm run build
php artisan migrate --force && php artisan config:cache && php artisan route:cache && php artisan view:cache
php artisan queue:restart
php artisan security:check        # exits with an error if a go-live security setting is wrong
composer audit && npm audit --omit=dev
```
With Docker: `docker compose build app && docker compose up -d app` (it migrates on start).

## Security built in

| Layer | What it does |
|---|---|
| Sign-in | 5 tries/min per email+IP and 20/min per IP; after 10 wrong passwords an account pauses for 15 minutes on the website **and** the phone API (audited as `account_locked`); unknown emails take as long to answer as known ones |
| Two-factor | Required for the platform owner and for the roles each church chooses; codes can't be replayed; recovery codes are single-use |
| Passwords | 10+ characters, mixed case and a number; on production also checked against leaked-password lists (HaveIBeenPwned, k-anonymity, the password never leaves the server). Changing it signs out every other device and phone token |
| Browser | Content-Security-Policy (scripts only from this site with a per-request nonce, plus Mono Connect), no framing, nosniff, strict referrer, HSTS on HTTPS, no caching of signed-in pages, history wiped on sign-out |
| Transport | On production plain HTTP is redirected to HTTPS and cookies are HTTPS-only (`SECURITY_FORCE_HTTPS`); set `TRUSTED_PROXIES` behind Cloudflare/a load balancer |
| Data | Every query tenant-scoped; sessions, 2FA secrets and welfare details encrypted; parameterised SQL only; CSRF on all forms; webhooks verified by signature with constant-time comparison |
| Exports | Text starting with `= + - @` is neutralised in CSV/Excel downloads (formula injection) |
| Files | Only `public/` is served; `.env`, storage, logs and vendor return 403 |

## Monitoring

- `GET /up` for uptime checks (UptimeRobot, Better Stack…).
- Platform console (`/admin/tenants`) shows the last backup result and warns if it is older than a day or failed.
- Failed messages: *Messages & statements* (retry button). Failed report schedules show their last error.
- Logs: `storage/logs/laravel.log` (or container stdout).
