Files
property-management-network/scripts/migrate-prod.mjs
T
Leon SerfatyandClaude Opus 4.8 c9968531e4 Consolidate audit-fixes branch: webhooks, integrations, and deploy hardening
Batch commit of the pending working tree on security/audit-fixes-2026-07.
Major areas:
- Outbound webhooks / Zapier: schema + signed delivery with retries, public
  v1 API (REST-hook subscribe/unsubscribe), settings UI, cron drain.
- Deploy hardening: email via SMTP2GO (Resend fully removed), verified DB TLS
  (DATABASE_SSL=require + DATABASE_CA), storage fails loud in production when
  Spaces is unconfigured instead of silently using ephemeral disk.
- Integrations & features (concurrent work): accounting (QuickBooks/Xero),
  e-signature (DocuSign/Dropbox Sign), PayPal, geocoding/maps, onboarding,
  expanded legal pages.
- DB migrations 0006–0009.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 13:42:34 -04:00

69 lines
2.3 KiB
JavaScript

// One-off production migration runner (DigitalOcean Managed Postgres).
//
// Applies any pending Drizzle migrations in lib/db/migrations to the production
// database, using the doadmin connection. Drizzle tracks what's already applied
// in the __drizzle_migrations table, so this only runs the missing migrations
// and is safe to re-run.
//
// The connection string is read from PROD_DATABASE_URL so the credential never
// has to be typed on the command line. Set it in .env.local (git-ignored):
//
// PROD_DATABASE_URL=postgresql://doadmin:<pw>@<cluster>...:25060/<app-db>
//
// Then run: node scripts/migrate-prod.mjs
//
// TLS: verified against ca-certificate.crt when present, otherwise encrypted
// but unverified. Uses the direct port (25060), NOT the 25061 pooler — the
// migrator needs a real session (transactions + advisory locks).
import { readFileSync } from "node:fs"
import { config } from "dotenv"
import { drizzle } from "drizzle-orm/node-postgres"
import { migrate } from "drizzle-orm/node-postgres/migrator"
import pg from "pg"
config({ path: ".env.local", quiet: true })
const url = process.env.PROD_DATABASE_URL
if (!url) {
console.error(
"[migrate-prod] PROD_DATABASE_URL is not set. Add the doadmin connection " +
"string (port 25060, app database) to .env.local and re-run."
)
process.exit(1)
}
if (url.includes(":25061")) {
console.error(
"[migrate-prod] Refusing to run migrations through the 25061 connection " +
"pooler. Use the direct port 25060."
)
process.exit(1)
}
let ssl
try {
const ca = readFileSync("ca-certificate.crt", "utf8")
ssl = { rejectUnauthorized: true, ca }
console.log("[migrate-prod] TLS: verifying against ca-certificate.crt")
} catch {
ssl = { rejectUnauthorized: false }
console.log("[migrate-prod] TLS: encrypted (certificate not verified)")
}
const { Pool } = pg
const pool = new Pool({ connectionString: url, ssl })
const db = drizzle(pool)
try {
const host = new URL(url).host
console.log(`[migrate-prod] Connecting to ${host} ...`)
await migrate(db, { migrationsFolder: "./lib/db/migrations" })
console.log("[migrate-prod] Migrations applied successfully.")
await pool.end()
process.exit(0)
} catch (err) {
console.error(`[migrate-prod] Failed: ${err?.message ?? err}`)
await pool.end().catch(() => {})
process.exit(1)
}