# ShipRex CMS/API — GitHub → cPanel deployment runbook (new subdomain)

End-to-end, copy-pasteable. Goal: deploy this repo from GitHub to a **new
subdomain** on cPanel using a **secure read-only SSH deploy key**, create the
**MySQL** database, run **migrations**, start the app on **Phusion Passenger**,
enable **HTTPS**, and **test** it.

Throughout, replace these placeholders:

| Placeholder | Example | Meaning |
|-------------|---------|---------|
| `CPUSER` | `shiprex` | your cPanel account username |
| `MAINDOMAIN` | `shiprex.com` | the parent domain in cPanel |
| `SUBDOMAIN` | `api.shiprex.com` | the new subdomain we deploy to |
| `OWNER/REPO` | `shiprex/shiprex-website-cms-api` | your GitHub repo |
| `APPDIR` | `/home/shiprex/apps/shiprex-api` | where the code lives (outside web roots) |

> Two facts that drive the whole design:
> 1. **Passenger injects the app's cPanel "Environment variables" only at
>    runtime** — they are *not* present in an interactive SSH shell. Migrations
>    run from SSH, so we keep config in a **`.env` file in `APPDIR`** (the app
>    reads it too). One source of truth for both runtime and CLI.
> 2. The **application root** (`APPDIR`) is kept **outside `public_html`**. Only
>    Passenger serves the app; the source is never directly web-accessible.

---

## Phase 0 — Prerequisites (check once)

In cPanel, confirm these icons exist (ask your host to enable any missing one):
- **Domains** / **Subdomains**
- **MySQL® Databases**
- **SSH Access** (Manage SSH Keys) and **Terminal** (or external SSH)
- **Git™ Version Control**
- **Setup Node.js App** (Application Manager / Passenger)
- **SSL/TLS Status** (AutoSSL)

---

## Phase 1 — Create the new subdomain

1. cPanel → **Domains** (or **Subdomains**) → **Create A New Domain** /
   **Create**.
2. **Domain:** `SUBDOMAIN` (e.g. `api.shiprex.com`).
3. **Document Root:** accept the default (e.g. `/home/CPUSER/api.shiprex.com`).
   We will *not* put code here — Passenger will be pointed at `APPDIR` and cPanel
   writes its proxy `.htaccess` into this docroot automatically.
4. Create. If the parent domain's DNS is on this same server, the subdomain
   resolves automatically within minutes. (If DNS is external, add an `A`/`CNAME`
   record for `SUBDOMAIN` pointing to the server IP.)

Verify it resolves: `ping SUBDOMAIN` should return the server IP.

---

## Phase 2 — Create a secure SSH deploy key (cPanel side)

We use a **dedicated key** used *only* for pulling this one repo. Never reuse a
personal key, and never give cPanel write access to GitHub.

### 2a. Generate the key in cPanel

1. cPanel → **SSH Access** → **Manage SSH Keys** → **Generate a New Key**.
2. **Key Name:** `github_deploy`
3. **Key Type:** `ED25519` (modern, short, strong). If only RSA is offered, pick
   **RSA 4096**.
4. **Passphrase:** **leave empty.** A deploy key is used by automated,
   non-interactive `git` operations; a passphrase would block them. We contain
   the risk by making the key **read-only and scoped to a single repo** (Phase
   3), so a leaked key cannot push or touch other repos.
5. Generate.

### 2b. Authorize the key for this account

Back on **Manage SSH Keys**, find `github_deploy` under *Private Keys* →
**Manage** → **Authorize**. (This lets the account use it for SSH.)

### 2c. Copy the **public** key

On **Manage SSH Keys** → *Public Keys* → next to `github_deploy.pub` →
**View/Download** → copy the whole line (starts with `ssh-ed25519 …` or
`ssh-rsa …`). You'll paste it into GitHub next.

### 2d. Tell SSH to use this key for github.com

cPanel may store the key as `~/.ssh/github_deploy` (a non-default name), so we add
an SSH config entry. Open cPanel → **Terminal** (or SSH in), then:

```bash
# Make sure perms are correct (SSH refuses loose perms)
chmod 700 ~/.ssh
chmod 600 ~/.ssh/github_deploy
chmod 644 ~/.ssh/github_deploy.pub

# Map github.com to this key
cat >> ~/.ssh/config <<'EOF'

Host github.com
  HostName github.com
  User git
  IdentityFile ~/.ssh/github_deploy
  IdentitiesOnly yes
EOF
chmod 600 ~/.ssh/config
```

> If the key file isn't at `~/.ssh/github_deploy`, run `ls -la ~/.ssh` and adjust
> the `IdentityFile` path to the actual private-key filename.

