# Production Deployment Guide — Viață la Țară API

Everything needed to run this API on a real server with a real domain, TLS, and a
production database. Two supported topologies — pick **A** (recommended) or **B**:

- **A. Reverse proxy terminates TLS** (nginx/Caddy) → app runs plain HTTP on an internal port. *Recommended: simplest cert renewal, standard, plays well with the `/ws` WebSocket upgrade.*
- **B. App terminates TLS directly** using the built-in HTTPS support (`SSL_KEY_PATH`/`SSL_CERT_PATH`). Use only if you can't run a proxy.

Follow the **Checklist** (§10) top to bottom; the sections above it are the detail.

---

## 1. Requirements

### Server
- A Linux VPS/VM (Ubuntu 22.04+ / Debian 12 assumed below). 1 vCPU / 1 GB RAM is enough to start; 2 GB recommended if MongoDB runs on the same box.
- Public IPv4, ports **80** and **443** open. App's internal port (default **3838**) must **not** be publicly exposed — only the proxy talks to it.
- Root/sudo access.

### Software on the server
- **Docker** + **Docker Compose plugin** (recommended path), **or** Node.js 20 LTS + a process manager (systemd/PM2) for the bare-metal path.
- **nginx** (or Caddy) as the reverse proxy — for topology A.
- **certbot** (Let's Encrypt) for TLS certs — for topology A.

### External services / accounts
- **Domain name** with DNS you control (e.g. `api.yourdomain.com`).
- **MongoDB** — either **MongoDB Atlas** (managed, recommended) or a self-hosted `mongod` with auth enabled. **Never** run Mongo open to the internet without auth.
- **Firebase project** with a **service-account JSON** (for Google/Apple sign-in + FCM push). Download from Firebase Console → Project Settings → Service accounts → *Generate new private key*.

### DNS
Create an **A record**: `api.yourdomain.com → <server public IP>`. Wait for it to
resolve (`dig +short api.yourdomain.com`) before requesting certs.

---

## 2. Environment variables (production values)

The app **fails fast** on startup if any required var is missing or invalid (see
`src/core/config/index.ts`). Required vars and production guidance:

| Var | Required | Production value |
|-----|----------|------------------|
| `NODE_ENV` | yes | `production` (hides internal error details from clients) |
| `PORT` | no (def 3838) | `3838` — internal only, behind the proxy |
| `MONGO_URI` | **yes** | Atlas SRV string or `mongodb://user:pass@host:27017/vt_api?authSource=admin` |
| `CORS_ORIGINS` | no | Comma-separated exact origins of your web/admin frontends. **No `*`.** Mobile apps don't need an entry. e.g. `https://app.yourdomain.com,https://admin.yourdomain.com` |
| `JWT_ACCESS_SECRET` | **yes** | Random ≥32 chars. `openssl rand -base64 48` |
| `JWT_REFRESH_SECRET` | **yes** | A **different** random ≥32 chars |
| `ACCESS_TTL` | no | `15m` (short-lived access token) |
| `REFRESH_TTL` | no | `30d` |
| `FIREBASE_SERVICE_ACCOUNT_PATH` | no* | Path to the mounted JSON, e.g. `/run/secrets/firebase.json`. *Omit to disable social login + push.* |
| `SSL_KEY_PATH` / `SSL_CERT_PATH` | topology B only | Paths to PEM key + fullchain. Leave **unset** for topology A. |
| `RATE_LIMIT_WINDOW_MS` | no | `900000` (15 min) |
| `RATE_LIMIT_MAX` | no | `100` per IP per window (global) |
| `AUTH_RATE_LIMIT_MAX` | no | `10` per IP per window (auth endpoints) |

Generate the two secrets:
```bash
echo "JWT_ACCESS_SECRET=$(openssl rand -base64 48)"
echo "JWT_REFRESH_SECRET=$(openssl rand -base64 48)"
```

Create `/opt/vt-api/.env` on the server (mode `600`, owned by the deploy user). **Never commit it** — `.env`, `*.pem`, `*.key`, and `firebase-service-account.json` are already gitignored.

Example production `.env` (topology A):
```dotenv
NODE_ENV=production
PORT=3838
MONGO_URI=mongodb+srv://vtuser:STRONGPASS@cluster0.xxxx.mongodb.net/vt_api?retryWrites=true&w=majority
CORS_ORIGINS=https://app.yourdomain.com,https://admin.yourdomain.com
JWT_ACCESS_SECRET=<paste 48-byte base64>
JWT_REFRESH_SECRET=<paste different 48-byte base64>
ACCESS_TTL=15m
REFRESH_TTL=30d
FIREBASE_SERVICE_ACCOUNT_PATH=/app/secrets/firebase-service-account.json
RATE_LIMIT_WINDOW_MS=900000
RATE_LIMIT_MAX=100
AUTH_RATE_LIMIT_MAX=10
```

---

## 3. Database (MongoDB)

### Option 1 — MongoDB Atlas (recommended)
1. Create a free/shared or dedicated cluster.
2. **Database Access:** create a user with a strong password, role `readWrite` on `vt_api`.
3. **Network Access:** allowlist your server's public IP (avoid `0.0.0.0/0`).
4. Copy the SRV connection string into `MONGO_URI`, appending `/vt_api`.
5. Atlas gives you automated backups, TLS, and monitoring out of the box.

### Option 2 — Self-hosted MongoDB (Docker)
Run Mongo with **auth enabled** and bound to the Docker network only (not published to the host's public interface). See the compose file in §5 which does exactly this. After first boot, create the app user, and take responsibility for backups (§9).

> The repo's existing `docker-compose.yml` is **dev-only** (no auth, published port). Do **not** use it as-is in production — use the prod compose in §5.

### Seed reference data
Categories must be seeded once (the app expects them):
```bash
# inside the app container or with prod env loaded:
npm run seed
```

---

## 4. TLS certificates (topology A)

Install nginx + certbot and obtain a cert for the API subdomain:
```bash
sudo apt update && sudo apt install -y nginx certbot python3-certbot-nginx
sudo certbot --nginx -d api.yourdomain.com
```
Certbot writes the nginx TLS config and sets up **auto-renewal** (systemd timer).
Verify renewal works: `sudo certbot renew --dry-run`.

---

## 5. Deploy — Docker path (recommended)

### 5.1 Add a `.dockerignore` (prevents secrets leaking into image layers)
A `.dockerignore` is included in this repo (see the file created alongside this guide). It keeps `.env`, certs, and the Firebase JSON out of the build context. **Confirm it exists before building** — without it, `COPY . .` bakes your secrets into an image layer.

### 5.2 Fix the exposed port (one-time)
The current `Dockerfile` ends with `EXPOSE 3000`, but the app listens on `PORT` (default **3838**). This is cosmetic (EXPOSE is documentation; the real mapping is in compose), but align it to avoid confusion — change to `EXPOSE 3838` or set `PORT=3000` in your env. The compose below maps the container port explicitly regardless.

### 5.3 Production compose file
Create `docker-compose.prod.yml` on the server (or in the repo):

```yaml
services:
  api:
    build: .                     # or: image: registry.yourdomain.com/vt-api:TAG
    restart: unless-stopped
    env_file: .env
    # Publish ONLY to localhost so the proxy (on the host) can reach it,
    # but the world cannot hit the app port directly.
    ports:
      - "127.0.0.1:3838:3838"
    volumes:
      # Mount the Firebase service account read-only at the path in FIREBASE_SERVICE_ACCOUNT_PATH.
      - ./secrets/firebase-service-account.json:/app/secrets/firebase-service-account.json:ro
    depends_on:
      - mongo
    healthcheck:
      test: ["CMD", "wget", "-qO-", "http://localhost:3838/health"]
      interval: 30s
      timeout: 5s
      retries: 3

  # Omit this whole service if using MongoDB Atlas.
  mongo:
    image: mongo:7
    restart: unless-stopped
    command: ["--auth"]
    environment:
      MONGO_INITDB_ROOT_USERNAME: root
      MONGO_INITDB_ROOT_PASSWORD: ${MONGO_ROOT_PASSWORD}
      MONGO_INITDB_DATABASE: vt_api
    volumes:
      - mongo_data:/data/db
    # NOT published to the host — only reachable on the compose network.
    expose:
      - "27017"

volumes:
  mongo_data:
```

If self-hosting Mongo, set `MONGO_URI=mongodb://root:${MONGO_ROOT_PASSWORD}@mongo:27017/vt_api?authSource=admin` (the service name `mongo` resolves on the compose network) and provide `MONGO_ROOT_PASSWORD` in `.env`.

### 5.4 Build & run
```bash
cd /opt/vt-api
docker compose -f docker-compose.prod.yml up -d --build
docker compose -f docker-compose.prod.yml logs -f api      # watch startup
curl -s http://127.0.0.1:3838/health                        # {"data":{"status":"ok"},"error":null}
```

### 5.5 Updates / redeploys
```bash
git pull
docker compose -f docker-compose.prod.yml up -d --build
docker image prune -f
```
The app already handles **graceful shutdown** (SIGTERM/SIGINT → closes WS server, drains HTTP, disconnects Mongo), so rolling restarts won't drop the DB mid-request.

---

## 6. Deploy — bare-metal path (alternative, no Docker)

```bash
# as the deploy user, in /opt/vt-api
npm ci
npm run build            # → dist/
npm run seed             # once, to load categories
```

systemd unit `/etc/systemd/system/vt-api.service`:
```ini
[Unit]
Description=Viata la Tara API
After=network.target

[Service]
Type=simple
User=vtapi
WorkingDirectory=/opt/vt-api
EnvironmentFile=/opt/vt-api/.env
ExecStart=/usr/bin/node dist/server.js
Restart=on-failure
RestartSec=5
# Hardening
NoNewPrivileges=true
ProtectSystem=strict
ProtectHome=true
ReadWritePaths=/opt/vt-api

[Install]
WantedBy=multi-user.target
```
```bash
sudo systemctl daemon-reload
sudo systemctl enable --now vt-api
sudo systemctl status vt-api
journalctl -u vt-api -f
```

---

## 7. Reverse proxy (nginx) — topology A

The critical bit: **proxy the `/ws` path with WebSocket upgrade headers** (STOMP realtime lives there), and forward the real client IP so rate limiting works (`trust proxy` is already set to `1` in the app).

`/etc/nginx/sites-available/api.yourdomain.com`:
```nginx
server {
    listen 80;
    server_name api.yourdomain.com;
    return 301 https://$host$request_uri;   # certbot may manage this block
}

server {
    listen 443 ssl http2;
    server_name api.yourdomain.com;

    ssl_certificate     /etc/letsencrypt/live/api.yourdomain.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/api.yourdomain.com/privkey.pem;

    # Body limit — app itself caps JSON at 10kb; keep proxy a bit higher.
    client_max_body_size 1m;

    # WebSocket / STOMP realtime
    location /ws {
        proxy_pass http://127.0.0.1:3838;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_read_timeout 3600s;   # keep long-lived sockets alive
    }

    location / {
        proxy_pass http://127.0.0.1:3838;
        proxy_http_version 1.1;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
}
```
```bash
sudo ln -s /etc/nginx/sites-available/api.yourdomain.com /etc/nginx/sites-enabled/
sudo nginx -t && sudo systemctl reload nginx
```

Flutter/web clients now use `https://api.yourdomain.com` and `wss://api.yourdomain.com/ws`.

> **Caddy alternative:** a 4-line Caddyfile (`api.yourdomain.com { reverse_proxy 127.0.0.1:3838 }`) gives automatic TLS with no certbot; it proxies WebSockets transparently.

---

## 8. Topology B — app terminates TLS (no proxy)

Only if you can't run a proxy. Put the certs where the app can read them and set:
```dotenv
SSL_KEY_PATH=/app/certs/privkey.pem
SSL_CERT_PATH=/app/certs/fullchain.pem
PORT=443
```
Then publish `443:443` in compose and mount the cert dir read-only. The app serves HTTPS and handles the `/ws` upgrade over TLS automatically. **Downside:** you must handle Let's Encrypt renewal + reloading the process yourself (the app reads certs at startup only), and the process needs permission to bind `443`. This is why topology A is recommended.

---

## 9. Backups, logging, monitoring

- **Backups:** Atlas → enable automated backups + point-in-time recovery. Self-hosted → cron a nightly `mongodump` to off-box storage (S3/Backblaze), and **test a restore**.
- **Logs:** the app logs structured JSON via pino (no secrets, request IDs). With Docker, `docker compose logs` or ship to a collector (Loki/CloudWatch/Datadog). Rotate host logs.
- **Health checks:** point an uptime monitor at `GET https://api.yourdomain.com/health`. The compose healthcheck also gates container readiness.
- **Metrics/alerts:** alert on 5xx rate, event-loop lag, Mongo connection failures (the app retries connect 5× then exits — a crash-loop means DB is unreachable).

---

## 10. Go-live checklist

**Pre-deploy**
- [ ] Domain A-record → server IP, resolves publicly.
- [ ] Firewall: only 80/443 public; app port + Mongo port private.
- [ ] MongoDB provisioned with a strong-password user; IP allowlisted (Atlas) or `--auth` (self-hosted).
- [ ] `.env` created on server (mode 600) with **production** values; both JWT secrets are fresh, random, ≥32 chars, and **different**.
- [ ] `CORS_ORIGINS` lists only real frontend origins (no `*`).
- [ ] Firebase service-account JSON on server, path matches `FIREBASE_SERVICE_ACCOUNT_PATH`, mounted read-only.
- [ ] `.dockerignore` present (secrets excluded from image).
- [ ] TLS cert issued (certbot) and auto-renewal `--dry-run` passes.

**Deploy**
- [ ] `docker compose -f docker-compose.prod.yml up -d --build` (or systemd start).
- [ ] Startup logs show `Server listening ...`, `[db] connected`, and `[firebase] initialized` (or the "disabled" warning if intentionally off).
- [ ] `npm run seed` run once — categories exist (`curl https://api.yourdomain.com/api/categories`).
- [ ] nginx `nginx -t` clean; reloaded.

**Smoke test through the real domain**
- [ ] `GET https://api.yourdomain.com/health` → `{"data":{"status":"ok"},"error":null}`.
- [ ] `POST /api/auth/register` → 201 with tokens; `GET /api/users/me` with the token → 200.
- [ ] `POST /api/auth/refresh` rotates tokens.
- [ ] A `wss://api.yourdomain.com/ws` connect + STOMP `CONNECT` with a valid token → `CONNECTED` frame.
- [ ] Hit an auth endpoint 11+ times fast → rate-limit `TOO_MANY_REQUESTS` (confirms `trust proxy` + limiter work behind nginx).
- [ ] Trigger a 500 in a non-prod check and confirm prod responses show only generic `INTERNAL_ERROR` (no stack traces).

**Post-deploy**
- [ ] Uptime monitor on `/health`.
- [ ] Backups enabled + one restore tested.
- [ ] Log shipping/retention configured.
- [ ] Document the redeploy command for the team (§5.5).

---

## 11. Security notes specific to this app (already enforced in code — verify, don't weaken)
- Helmet, CORS allowlist (credentials on, no `*`), `express-mongo-sanitize`, `hpp`, compression, and rate limiting are applied globally in `src/core/security/`.
- JSON body capped at **10 KB** — image uploads must go to object storage; send URLs in `photos[]`.
- Refresh tokens are stored **hashed** with rotation + reuse detection; access tokens are short-lived. Keep `ACCESS_TTL` short.
- `trust proxy` is set to `1` — correct for exactly **one** proxy hop (nginx). If you add a second proxy/load balancer in front, increase it accordingly or client IPs (hence rate limits) will be wrong.
- In `production`, the error handler returns generic messages; never set `NODE_ENV` to anything else in prod.
- Roles come from the verified access token only — the client can never self-assign `business`/`admin`.
```
