← back to NationalPaperHangers
GO_LIVE.md
281 lines
# NPH Go-Live Runbook
End-to-end checklist for taking National Paper Hangers from local
`http://localhost:9765` (Mac2) to `https://nationalpaperhangers.com`
(public, Kamatera-hosted). Every step is reversible until §6 (Stripe webhook
registration) and §3 (DNS swap) — those two are externally visible.
DNS swap is **gated on Steve's explicit approval**. Everything else can
ship in test mode without touching the public domain.
---
## 1. Pre-flight env-var checklist
Run `npm run go-live-check` to print the live state of every env var the
app reads. The script exits non-zero if any required-for-live var is absent
while `NODE_ENV=production`.
| Var | Req | Where | Notes |
|---|---|---|---|
| `NODE_ENV` | always | .env | `production` for live |
| `PORT` | always | .env | `9765` (Mac2) / `9765` (Kamatera) |
| `PUBLIC_URL` | live | .env | `https://nationalpaperhangers.com` |
| `PGHOST` / `PGPORT` / `PGDATABASE` / `PGUSER` / `PGPASSWORD` | always | .env | Standalone PG, NOT `dw_unified` |
| `SESSION_SECRET` | live | /secrets | 32+ random bytes; rotate quarterly |
| `BOOKING_SIGNING_SECRET` | live | /secrets | HMAC for `/bookings/:uuid?t=…` |
| `UNSUBSCRIBE_SIGNING_SECRET` | live | /secrets | One-click unsub HMAC |
| `STRIPE_SECRET_KEY` | live | /secrets → routes.json[`stripe`] | `sk_live_*` |
| `STRIPE_PUBLISHABLE_KEY` | live | /secrets | `pk_live_*` (exposed to browser) |
| `STRIPE_WEBHOOK_SECRET` | live | /secrets | `whsec_*` for `/webhooks/stripe` sig verify |
| `STRIPE_PRICE_PRO_MONTH` | live | /secrets | Stripe price ID |
| `STRIPE_PRICE_PRO_YEAR` | live | /secrets | Stripe price ID |
| `STRIPE_PRICE_SIGNATURE_MONTH` | live | /secrets | Stripe price ID |
| `STRIPE_PRICE_SIGNATURE_YEAR` | live | /secrets | Stripe price ID |
| `STRIPE_PRICE_ENTERPRISE_MONTH` | live | /secrets | Stripe price ID |
| `STRIPE_DEV_ACCEPT_UNSIGNED` | optional | .env | Dev escape only — refused in prod |
| `NPH_DEFAULT_DEPOSIT_CENTS` | optional | .env | Default `9900` ($99) |
| `NPH_PLATFORM_FEE_BPS` | optional | .env | Default `1000` (10%) |
| `NPH_PLATFORM_ADMIN_INSTALLER_IDS` | optional | .env | Comma list — staff see platform totals on /admin/billing |
| `GEORGE_URL` | live | .env | `http://localhost:9850` (Mac) / `http://100.107.67.67:9850` (tailnet) |
| `GEORGE_USER` / `GEORGE_PASS` | live | /secrets | George basic-auth |
| `GEORGE_ACCOUNT` | optional | .env | `info` for multi-account |
| `EMAIL_FROM` | live | .env | `info@nationalpaperhangers.com` |
| `EMAIL_FROM_NAME` | optional | .env | `National Paper Hangers` |
| `MAILING_ADDRESS` | live | .env | **CAN-SPAM gate refuses sends without this** |
---
## 2. Stripe live-mode steps
### 2.1 Save secrets via the secrets-manager skill
```
/secrets add STRIPE_SECRET_KEY=sk_live_…
/secrets add STRIPE_PUBLISHABLE_KEY=pk_live_…
/secrets add STRIPE_WEBHOOK_SECRET=whsec_…
/secrets add STRIPE_PRICE_PRO_MONTH=price_…
/secrets add STRIPE_PRICE_PRO_YEAR=price_…
/secrets add STRIPE_PRICE_SIGNATURE_MONTH=price_…
/secrets add STRIPE_PRICE_SIGNATURE_YEAR=price_…
/secrets add STRIPE_PRICE_ENTERPRISE_MONTH=price_…
```
The secrets skill fans these into:
- master `~/Projects/secrets-manager/.env`
- desktop `~/Desktop/site-factory.env`
- this project's `.env` (registered route)
### 2.2 Configure the webhook endpoint in Stripe Dashboard
URL: `https://nationalpaperhangers.com/webhooks/stripe`
Subscribe to events:
- `checkout.session.completed`
- `customer.subscription.created`
- `customer.subscription.updated`
- `customer.subscription.deleted`
- `payment_intent.succeeded`
- `payment_intent.payment_failed`
- `payment_intent.canceled`
- `account.updated` (Connect status mirror)
### 2.3 Connect platform setup
In Stripe Dashboard → Connect → Settings:
- Platform name: `National Paper Hangers`
- Branding: brand mark / palette
- Onboarding type: **Express**
- Allowed countries: US (extend later)
- Capabilities: `card_payments`, `transfers`
### 2.4 Subscription product/prices
In Stripe Dashboard → Products, create:
- **NPH Pro** — $39/mo, $399/yr (`STRIPE_PRICE_PRO_MONTH`, `_YEAR`)
- **NPH Signature** — $149/mo, $1500/yr (`STRIPE_PRICE_SIGNATURE_MONTH`, `_YEAR`)
- **NPH Enterprise** — $399/mo (`STRIPE_PRICE_ENTERPRISE_MONTH`)
Match `lib/stripe.js#PRICE_TABLE`.
### 2.5 Flip NODE_ENV
`.env`: `NODE_ENV=production`. `npm run go-live-check` should now exit 0.
---
## 3. DNS swap plan — **PENDING STEVE APPROVAL, do NOT execute**
Current: `nationalpaperhangers.com` → GoDaddy parking (AdSense-for-Domains).
Target: Cloudflare zone → A-record → Kamatera (`45.61.58.125`) → nginx → pm2 `national-paper-hangers`.
### 3.1 Pre-checks
- ✅ Domain protected-list verified — `nationalpaperhangers.com` NOT in DNS do-not-touch (memory `feedback_dns_donotouch.md`).
- ⚠️ Cloudflare token gotcha — current CF token is **DNS-edit only**. Zone-create needs a fresh token with `Zone:Edit` permission. Memory: `feedback_cf_token_leaked.md` (rotate + scrub if zone-create needed).
### 3.2 Steps (when approved)
```
/domain-setup nationalpaperhangers.com
```
The skill drives: CF zone create → NS swap at GoDaddy → stale-record cleanup → certbot LE cert on Kamatera → CF proxy enable.
### 3.3 Verisign verify
After NS swap, confirm propagation via the source of truth:
```
whois -h whois.verisign-grs.com nationalpaperhangers.com
```
(MCP `check_availability` and GoDaddy FAST give false positives — memory `feedback_verify_domain_availability.md`.)
---
## 4. Email (`info@nationalpaperhangers.com`)
Steve's standing rule: every site gets `info@<domain>` provisioned at build
time and shown in footer + JSON-LD (memory `feedback_every_site_info_email.md`).
### 4.1 MX provider
Pick one:
- **Purelymail** (preferred, cheap) — use the `purelymail` MCP / skill to add domain + create `info` user
- **Google Workspace** — manual setup at admin.google.com
### 4.2 SPF + DKIM + DMARC
Use the `domain-suite` MCP:
```
mcp__domain-suite__setup_spf domain=nationalpaperhangers.com provider=purelymail
mcp__domain-suite__setup_dkim domain=nationalpaperhangers.com provider=purelymail
mcp__domain-suite__setup_dmarc domain=nationalpaperhangers.com policy=quarantine
```
### 4.3 George registration
Add the new info account to George (Gmail relay), so booking-confirmation
emails go out as `info@nationalpaperhangers.com` not `…@gmail.com`. Update
`.env`: `EMAIL_FROM=info@nationalpaperhangers.com`, `GEORGE_ACCOUNT=info`.
---
## 5. Deploy
`DEPLOY_KAMATERA.md` (already in repo, 777-line runbook) is the source of
truth. Steps A–J. Headline gates:
- `ecosystem.kamatera.config.js` — 400M cap, fork mode, env_production, distinct PM2 dump filename so Mac2's dump isn't disturbed
- `scripts/deploy-kamatera.sh` — DRY-RUN by default; pass `--commit` to live-fire; refuses to touch any nginx config containing a protected-DNS host
Steps A–G can complete and validate via `curl http://45.61.58.125:9765/`
WITHOUT touching DNS — that gives Steve a chance to click around the live
copy before the public NS swap.
---
## 6. Smoke-test commands
After each phase, run:
```bash
# Public surface
for r in / /find /map /healthz /sitemap.xml /installer/atelier-bond-nyc /installer/atelier-bond-nyc/book; do
printf "%-45s " "$r"
curl -s -o /dev/null -w "%{http_code}\n" https://nationalpaperhangers.com$r
done
# Map data
curl -s https://nationalpaperhangers.com/api/installers.geo | python3 -c \
"import json,sys; d=json.load(sys.stdin); print('pinned:', d['count'])"
# Expect ≥420 pinned (current local state).
# Rate limit fires after 60 hits
for i in $(seq 1 70); do
curl -s -o /dev/null -w "%{http_code} " https://nationalpaperhangers.com/api/installers.geo
done | awk '{c=0; for(i=1;i<=NF;i++){if($i==429)c++}; printf "\n429 count: %d\n", c}'
# Expect c >= 1.
# Auth gate
curl -s -o /dev/null -w "/admin → %{http_code}\n" https://nationalpaperhangers.com/admin
# Expect 302 to /login.
# Sitemap
curl -s https://nationalpaperhangers.com/sitemap.xml | grep -c '<url>'
# Expect ≥528 (5 static + 524 installer pages).
# In-repo
cd ~/Projects/NationalPaperHangers
npm run go-live-check # 0 missing required-for-live
npm test # 21/21 passing
```
---
## 7. Post-launch checklist (T+0 → T+72h)
- [ ] Open `/admin` as the first claimed installer, run `/admin/connect/onboard`, complete Stripe Express onboarding, confirm `account.updated` webhook arrives and flips `stripe_account_charges_enabled` to true.
- [ ] Make a real test booking against a paid+active studio. Confirm:
- [ ] `/api/installers/:slug/book` returns `deposit.client_secret`
- [ ] Stripe Elements card field accepts `4242 4242 4242 4242`
- [ ] `payment_intent.succeeded` webhook fires
- [ ] `bookings.deposit_status` flips to `paid`, `status` to `confirmed`, `confirmed_at` set
- [ ] Stripe dashboard shows `application_fee_amount` retained on the platform side, balance transferred to installer's Connect account
- [ ] Repeat for 5 additional installers spread across tier=pro / signature / enterprise.
- [ ] Confirm GA4 events firing in real-time view (`G-1WZ49HYY39`):
- [ ] `booking_started` on /book page load
- [ ] `booking_confirmed` post-payment redirect
- [ ] `claim_completed` on claim flow finish
- [ ] Verify booking-confirmation emails arrive at customer inbox (Gmail spam check) AND installer inbox (Pro+ tier).
- [ ] Hit `/sitemap.xml` from search-console-style crawler; submit to Google Search Console + Bing Webmaster Tools.
- [ ] Set up Stripe Connect dashboard alert: any failed `payment_intent` over $200.
- [ ] Confirm CAN-SPAM `MAILING_ADDRESS` env is set BEFORE any list-send (the pre-flight gate refuses without it).
---
## 8. Rollback plan
Severity tiers — pick the lightest action that works.
### 8.1 App-only regression (UI bug, bad query, etc.)
```bash
ssh root@45.61.58.125
pm2 restart national-paper-hangers --update-env
pm2 logs national-paper-hangers --lines 100
```
If still broken:
```bash
cd /var/www/national-paper-hangers
git log --oneline -5
git checkout <last-known-good-sha>
pm2 restart national-paper-hangers --update-env
```
### 8.2 Database migration breakage
Migrations are forward-only. If an ALTER breaks reads, write a 011_revert
SQL that reverts the offending change. **Do not** drop columns that have
data in them — use `RENAME` to a `_dead_` suffix.
### 8.3 nginx routing breakage
```bash
cd /etc/nginx/sites-available
ls -la nationalpaperhangers.com.conf* # snapshots are timestamped via deploy script
sudo cp nationalpaperhangers.com.conf.<timestamp>.bak nationalpaperhangers.com.conf
sudo nginx -t && sudo systemctl reload nginx
```
### 8.4 DNS rollback
Cloudflare dashboard → Zone → DNS → revert A record OR pause CF proxy
(orange cloud → grey cloud) so traffic bypasses CF and hits Kamatera direct.
Last resort: NS swap back at GoDaddy (TTL ~24h to fully propagate).
### 8.5 Stripe webhook disable
Stripe Dashboard → Developers → Webhooks → toggle endpoint to disabled.
The app fails-closed (503) on unsigned events when `STRIPE_DEV_ACCEPT_UNSIGNED`
is unset, which is the desired posture in prod.
---
## 9. Status (as of 2026-05-06)
- Mac2 :9765 — full marketplace MVP, 21/21 tests, mock-mode-safe.
- 524 installers seeded; 420 geocoded + map-pinned.
- 85 PD/CC0 images rolled across cards, map popups, installer hero.
- Stripe Connect deposit flow + webhooks + admin dashboard wired.
- Public DNS still parked on GoDaddy. **Awaiting Steve's go to swap.**
When you're ready: `/domain-setup nationalpaperhangers.com` is the single
command that starts the cutover. Everything else is ready.