# Deployment — cPanel Node.js App (Passenger)

Target: `https://api.genesiscoworkingspace.com.my`. Reference: `PLANNING.md` §11, §13.
This is a real Node process under Passenger, not a static drop.

---

## 0. One-time infrastructure

| Thing                     | How                                                                                                                                                                                                                                                                          |
| ------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Subdomain + SSL**       | Create the `api` subdomain record for `genesiscoworkingspace.com.my`, then issue an **AutoSSL** certificate covering `api.genesiscoworkingspace.com.my`. This is the classic launch-day blocker and must exist before any CORS testing is meaningful — do it first.          |
| **MySQL database + user** | cPanel → MySQL Databases. Create the DB and a user (both get the account-name prefix, e.g. `acct_genesis`). Grant **all privileges** on the DB to the user. Create the DB as `CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci` (MySQL 8).                                   |
| **Mail**                  | A real mailbox on the domain for `MAIL_FROM` (e.g. `no-reply@genesiscoworkingspace.com.my`) and the enquiry inbox for `ENQUIRY_NOTIFY_TO`. Add **SPF** and **DKIM** for the domain or transactional mail will land in spam / be rejected. SMTP is `localhost:465` on cPanel. |
| **Directories**           | `MEDIA_ROOT` — a directory Apache serves directly (e.g. `~/public_html/media` on the api subdomain docroot, or a dedicated docroot). `STORAGE_ROOT` — **outside** the web root (e.g. `~/backend-storage`), holds `originals/` and `tmp/`.                                    |

---

## 1. Create the Node.js App

cPanel → **Setup Node.js App** → Create Application:

- **Node version**: match `.nvmrc` (22).
- **Application mode**: Production.
- **Application root**: `genesiscoworkingspace-backend`
- **Application URL**: `api.genesiscoworkingspace.com.my`
- **Application startup file**: `app.js`

`app.js` is a two-line CommonJS shim (`require('./dist/server.js')`). Passenger's
entry point stays stable while the TypeScript build output under it is replaced
each deploy.

---

## 2. Environment

In the Node.js App panel, add every variable from `.env.example`. Do **not** upload
a `.env` file into the app root if it is web-reachable — a `.env` inside
`public_html` is downloadable. If you use a `.env` file, keep it in the app root
(not a docroot) and `chmod 600` it.

Production-required (the app **fails to boot** without these — `src/env.ts`):
`DB_*`, `PUBLIC_ORIGINS`, `ADMIN_ORIGINS`, `ASSET_BASE_URL`, `MEDIA_ROOT`,
`STORAGE_ROOT`, `JWT_ACCESS_SECRET`, `JWT_REFRESH_SECRET` (distinct 32+ char
randoms), `ADMIN_PORTAL_URL`, `IP_HASH_SALT` (16+ chars), and — only when
`NODE_ENV=production` — `SMTP_HOST`, `ENQUIRY_NOTIFY_TO`, `MAIL_FROM`.

Also set `NODE_ENV=production` and `GIT_SHA` (the commit you built from — it shows
on `/health`). `PORT` is set by Passenger — never hardcode it.

---

## 3. Build locally, deploy the output

```bash
npm ci
npm run build          # tsc -> dist/
```

Upload to the application root: **`dist/`**, `package.json`, `package-lock.json`,
`migrations/`, `seeds/`, `knexfile.ts`, `app.js`, `.nvmrc`. Do **not** upload a
locally-built `node_modules` — the native binaries (`sharp`, `bcryptjs` is pure JS
but `sharp` is not) will be wrong for the host.

Then in the Node.js App panel click **Run NPM Install** (it uses the venv Node that
Passenger runs).

---

## 4. First-deploy checks (over SSH)

```bash
cd ~/genesiscoworkingspace-backend
source /home/<acct>/nodevenv/genesiscoworkingspace-backend/22/bin/activate   # path from the panel's "Enter to the virtual environment" line

npm run check:sharp        # PLANNING §13 step 0 — the one remaining host probe
```

If `check:sharp` fails to load `sharp`, switch `src/modules/media/derivative.service.ts`
to the `@jsquash/*` WASM path (it is the one file shaped for that swap) and
redeploy. Everything else about the media module is unaffected.

```bash
npm run migrate            # NEVER on boot — a failed migration in a Passenger
                           # start loop is very hard to see
npm run seed               # media categories + the bootstrap superadmin (once)
```

`npm run seed:dev` is **local only** — it refuses `NODE_ENV=production`.

---

## 5. Apache: serve the media derivatives + lock down the app tree

Two `.htaccess` files (samples in `docs/`):

- **`docs/htaccess-media.sample`** → drop into `MEDIA_ROOT`. Adds the immutable
  year-long cache header (`Cache-Control: public, max-age=31536000, immutable`) and
  `Cross-Origin-Resource-Policy: cross-origin` so the landing page and admin portal
  can embed the images. Node writes the bytes; Apache serves them (PLANNING §7).
- **`docs/htaccess-api.sample`** → the api subdomain docroot. Denies direct access
  to dotfiles, `storage/`, `logs/`, `src/`, `migrations/`, `seeds/`,
  `node_modules/`, and `package*.json`. Passenger routes real requests before
  Apache would serve a file, so this is defence in depth.

---

## 6. Restart / redeploy

```bash
npm run build              # locally, then upload dist/
mkdir -p tmp && touch tmp/restart.txt        # Passenger picks this up
```

For a code change with no dependency change you only need to replace `dist/` and
touch `tmp/restart.txt`. For a schema change, run `npm run migrate` over SSH
**before** restarting.

---

## 7. Backups

- cPanel scheduled dump of **the database** and **`STORAGE_ROOT/originals/`**.
- Derivatives under `MEDIA_ROOT` are regenerable — `npm run reprocess-media` rebuilds
  every derivative from the stored originals. Originals are not regenerable from
  anything, so they are the thing that must be backed up.
- `npm run reprocess-media -- --gc` hard-deletes images soft-deleted more than 30
  days ago and removes their derivative directories — run it from cron monthly.

---

## 8. Post-deploy smoke test

```bash
curl https://api.genesiscoworkingspace.com.my/health
# -> { "status":"ok", "db":{"status":"ok",...}, "sha":"<GIT_SHA>", ... }

curl https://api.genesiscoworkingspace.com.my/spaces/genesis/gallery
# -> 200, JSON matching docs/API-CONTRACT.md (empty images[] is valid if nothing is published)

curl -X OPTIONS https://api.genesiscoworkingspace.com.my/enquiries \
  -H 'Origin: https://genesiscoworkingspace.com.my' \
  -H 'Access-Control-Request-Method: POST' -i
# -> 204 with Access-Control-Allow-Origin echoing the request origin, Max-Age 86400
```

Then in the admin portal: log in with the bootstrap superadmin phone + password,
confirm `GET /admin/auth/me`, and upload one image through `POST /admin/media`.
