# Deploying AI Radar

Target: a cPanel account with SSH at `/home/onairada`, MySQL 8, Node 20+.

## The shape of it

Two Node processes stay running; Apache only forwards to them.

```
  browser
    |
  Apache  :443            /home/onairada/public_html/.htaccess
    |  /api/*  ->  127.0.0.1:4000   NestJS   (ingestion cron lives here)
    |  /*      ->  127.0.0.1:3000   Next.js  (SSR)
    |
  MySQL 8
```

**Code does not go in `public_html`.** It goes in `/home/onairada/app`.
`public_html` keeps only the `.htaccess` above. Putting a Node project in a
web-served directory exposes `.env` — every secret in one HTTP request.

## First deploy

```bash
ssh onairada@your-host

# 1. Node 20+. Check first; cPanel default is often ancient.
node -v

# 2. Get the code OUTSIDE public_html
cd ~
git clone <your-repo-url> app
cd app

# 3. Database — create via cPanel > MySQL Databases, then:
#    note the cPanel-prefixed names, e.g. onairada_airadar

# 4. Config
cp apps/api/.env.example apps/api/.env
cp apps/web/.env.example apps/web/.env.local
nano apps/api/.env          # see "Production values" below
nano apps/web/.env.local

chmod 600 apps/api/.env apps/web/.env.local

# 5. PM2
npm install -g pm2   # if no global install rights: npm i -D pm2, use npx

# 6. Build, migrate, start
bash deploy/deploy.sh

# 7. Survive reboots
pm2 startup     # run the command it prints
pm2 save

# 8. Apache
cp deploy/htaccess-public_html ~/public_html/.htaccess
```

## Production values that differ from `.env.example`

`apps/api/.env`:

```ini
NODE_ENV=production
PORT=4000
PUBLIC_BASE_URL=https://yourdomain.com      # drives CORS — must be exact
API_PUBLIC_URL=https://yourdomain.com/api

DB_HOST=127.0.0.1
DB_USER=onairada_radar                      # cPanel prefixes these
DB_NAME=onairada_airadar
DB_PASSWORD=...

ANTHROPIC_API_KEY=sk-ant-...
CRON_ENABLED=true                           # false in the example; the
                                            # pipeline never runs without it
ADMIN_PASSWORD=...                          # long and random
ADMIN_SESSION_SECRET=...                    # openssl rand -hex 32
```

`apps/web/.env.local`:

```ini
PUBLIC_BASE_URL=https://yourdomain.com
API_BASE_URL=http://127.0.0.1:4000/api      # server-side: stays on localhost
NEXT_PUBLIC_API_BASE_URL=https://yourdomain.com/api   # browser: must be public
```

The two API URLs are deliberately different. The server talks to the API
directly over loopback; the browser cannot, so the admin console needs the
public one.

## Later deploys

```bash
ssh onairada@your-host 'bash ~/app/deploy/deploy.sh'
```

## Things that bite

**Unstyled site, no client JS.** `.next/static` is not part of the standalone
bundle. `deploy.sh` copies it; if you deploy by hand, copy it too.

**`npm ci --omit=dev` breaks two things.** The build needs TypeScript, and
`db:migrate` runs through `ts-node` — both are dev dependencies. This host must
keep dev dependencies installed; do not trim them to save space.

**CORS errors in the admin.** `PUBLIC_BASE_URL` in the API env must match the
browser's origin exactly — protocol and `www` included.

**Cron never fires.** `CRON_ENABLED` defaults to `false`.

**502 from Apache.** The Node process is down (`pm2 status`) or the host
disallows `mod_proxy` in `.htaccess`.

**Two of every article.** More than one API instance. Keep it at one.

## Before enabling AI spend

The editor has never run against a live model. Set `ANTHROPIC_API_KEY` but
leave `AI_ENABLED=false`, then:

```bash
cd ~/app && npm run dry-run    # fetch + filter only, costs nothing
```

Confirm sources fetch and the prefilter rejects as expected, then set
`AI_ENABLED=true` with `AI_MONTHLY_BUDGET_USD` low for the first month.
