# Production deployment

This application deploys from GitHub Actions to a dedicated Linux server. Each
commit gets its own release directory. PM2 reloads the web process and queue
worker only after dependencies, the production build, and Prisma migrations
succeed.

## One-time server setup (WHM/cPanel)

Production runs on the same WHM/cPanel box as dev, but under its **own cPanel
account** (e.g. `awprod`) so a compromised or malicious dependency pulled in
by a dev deploy can't read production's `.env.production` — `npm ci` and
`npm run build` run arbitrary lifecycle scripts as whatever OS user runs
them, and cPanel accounts are separate OS users with separate home
directories. Do not reuse `newaw` (the dev account) for production, and do
not point the account's domain at `adultworld.ai` until the pipeline below is
verified end-to-end — that domain currently belongs to a different account
running the pre-migration site, and repointing it is a deliberate cutover
step, not part of initial setup.

There's no root/Caddy install here: ports 80/443 are already owned by the
box's front-end web server, and the account's docroot is reverse-proxied to
the local Node app via `.htaccess`, the same mechanism `dev.adultworld.ai`
already uses. Pick a port not already in use on the box (dev uses `3001`;
check `pm2 list` across accounts if unsure) — the example below uses `3002`.

In WHM: create the cPanel account, with its primary (or an addon) domain set
to whatever hostname you're verifying against before the real cutover. Then,
as that account's user:

```bash
mkdir -p ~/public_html/{shared,releases,incoming}
```

Create `~/public_html/shared/.env.production` with all production values. It
must at least contain the database, authentication, Redis, email, storage,
Stripe, and public URL variables used by the application. Restrict the file:

```bash
chmod 600 ~/public_html/shared/.env.production
```

Add `~/public_html/.htaccess` (this file is not touched by deploys — it lives
alongside `releases/`, `shared/`, and the `current` symlink, not inside a
release):

```apache
RewriteEngine On
RewriteRule ^(.*)$ http://127.0.0.1:3002/$1 [P,L]
```

If `node`/`npm`/`pm2` aren't on `PATH` for non-interactive SSH sessions (WHM's
Node Selector usually doesn't put them there), add `~/public_html/shared/env.sh`
sourcing the Node Selector's activation script — see the dev section below for
the exact mechanism; `activate-release.sh` sources it automatically if
present. Then install PM2 for this account and configure it to start on
reboot: `npm install --global pm2` followed by `pm2 startup` (as this
account's user, not root) and the command it prints.

## GitHub production environment

Create an environment named `production`. Optionally require an approval for
deployments. Add these environment secrets — note the names are `DEV_*`,
matching what dev's secrets are called, but these live in the `production`
environment so they're a completely separate set of values (`deploy-production.yml`
reads them from here, not from dev's):

| Secret | Example | Purpose |
| --- | --- | --- |
| `DEV_SSH_HOST` | server IP | Same box as dev, different account |
| `DEV_SSH_PORT` | `22` | SSH port; defaults to 22 |
| `DEV_SSH_USER` | `awprod` | The dedicated production cPanel account |
| `DEV_PATH` | `/home/awprod/public_html` | That account's docroot |
| `DEV_SSH_KEY` | private key | Key accepted by `awprod`, generated fresh — do not reuse the dev key |

Add the corresponding public key to `awprod`'s `~/.ssh/authorized_keys`. The
workflow deploys on every push to `main`, and it can also be started manually
from GitHub Actions. The first run creates the initial release; the server
does not need access to the GitHub repository.

`deploy-production.yml`'s activate step already passes `3002` as the port
(confirmed free on the box alongside dev's `3001` and the old `newaw`
prod-in-waiting app's `3000`), the same way `deploy-dev.yml` passes
`3001 -dev`. Match that port in the `.htaccess` above and reuse it if you
ever change it.

### Domain cutover to adultworld.ai

Once the `awprod` deploy pipeline is verified against its interim domain: in
WHM, remove `adultworld.ai`/`www.adultworld.ai` from the old site's account
and add them to `awprod` (or move the account's primary domain). This is real
downtime for the current live site at that domain — do it deliberately, not
as a side effect of testing.

## Dev deployment (WHM/cPanel)

`deploy-dev.yml` deploys `develop` the same way, to a second app root so it can
run alongside production on the same box without colliding: PM2 apps are
suffixed (`web-dev`, `worker-dev` — via `PM2_APP_SUFFIX`) and the app listens
on port 3001 instead of 3000 (via `PORT`), both passed as extra args to
`activate-release.sh`.

One-time setup on the dev app root (e.g. `/home/USER/dev.adultworld.ai`
under WHM):

```bash
mkdir -p /home/USER/dev.adultworld.ai/{shared,releases,incoming}
```

Create `shared/.env.production` there (same rules as prod, dev values), and
add `.htaccess` in the app root (alongside `shared/`, `releases/`, `current`)
reverse-proxying to the app:

```apache
RewriteEngine On
RewriteRule ^(.*)$ http://127.0.0.1:3001/$1 [P,L]
```

WHM's Node Selector usually does **not** put `node`/`npm`/`pm2` on `PATH` for
a non-interactive SSH session (which is what the deploy runs under). If
commands fail with "command not found", create `shared/env.sh` in the app
root with a line sourcing the Node Selector's activation script, e.g.:

```bash
source /home/USER/nodevenv/dev.adultworld.ai/22/bin/activate
```

`activate-release.sh` sources this file automatically if it exists — same
mechanism works for the production app root if it ever needs it.

Add a `development` GitHub environment with `DEV_SSH_HOST`, `DEV_SSH_PORT`,
`DEV_SSH_USER`, `DEV_SSH_KEY`, and `DEV_PATH` secrets, and add the deploy
user's public key to that account's `~/.ssh/authorized_keys`. Note these are
a separate set of values from the identically-named secrets in the
`production` environment above — GitHub environment secrets are scoped per
environment, so the same names holding different values in each is
intentional, if confusing.

## Rollback

The script automatically restores the prior release if the PM2 reload or health
check fails. For a manual rollback:

```bash
ln -sfn ~/public_html/releases/PREVIOUS_SHA ~/public_html/current
cd ~/public_html/current
pm2 startOrReload ecosystem.config.cjs --update-env
```

Database migrations cannot be automatically reversed. Production migrations
must remain compatible with both the new release and the immediately preceding
release. Use expand-and-contract migrations for destructive schema changes.

## Operational notes

- `/api/health` is intentionally a lightweight process health check and does not
  expose database or Redis details publicly.
- The worker currently runs through `tsx`, so deployments install development
  dependencies as well as runtime dependencies.
- PM2 logs are available through `pm2 logs`; configure log rotation on the
  server.
- Back up PostgreSQL and uploaded data independently of application releases.