---

## Phase 3 — Register the key on GitHub (read-only deploy key)

**Use a repository Deploy Key, not an account SSH key.** A deploy key grants
access to **only this repo**, and we keep it **read-only**.

1. GitHub → your repo **OWNER/REPO** → **Settings** → **Deploy keys** →
   **Add deploy key**.
2. **Title:** `cPanel SUBDOMAIN (read-only)`
3. **Key:** paste the public key from step **2c**.
4. **Allow write access:** **leave UNCHECKED.** cPanel only pulls; it never
   needs to push. Read-only = smallest blast radius if the key leaks.
5. **Add key.**

### Security rationale (why this is the safe choice)

- **Deploy key vs personal key:** a deploy key is scoped to one repository. A
  personal/account key would expose *every* repo you can access.
- **Read-only:** even if the server is compromised, the attacker cannot rewrite
  your source on GitHub.
- **Passphraseless but contained:** automation needs it; scope + read-only +
  single-repo limit the damage. Rotate it any time by deleting it on GitHub and
  regenerating in cPanel.
- **Secrets never go in Git:** DB passwords, JWT/session secrets, Brevo and AI
  keys live in `.env` on the server (Phase 6), which is git-ignored and denied by
  `.htaccess`.

### Verify the connection

In cPanel Terminal:

```bash
ssh -T git@github.com
# First time: type "yes" to trust github.com's fingerprint.
# Success looks like:
#   Hi OWNER/REPO! You've successfully authenticated, but GitHub does not
#   provide shell access.
```

That "does not provide shell access" message is **success** for a deploy key.

---

## Phase 4 — Clone the repo with cPanel Git Version Control

1. cPanel → **Git™ Version Control** → **Create**.
2. Toggle **Clone a Repository: ON**.
3. **Clone URL:** the **SSH** URL (must be SSH so the deploy key is used):
   ```
   git@github.com:OWNER/REPO.git
   ```
   (Do **not** use the `https://github.com/...` URL — that would ask for a
   password and ignore the key.)
4. **Repository Path:** `APPDIR` (e.g. `/home/CPUSER/apps/shiprex-api`). cPanel
   creates the folder. Keep it **outside** `public_html` and outside the
   subdomain docroot.
5. **Repository Name:** `shiprex-api` (label only).
6. **Create.** cPanel clones the default branch into `APPDIR`.

> If cloning fails with a permission/host error, re-check Phase 2d (`~/.ssh/config`
> + perms) and Phase 3 (key added to the repo), then `ssh -T git@github.com`
> again.

---

## Phase 5 — Create the MySQL database

1. cPanel → **MySQL® Databases**.
2. **Create New Database:** name it (cPanel prefixes it) →
   `CPUSER_shiprex` → **Create Database**.
3. **MySQL Users → Add New User:** username `CPUSER_shiprexapp`, generate a
   strong password (save it). **Create User.**
4. **Add User To Database:** select the user + the database → **Add** → grant
   **ALL PRIVILEGES** → **Make Changes**.

Record the **real prefixed names**:
- DB name: `CPUSER_shiprex`
- DB user: `CPUSER_shiprexapp`
- DB pass: the one you generated
- DB host: `localhost`

---

## Phase 6 — Create the `.env` file in `APPDIR`

This single file configures **both** the running app and the migration CLI.

cPanel → **File Manager** → go to `APPDIR` → (enable "Show Hidden Files") →
**+ File** → name it `.env`. Select it → **Edit**. Paste and fill:

```env
NODE_ENV=production
PORT=3000
APP_NAME=ShipRex CMS
APP_URL=https://SUBDOMAIN
PUBLIC_API_URL=https://SUBDOMAIN/api/v1

# Database (use the prefixed names from Phase 5)
DB_HOST=localhost
DB_PORT=3306
DB_NAME=CPUSER_shiprex
DB_USER=CPUSER_shiprexapp
DB_PASSWORD=YOUR_DB_PASSWORD
DB_SSL=false

# Secrets — generate unique random values (see command below)
SESSION_SECRET=PASTE_RANDOM_HEX
JWT_SECRET=PASTE_RANDOM_HEX
COOKIE_SECURE=true

# Public API access for the marketing website
CORS_ALLOWED_ORIGINS=https://www.MAINDOMAIN
BOOTSTRAP_API_KEY=PASTE_RANDOM_HEX

# Chatbot — keep disabled until you choose a provider
AI_PROVIDER=disabled
AI_MODEL=claude-3-5-sonnet-latest
ANTHROPIC_API_KEY=
OPENAI_API_KEY=

# Brevo (email) — fill when ready; leads are still captured if blank
BREVO_API_KEY=
BREVO_SENDER_NAME=ShipRex
BREVO_SENDER_EMAIL=hello@MAINDOMAIN
BREVO_CONTACT_LIST_ID=
SALES_NOTIFY_EMAIL=sales@MAINDOMAIN

# First admin user (created by the seeder)
ADMIN_EMAIL=you@MAINDOMAIN
ADMIN_PASSWORD=A_STRONG_PASSWORD
ADMIN_NAME=ShipRex Admin
```

