← back to NationalPaperHangers
scripts/go-live-check.js
96 lines
#!/usr/bin/env node
// Pre-flight env-var checklist for going live.
// Reads each env var the app expects, prints PRESENT/ABSENT, and (when
// NODE_ENV=production) exits non-zero if any required-for-live var is absent.
require('dotenv').config({ path: require('path').join(__dirname, '..', '.env') });
// requirement: 'live' | 'optional' | 'always'
// live — must be present when NODE_ENV=production
// always — must be present in every env (dev + prod)
// optional — feature flag; absence just disables a thing
const VARS = [
// Core
{ name: 'NODE_ENV', req: 'always', desc: 'Environment marker. Set to "production" for live.' },
{ name: 'PORT', req: 'always', desc: 'HTTP port. Defaults to 9765.' },
{ name: 'PUBLIC_URL', req: 'live', desc: 'Canonical public URL. Used for emails, OG, callback URLs.' },
// Database
{ name: 'PGHOST', req: 'always', desc: 'Postgres host.' },
{ name: 'PGPORT', req: 'optional', desc: 'Postgres port (default 5432).' },
{ name: 'PGDATABASE', req: 'always', desc: 'Postgres DB name (national_paper_hangers).' },
{ name: 'PGUSER', req: 'always', desc: 'Postgres user.' },
{ name: 'PGPASSWORD', req: 'optional', desc: 'Postgres password (omit for unix-socket trust).' },
// Sessions / signing
{ name: 'SESSION_SECRET', req: 'live', desc: 'Cookie session signing secret.' },
{ name: 'BOOKING_SIGNING_SECRET', req: 'live', desc: 'HMAC for /bookings/:uuid?t=… view tokens.' },
{ name: 'UNSUBSCRIBE_SIGNING_SECRET', req: 'live', desc: 'HMAC for one-click unsubscribe links.' },
// Stripe — subscriptions + booking deposits + Connect
{ name: 'STRIPE_SECRET_KEY', req: 'live', desc: 'sk_live_… (or sk_test_… in staging). Drives all Stripe SDK calls.' },
{ name: 'STRIPE_PUBLISHABLE_KEY', req: 'live', desc: 'pk_live_… exposed to book.ejs for Stripe Elements.' },
{ name: 'STRIPE_WEBHOOK_SECRET', req: 'live', desc: 'whsec_… for /webhooks/stripe signature verify.' },
{ name: 'STRIPE_DEV_ACCEPT_UNSIGNED', req: 'optional', desc: 'Dev escape hatch — only honored when NODE_ENV != production.' },
{ name: 'STRIPE_PRICE_PRO_MONTH', req: 'live', desc: 'Stripe price ID for Pro monthly subscription.' },
{ name: 'STRIPE_PRICE_PRO_YEAR', req: 'live', desc: 'Stripe price ID for Pro annual subscription.' },
{ name: 'STRIPE_PRICE_SIGNATURE_MONTH', req: 'live', desc: 'Stripe price ID for Signature monthly.' },
{ name: 'STRIPE_PRICE_SIGNATURE_YEAR', req: 'live', desc: 'Stripe price ID for Signature annual.' },
{ name: 'STRIPE_PRICE_ENTERPRISE_MONTH', req: 'live',desc: 'Stripe price ID for Enterprise monthly.' },
// Marketplace tunables
{ name: 'NPH_DEFAULT_DEPOSIT_CENTS', req: 'optional', desc: 'Default booking deposit in cents (default 9900 = $99).' },
{ name: 'NPH_PLATFORM_FEE_BPS', req: 'optional', desc: 'Platform fee in basis points (default 1000 = 10%).' },
{ name: 'NPH_PLATFORM_ADMIN_INSTALLER_IDS', req: 'optional', desc: 'Comma-separated installer IDs that see platform-wide totals on /admin/billing.' },
// George (outbound email)
{ name: 'GEORGE_URL', req: 'live', desc: 'George Gmail relay URL (http://localhost:9850 in dev, http://100.107.67.67:9850 from Kamatera tailnet).' },
{ name: 'GEORGE_USER', req: 'live', desc: 'George basic-auth user.' },
{ name: 'GEORGE_PASS', req: 'live', desc: 'George basic-auth password.' },
{ name: 'GEORGE_ACCOUNT', req: 'optional', desc: 'Account label for George multi-account support (e.g. info).' },
{ name: 'EMAIL_FROM', req: 'live', desc: 'From address for outbound mail (e.g. info@nationalpaperhangers.com).' },
{ name: 'EMAIL_FROM_NAME', req: 'optional', desc: 'From display name (e.g. National Paper Hangers).' },
{ name: 'MAILING_ADDRESS', req: 'live', desc: 'CAN-SPAM physical mailing address. Pre-flight gate REFUSES sends without this.' },
// Optional integrations
{ name: 'BROWSERBASE_API_KEY', req: 'optional', desc: 'Cloud browser for vendor scrapes.' },
{ name: 'BROWSERBASE_PROJECT_ID', req: 'optional', desc: 'Browserbase project ID.' }
];
const isProd = process.env.NODE_ENV === 'production';
const rows = VARS.map(v => ({
...v,
present: typeof process.env[v.name] === 'string' && process.env[v.name].length > 0
}));
function pad(s, n) { return (s + ' '.repeat(n)).slice(0, n); }
const NAME_W = Math.max(...rows.map(r => r.name.length)) + 2;
console.log('');
console.log(`NPH go-live env-var checklist (NODE_ENV=${process.env.NODE_ENV || '<unset>'})`);
console.log('='.repeat(72));
let missingLive = 0, missingAlways = 0;
for (const r of rows) {
const tag = r.present ? '✓ PRESENT' : '✗ ABSENT ';
const reqTag = r.req === 'always' ? '[always]' : r.req === 'live' ? '[live] ' : '[opt] ';
console.log(` ${tag} ${reqTag} ${pad(r.name, NAME_W)} ${r.desc}`);
if (!r.present) {
if (r.req === 'always') missingAlways += 1;
if (r.req === 'live') missingLive += 1;
}
}
console.log('');
console.log(`Required-always missing: ${missingAlways}`);
console.log(`Required-for-live missing: ${missingLive}`);
if (missingAlways > 0) {
console.error('\n✗ FAIL — required-always env vars are missing. Fix .env before any boot.');
process.exit(2);
}
if (isProd && missingLive > 0) {
console.error('\n✗ FAIL — running in production but required-for-live env vars are absent. Save them via the secrets-manager skill.');
process.exit(1);
}
console.log('\n✓ OK for current NODE_ENV. (live keys absent is fine in dev.)');
process.exit(0);