# NCII Shield — Production Deploy Guide

Target: Namecheap shared hosting with cPanel + Phusion Passenger + Node.js.

## Prerequisites

- cPanel access with "Setup Node.js App" enabled
- SSH access (Namecheap shared supports this; if not, skip the SSH-only steps and use the cPanel File Manager + env editor instead)
- Domain pointed at the cPanel server with a valid TLS cert (Namecheap auto-issues via AutoSSL)
- Stripe account in good standing (charges enabled, business profile complete)

## 1. Stripe setup (one-time)

Run the automated onboarding script with your Stripe **live** restricted key:

```bash
STRIPE_SECRET_KEY=rk_live_xxx \
STRIPE_PUBLISHABLE_KEY=pk_live_xxx \
node scripts/setup-stripe-products.js
```

This creates:
- 4 products: Standard, Pro, Agency, Credit Pack
- 7 prices (monthly + annual × 3 subscription plans, 1 one-time credit pack)
- 1 webhook endpoint at `https://takedowns.vesamuni.com/webhook/stripe`

Idempotent — safe to re-run if you change pricing later.

After the script prints its `.env` fragment, in **Stripe Dashboard** finish:

| Setting | Where | Why |
|---------|-------|-----|
| Enable Stripe Tax | Settings → Tax | Required for `automatic_tax` on checkouts |
| Allow promotion codes | Settings → Customer portal | `allow_promotion_codes: true` is set in every checkout |
| Business name / logo / support email | Settings → Branding | Shows on Stripe-hosted checkout pages |
| Public business details | Settings → Public details | Required before going live |

> **After launch + you confirm everything works → ROLL THIS KEY** in
> Dashboard → API keys → click the restricted key → Roll. Then update
> `STRIPE_SECRET_KEY` on the server.

## 2. Server upload (SSH + rsync, excludes runtime artifacts)

From your local box:

```bash
cd /path/to/local/ncii-shield
rsync -avz --exclude=node_modules \
           --exclude=.git \
           --exclude='data/' \
           --exclude='backups/' \
           --exclude='uploads/' \
           --exclude='*.log' \
           --exclude='*.sqlite*' \
           --exclude='.env' \
           --exclude='.preview/' \
           --exclude='test-debug*' \
           --exclude='cookies.txt' \
           --exclude='state-snapshot.txt' \
           ./  user@yourserver:~/ncii-shield/
```

> The `--exclude` list matters: `node_modules/` will be rebuilt on the
> server (Linux natives: `better-sqlite3`, `sharp`, `bcrypt`), `.env`
> contains secrets (paste via cPanel UI instead), `data/` is the local
> dev database + log dir (the server creates a fresh empty `data/` on
> first boot via `config/db.js`).

If you can't use SSH/rsync and are uploading via **cPanel File Manager**
instead, do **not** upload `data/`, `backups/`, `*.sqlite*`, `*.log`,
`.env`, or `node_modules/`. The `.htaccess` deny rules below also block
these as a defense-in-depth backup.

## 3. cPanel: create the Node.js App

1. cPanel → **Setup Node.js App** → **Create Application**
2. Fill in:
   - **Node.js version**: `20` (or `22`) — pick one that matches the `engines.node` range in `package.json`
   - **Application mode**: `Production`
   - **Application root**: `ncii-shield`
   - **Application URL**: `takedowns.vesamuni.com` (or a subdirectory if mounted under a subpath)
   - **Application startup file**: `app.js`
   - **Application passenger log file**: `logs/ncii-shield.log` (Passenger will create this)
3. Click **Create**
4. **Click "Run NPM Install"** — this builds `node_modules/` on the server (Linux natives: `better-sqlite3`, `sharp`, `bcrypt`).
5. cPanel shows you the **virtual environment** activation command. Note it; you'll need it for `seed-admin.js`.

## 4. Environment variables (cPanel UI)

In the Node.js App card, click **"Add Variable"** for each:

| Variable | Value |
|----------|-------|
| `NODE_ENV` | `production` |
| `APP_URL` | `https://takedowns.vesamuni.com` |
| `PUBLIC_BASE_URL` | `https://takedowns.vesamuni.com` |
| `SESSION_SECRET` | output of `node -e "console.log(require('crypto').randomBytes(48).toString('base64'))"` run on YOUR local box |
| `TRUST_PROXY` | `1` |
| `DB_PATH` | `./data/ncii.sqlite` |
| `BACKUP_DIR` | `./backups` |
| `SESSION_DB_DIR` | `./data` |
| `STRIPE_SECRET_KEY` | your live restricted key (`rk_live_…`) |
| `STRIPE_PUBLISHABLE_KEY` | matching `pk_live_…` |
| `STRIPE_WEBHOOK_SECRET` | `whsec_…` from `setup-stripe-products.js` output |
| `STRIPE_API_VERSION` | `2026-06-24.dahlia` (already pinned in code) |
| `STRIPE_PRICE_STANDARD` | `price_…` from script output |
| `STRIPE_PRICE_STANDARD_ANNUAL` | `price_…` |
| `STRIPE_PRICE_PRO` | `price_…` |
| `STRIPE_PRICE_PRO_ANNUAL` | `price_…` |
| `STRIPE_PRICE_AGENCY` | `price_…` |
| `STRIPE_PRICE_AGENCY_ANNUAL` | `price_…` |
| `STRIPE_PRICE_CREDIT_PACK_SMALL` | `price_…` |
| `RESEND_API_KEY` | from Resend dashboard |
| `RESEND_FROM_EMAIL` | `noreply@takedowns.vesamuni.com` |
| `RESEND_FROM_NAME` | `NCII Shield` |
| `RESEND_ALERT_TO` | `admin@takedowns.vesamuni.com` |
| `SCRAPEDO_API_KEY` | from Scrape.do dashboard (or add via `/admin → API Keys`) |
| `MiniMax_API_KEY` | from MiniMax dashboard (or add via `/admin → API Keys`) |
| `MiniMax_API_URL` | `https://api.minimax.io/v1` |
| `MiniMax_MODEL` | `MiniMax-M2.7` |
| `SCRAPEDO_DAILY_QUOTA` | `950` |
| `MiniMax_DAILY_QUOTA` | `950` |
| `RESEND_DAILY_QUOTA` | `95` |
| `LOGIN_MAX_ATTEMPTS` | `5` |
| `LOGIN_LOCKOUT_MINUTES` | `15` |
| `SEED_ADMIN_EMAIL` | `admin@takedowns.vesamuni.com` |
| `SEED_ADMIN_PASSWORD` | a STRONG password (≥10 chars, number, symbol) |
| `SEED_ADMIN_NAME` | `Platform Admin` |

