# Reach Immigration site: running this folder on the server

This folder is a self-contained Next.js production build. It is **not** static files: it runs as a Node.js server (`server.js`). There is nothing to build, `npm install` or `npm start` on the server. Run `server.js` directly.

## Contents

| Item | Notes |
|---|---|
| `server.js`, `package.json` | the server entry (generated by Next, do not edit) |
| `dist/` | the build: pre-rendered pages and static assets |
| `public/` | images, icons, fonts |
| `node_modules/` | only the dependencies the server needs |
| `.env.local` | **hidden file**: API URL, API key, webhook secret |
| `ecosystem.config.cjs` | optional pm2 config (port 3000, name `reach-site`) |

All of it must be in the same folder. The hidden `dist/` and `.env.local` are easy to miss when copying by hand.

## Server requirements

- **Node.js 22 LTS** (minimum 20.9). No npm install needed.
- **unzip**: `sudo apt install -y unzip` if `unzip` is not found.
- **pm2**: `npm i -g pm2`
- **nginx** in front for HTTPS (or Cloudflare).
- The server must reach the API at `REACH_API_URL` (in `.env.local`) **at runtime**. Pages refresh from the API every hour.

## Release steps

### 1. On your machine: build and compress

```bash
cd customer-site
rm -rf dist/cache/fetch-cache      # so pages are built from the latest CMS content
npm run package                    # builds, assembles release/, and writes reach-release.zip
```

`reach-release.zip` is the one file to upload. It includes the hidden files (`.env.local`).

### 2. Upload

```bash
scp reach-release.zip user@SERVER_IP:/tmp/
```

Or upload the file to `/tmp/` with FileZilla.

### 3. On the server: unpack

Live path used on this server: `/var/www/html/reachimmigration`.

```bash
cd /var/www/html/reachimmigration
pwd                                  # check it before the next line
rm -rf ./* ./.[!.]*                  # empty the old version (back it up first if unsure)
unzip -o /tmp/reach-release.zip
ls -a                                # server.js, dist, public, node_modules, .env.local must be listed
rm /tmp/reach-release.zip
```

`server.js` must sit **directly** in this folder, not inside a `release/` subfolder.

### 4. Start (first time) or reload (later releases)

First time:

```bash
PORT=3001 HOSTNAME=127.0.0.1 pm2 start server.js --name reach-immigration-web
pm2 save
pm2 startup          # run the sudo command it prints so the site survives reboots
```

Later releases, after steps 1 to 3:

```bash
pm2 reload reach-immigration-web
```

Using the bundled config instead (port 3000, name `reach-site`): `pm2 start ecosystem.config.cjs`.

### 5. Check

```bash
curl -I http://127.0.0.1:3001/en     # expect HTTP 200
pm2 status                           # reach-immigration-web must be "online" with 0 restarts
```

Then open `https://<domain>/en` and `/ar` in a browser, and submit the contact form once.

The server listens on `127.0.0.1` only. Keep the port closed to the internet and let nginx proxy to it.

## Next release: what to do

The server is already set up (pm2 process, domain, webhook), so a new release is only these steps.

1. **Machine:** commit your changes, and merge `development` into `main` if the release is cut from `main`.
2. **Machine:** build and compress (one command).
   ```bash
   rm -rf dist/cache/fetch-cache
   npm run package          # also creates reach-release.zip
   ```
3. **Machine:** upload `scp reach-release.zip user@SERVER_IP:/tmp/`
4. **Server:** back up the current version, so you can roll back.
   ```bash
   tar -czf ~/reach-backup-$(date +%F).tar.gz -C /var/www/html/reachimmigration .
   ```
5. **Server:** stop the app, replace the files, start it.
   ```bash
   pm2 stop reach-immigration-web
   cd /var/www/html/reachimmigration
   pwd                                  # must print /var/www/html/reachimmigration
   rm -rf ./* ./.[!.]*
   unzip -o /tmp/reach-release.zip
   ls -a                                # server.js, dist, public, node_modules, .env.local
   pm2 restart reach-immigration-web --update-env
   rm /tmp/reach-release.zip
   ```
6. **Server:** check.
   ```bash
   curl -I http://127.0.0.1:3001/en     # expect HTTP 200
   pm2 status                           # online, 0 restarts
   ```
   Then open the live site in `/en` and `/ar`.