Generate the three random secrets (cPanel Terminal):

```bash
for s in SESSION JWT BOOTSTRAP; do echo "$s=$(node -e "console.log(require('crypto').randomBytes(32).toString('hex'))")"; done
```

Lock the file down:

```bash
chmod 600 APPDIR/.env
```

`.env` is already in `.gitignore` and blocked by `.htaccess`, so it is never
committed and never web-served.

---

## Phase 7 — Create the Node.js application (Passenger)

1. cPanel → **Setup Node.js App** → **Create Application**.
2. **Node.js version:** **`22.x` or higher** (required — the chatbot's AI SDK needs Node ≥ 22). The `nodevenv` path below then uses `/22/` instead of `/18/`.
3. **Application mode:** `Production`.
4. **Application root:** `apps/shiprex-api` (the `APPDIR`, relative to home).
5. **Application URL:** select **`SUBDOMAIN`** from the dropdown. Leave the
   subpath empty (serve at the domain root).
6. **Application startup file:** `app.js`.
7. **Create.** cPanel provisions a Node virtualenv and writes the Passenger
   `.htaccess` into the subdomain's docroot.

Leave this page open — you'll use **Run NPM Install** and **Restart** here, and
the **"Enter to the virtual environment"** command it shows (needed next).

---

## Phase 8 — Install dependencies

Easiest: on the Node.js App page, click **Run NPM Install**.

Or via SSH (more control — copy the activate command from the app page; it looks
like this):

```bash
source /home/CPUSER/nodevenv/apps/shiprex-api/22/bin/activate && cd /home/CPUSER/apps/shiprex-api
npm install --omit=dev
```

> The chatbot provider SDKs are **optionalDependencies**, so install never breaks
> even though `AI_PROVIDER=disabled`. When you enable the chatbot later, run e.g.
> `npm install @ai-sdk/anthropic` inside this same virtualenv.

---

## Phase 9 — Run migrations + seed

Still inside the activated virtualenv and `APPDIR` (from Phase 8). The `.env` you
created supplies the DB credentials to the CLI:

```bash
# Create all tables + indexes (incl. FULLTEXT for the chatbot)
npx sequelize-cli db:migrate

# Bootstrap: first admin user, sample KB article, and the API client
# matching BOOTSTRAP_API_KEY
npx sequelize-cli db:seed:all
```

Expected: `== 20260101000000-init-schema: migrated`, then the seed completes with
no errors. (The session table is created automatically by the app on first boot.)

Quick sanity check:

```bash
node -e "const m=require('mysql2/promise');require('dotenv').config();m.createConnection({host:process.env.DB_HOST,user:process.env.DB_USER,password:process.env.DB_PASSWORD,database:process.env.DB_NAME}).then(async c=>{const[r]=await c.query('SELECT COUNT(*) n FROM admin_users');console.log('admin_users:',r[0].n);await c.end();})"
```

---

## Phase 10 — Start the app

On the **Setup Node.js App** page, click **Restart** (or **Start**).

CLI alternative (touches the Passenger restart trigger):

```bash
cd APPDIR && npm run cpanel:restart
```

Check the app's **stderr log** link on the Node.js App page if anything is off.
On a healthy boot you'll see `Database connection established.` and
`ShipRex CMS listening on port …`.

---

## Phase 11 — Enable HTTPS on the subdomain

1. cPanel → **SSL/TLS Status**.
2. Find `SUBDOMAIN`, tick it, click **Run AutoSSL**. Wait for the padlock /
   "Certificate: … valid".
3. We already set `COOKIE_SECURE=true` and `APP_URL=https://…` in `.env`, so
   sessions use Secure cookies.
4. (Optional) Force HTTPS: in the **subdomain docroot** `.htaccess`, **above** the
   Passenger block, add:
   ```apache
   RewriteEngine On
   RewriteCond %{HTTPS} off
   RewriteRule ^(.*)$ https://%{HTTP_HOST}/$1 [R=301,L]
   ```
   Do not remove the cPanel-managed Passenger lines.

---

## Phase 12 — Test the deployment

From your laptop (or cPanel Terminal). Replace `SUBDOMAIN` and use the
`BOOTSTRAP_API_KEY` you set in `.env`.

```bash
# 1. Health — DB connectivity
curl -s https://SUBDOMAIN/health
# → {"ok":true,"db":"up","uptime":...}

# 2. Public API status — feature flags
curl -s https://SUBDOMAIN/api/v1/status

# 3. Auth is enforced (no key → 401)
curl -s https://SUBDOMAIN/api/v1/kb/articles
# → {"ok":false,"error":{"code":"missing_api_key",...}}

# 4. KB read WITH key
curl -s -H "x-api-key: BOOTSTRAP_API_KEY" https://SUBDOMAIN/api/v1/kb/articles

# 5. Contact lead (marketing form path)
curl -s -X POST -H "x-api-key: BOOTSTRAP_API_KEY" -H "Content-Type: application/json" \
  -d '{"name":"Test Lead","email":"lead@example.com","reason":"callback"}' \
  https://SUBDOMAIN/api/v1/contact

# 6. Open a support ticket
curl -s -X POST -H "x-api-key: BOOTSTRAP_API_KEY" -H "Content-Type: application/json" \
  -d '{"requester_name":"Tester","requester_email":"t@example.com","subject":"Hi","description":"Testing deploy"}' \
  https://SUBDOMAIN/api/v1/tickets
```

**Admin portal:** open `https://SUBDOMAIN/admin` in a browser → sign in with
`ADMIN_EMAIL` / `ADMIN_PASSWORD` from `.env`. You should land on the dashboard and
see the test lead + ticket you just created.

> ⚠️ After confirming login works, change the admin password (or re-seed with a
> strong one) and, in **Admin → API & Settings**, create a *real* API client for
> the marketing site and disable/delete the bootstrap key.

---

## Phase 13 — Redeploying after you push to GitHub

Because the repo **is** the application root, redeploys are a pull + restart.

### Option A — cPanel UI (no SSH)
1. cPanel → **Git™ Version Control** → your repo → **Manage** → **Pull or Deploy**
   tab → **Update from Remote** (fetches + fast-forwards the branch).
2. If dependencies changed: **Setup Node.js App** → **Run NPM Install**.
3. If the schema changed: run migrations (Phase 9) over SSH.
4. **Setup Node.js App** → **Restart**.

### Option B — SSH one-liner
```bash
source /home/CPUSER/nodevenv/apps/shiprex-api/22/bin/activate && cd APPDIR \
  && git pull \
  && npm install --omit=dev \
  && npx sequelize-cli db:migrate \
  && npm run cpanel:restart
```

### Optional — auto-tasks on "Deploy HEAD Commit"
Add a `.cpanel.yml` at the repo root to run steps when you click **Deploy HEAD
Commit** in Git Version Control. Keep it minimal and reliable:

```yaml
---
deployment:
  tasks:
    - export NODEBIN=/home/CPUSER/nodevenv/apps/shiprex-api/22/bin
    - $NODEBIN/npm install --omit=dev --prefix /home/CPUSER/apps/shiprex-api
    - cd /home/CPUSER/apps/shiprex-api && $NODEBIN/npx sequelize-cli db:migrate
    - mkdir -p /home/CPUSER/apps/shiprex-api/tmp && date > /home/CPUSER/apps/shiprex-api/tmp/restart.txt
```
(Migrations are idempotent, so running them every deploy is safe.)

---

## Troubleshooting quick table

| Symptom | Likely cause / fix |
|---------|--------------------|
| `git clone` permission denied | `~/.ssh/config` missing/wrong, perms too open, or deploy key not added to the repo. Re-run `ssh -T git@github.com`. |
| 503 / "Incomplete response" in browser | Check the app **stderr log**. Usually a bad `DB_*` value or missing `.env`. |
| Migrations: `ER_ACCESS_DENIED` | DB user not added to the DB, or wrong **prefixed** name in `.env`. |
| Migrations can't find DB config | `.env` not in `APPDIR`, or you didn't run from `APPDIR`. The CLI reads `.env` via `config/config.js`. |
| Admin login redirect loop | `COOKIE_SECURE=true` but no SSL yet — finish Phase 11, or temporarily set it `false` and restart. |
| Chatbot returns the fallback line | Expected while `AI_PROVIDER=disabled`. Set provider + key + `npm install @ai-sdk/...`, then restart. |
| Code changed but no effect | Click **Restart** (or `npm run cpanel:restart`). Passenger caches the running process. |
| FULLTEXT migration error | Needs MySQL 5.6+/InnoDB. The KB search falls back to `LIKE` automatically if absent. |