The cPanel UI masks values after save. Good.

## 5. Apache / `.htaccess`

The repo's `.htaccess` is configured for cPanel + Passenger. Copy it:

```bash
cp ~/ncii-shield/.htaccess ~/public_html/.htaccess
```

Then edit the placeholders in `~/public_html/.htaccess`:

- Replace `yourcpanel` with your actual cPanel username (visible in cPanel sidebar)
- Replace `nodevenv/ncii-shield/18/bin/node` with the version you selected in step 3 (e.g. `20/bin/node`)

Verify the file ends up with paths like:
```
PassengerAppRoot /home/YOURUSER/ncii-shield
PassengerNodejs /home/YOURUSER/nodevenv/ncii-shield/20/bin/node
```

## 6. Bootstrap the superadmin (SSH once)

```bash
ssh user@yourserver
source /home/YOURUSER/nodevenv/ncii-shield/20/bin/activate   # cPanel shows this exact command
cd ~/ncii-shield
node seed-admin.js --email=admin@takedowns.vesamuni.com --password='STRONG-Pw-2026!' --name='Platform Admin'
```

Sign in at `https://takedowns.vesamuni.com/login` and enroll 2FA from `/2fa`.

## 7. Smoke test

```bash
# 1. Liveness — must always 200
curl -i https://takedowns.vesamuni.com/health/live

# 2. Readiness — needs DB + Stripe + workers
curl -i https://takedowns.vesamuni.com/health/ready
# → 200 { "status": "ok", "db": "ok", "workers": { all "running" }, "keys": { scrape_do: {active: ≥1}, resend: {active: ≥1} } }
```

If `/health/ready` returns 503 with `keys.resend.active === 0`, you forgot to set `RESEND_API_KEY` (or the key was rolled in Resend).

If `/billing/plans` returns the full plan list, Stripe is wired correctly.

## 8. Real checkout test

1. Open `https://takedowns.vesamuni.com/billing` in a browser, signed in as your superadmin
2. Click **Buy Credit Pack ($3)** → confirm Stripe Checkout opens
3. Pay with a real card (this is LIVE — $3.00 will be charged)
4. Confirm:
   - Stripe Dashboard → Payments shows the charge
   - Webhook fires (Dashboard → Webhooks → your endpoint → recent deliveries)
   - In the app: `/admin/alerts` shows no failures, user has `extra_scans: 5, extra_takedowns: 3`
5. Refund via Dashboard if you don't want to keep the $3

## 9. Daily backup cron

cPanel → **Cron Jobs** → add:

```
0 3 * * *  cd ~/ncii-shield && node scripts/backup-data.js >> ~/logs/backup.log 2>&1
```

Verifies backup integrity (PRAGMA integrity_check + table counts) and writes a timestamped zip-free directory under `~/ncii-shield/backups/`.

Optional: add a second cron that rsyncs `~/ncii-shield/backups/` to offsite storage.

## 10. Post-launch

1. **Roll the Stripe key** in Dashboard → API keys (the one we used was pasted in chat, so it MUST be rotated before launch + publicly advertising the app).
2. Submit `takedowns.vesamuni.com` to https://hstspreload.org (header is already set with `preload`).
3. Update the Resend "from" domain's DKIM/SPF in DNS (Resend walks you through it when you add the domain).
4. Add a Cloudflare proxy in front of the Namecheap origin (optional, gives you WAF + DDoS + analytics).

## Troubleshooting

| Symptom | Cause |
|---------|-------|
| All API routes 404 | `.htaccess` rewrite to `index.html` not removed. Check the file's catch-all rule is gone. |
| `/health/ready` returns 503 `db: down` | `data/` directory not writable by Passenger user. `chmod 775 ~/ncii-shield/data` and ensure ownership. |
| Stripe webhook returns 400 "Signature mismatch" | `STRIPE_WEBHOOK_SECRET` in `.env` doesn't match the webhook endpoint's signing secret. Re-run `scripts/setup-stripe-products.js` or copy from Dashboard. |
| Better-sqlite3 fails to load | Node version mismatch. Verify `package.json` engines + the Node selected in Setup Node.js App. Re-run NPM Install. |
| Sharp fails to load | Same as above — sharp downloads prebuilts at install time and is version-sensitive. |
| Email sends fail with `401` | `RESEND_API_KEY` rolled in Resend Dashboard. Update the env var. |
| `Address already in use` on boot | Stale Passenger worker. Click Restart in Setup Node.js App. |
| Sessions lost on every request | `SESSION_SECRET` empty or placeholder — assertProductionEnv should have caught this. Check `~/.ncii-shield/data/server.log` for the boot-fail message. |