7. **If something is wrong, roll back:** run `pm2 stop reach-immigration-web`, empty the folder as in step 5, unpack `~/reach-backup-<date>.tar.gz` into it, then `pm2 restart reach-immigration-web`.

Do not run `pm2 delete` or `pm2 start` again. The process definition is already saved, so `pm2 restart` is enough.

`.env.local` is part of every release and overwrites the server's copy. Make environment changes in the project's `.env.local`, not on the server, or the next release will undo them.

## Domain and HTTPS: pick one option

The app listens on `127.0.0.1:3001`, so something has to put it on the public domain. Cloudflare's proxy cannot reach port 3001 directly (it only forwards to ports such as 80, 443, 8080 and 8443). Use Option A or Option B.

### Option A: nginx (or Apache) in front

Use this if the server already runs nginx for other sites.

```nginx
server {
    listen 80;
    server_name example.com www.example.com;   # the real domain

    location / {
        proxy_pass         http://127.0.0.1:3001;   # the PORT you started with
        proxy_http_version 1.1;
        proxy_set_header   Host              $host;
        proxy_set_header   X-Forwarded-For   $proxy_add_x_forwarded_for;
        proxy_set_header   X-Forwarded-Proto $scheme;
        proxy_set_header   Upgrade           $http_upgrade;
        proxy_set_header   Connection        "upgrade";
    }
}
```

Then `sudo certbot --nginx -d example.com -d www.example.com`, or put the domain behind Cloudflare.

### Option B: Cloudflare Tunnel (no nginx or Apache)

The `cloudflared` service on the server opens an outbound connection to Cloudflare, so no inbound ports are needed and Cloudflare handles HTTPS. The app keeps running on `127.0.0.1:3001` exactly as above.

Requirement: the domain's DNS is managed in Cloudflare.

1. In the Cloudflare dashboard open **Zero Trust > Networks > Tunnels > Create a tunnel** and choose **Cloudflared**. Give it a name.
2. Choose **Debian** (or your OS). Cloudflare shows an install command that contains your tunnel token. Run it on the server. It installs `cloudflared` and starts it as a service.
3. Open the tunnel's **Public Hostname** tab and click **Add a public hostname**:
   - Hostname: `example.com` (add a second entry for `www.example.com` if needed)
   - Service type: `HTTP`
   - URL: `localhost:3001` (the `PORT` you started with)
4. If the hostname already has an A, AAAA or CNAME record in DNS, delete it first. The tunnel creates its own CNAME record.
5. Check:
   ```bash
   systemctl status cloudflared          # active (running)
   curl -I https://example.com/en        # expect HTTP 200
   ```

Notes:
- Keep the server firewall closed for 80, 443 and 3001. The tunnel needs no inbound access.
- The app reads Cloudflare's `cf-ipcountry` header (rating badge, nearest office). Traffic through the tunnel carries it, so it works.
- Running Node directly on port 80 with Cloudflare "Flexible" SSL also works, but it needs root and leaves the Cloudflare-to-server leg unencrypted. Use the tunnel instead.

## Webhook

In the backend's webhook config, point the cache-purge hook at `https://<domain>/api/revalidate` and use the `REACH_REVALIDATE_SECRET` value from `.env.local`. Without it, CMS edits show only after the hourly refresh.

## Troubleshooting

- **Logs:** `pm2 logs reach-immigration-web --lines 40 --nostream`
- **Status `errored` with many restarts, script path `/usr/bin/npm`:** the process was started with `npm start`. Delete it (`pm2 delete <id>`) and start it with `server.js` as in step 4. This build has no `start` script.
- **`Cannot find module` or missing `dist/`:** the upload was incomplete. Unpack the archive again into an emptied folder.
- **Site shows no CMS content:** the API was unreachable at build time, or `API_SOURCE_MODE` in `.env.local` is `standin`. Pages refresh from the API every hour once it is reachable.
- **`EADDRINUSE`:** the port is taken. Check `ss -tlnp | grep 3001`, or start on another `PORT` and update the nginx `proxy_pass` to match.
- **502 from nginx:** the app is not running on the port nginx points at. Compare `pm2 status` with `proxy_pass`.
