Pulse_Intel

Backup And Restore

Pulse Intelligence stores operational CTI, API key hashes, users, sessions, audit logs, and analyst-authored reports in PostgreSQL. A dump is therefore a sensitive intelligence artifact: treat it like a production credential store, not a log file.

This document is the operator workflow for the db:backup / db:restore scripts. It covers a safe default path — scheduled, rotated, encrypted, offsite — and a restore runbook.

What the scripts do

Requirements

Creating backups

npm run db:backup

Writes backups/pulse-intelligence-<timestamp>.dump. Options:

Flag Meaning Default
--dir <path> backup directory (also where rotation looks) backups
--output <path> explicit output file timestamped name in --dir
--retain <n> keep the newest n timestamped backups, prune the rest 7
--verify smoke-test the dump with pg_restore --list before trusting it off
--encrypt AES-256-CBC encrypt (OpenSSL PBKDF2) to <output>.dump.enc off

Encryption example (passphrase read from the environment so it never appears in ps/argv):

export BACKUP_PASSPHRASE="$(openssl rand -base64 32)"
npm run db:backup -- --encrypt --verify

Only the ciphertext is left on disk; the plaintext dump is deleted after encryption.

Scheduled, rotated workflow

Run daily from cron (or a systemd timer / your container scheduler). The scripts exit non-zero on failure, so a failed run surfaces in cron mail / alerting:

10 2 * * *  cd /opt/pulse-intel && BACKUP_PASSPHRASE_FILE=$(cat /run/secrets/pulse-backup-passphrase) \
             BACKUP_PASSPHRASE="$BACKUP_PASSPHRASE_FILE" npm run db:backup -- --encrypt --verify --retain 7

Then copy the newest *.dump.enc offsite (object storage, another host, an immutable bucket). Keep the passphrase out of the backup storage: a dump with its key is not a backup.

Retention tip: the dump contains everything, so “keep 7 daily” plus a weekly offsite copy covers most self-hosted deployments; scale to your own retention policy.

Restoring

Restores are destructive — pg_restore --clean drops existing objects. The script refuses to run without --yes for exactly that reason.

Runbook

  1. Stop the app and worker so feed jobs and API writes do not race the restore. (docker compose stop if you run the compose stack.)
  2. Confirm DATABASE_URL points at the intended target host/database — one typo here is how you restore a prod dump over staging.
  3. Take a fresh backup of the target if it contains anything you might want.
  4. Restore:
    npm run db:restore -- --input backups/pulse-intelligence-2026-08-11T02-10-00-000Z.dump.enc --yes
    

    .enc inputs are decrypted to a temp file that is deleted as soon as the restore finishes — even on failure.

  5. Verify before resuming traffic:
    • npm run db:generate if this is part of a deployment rebuild (schema matches the client), then apply any migrations that postdate the dump (npm run db:migrate).
    • Restart app and worker.
    • Check /api/health, then spot-check real data: a known indicator, a source’s lastRunAt on the feeds page, and the audit log.
  6. Expect a re-sync, not instant freshness: feeds only write on their next scheduled run, and expiry/prune jobs will trim data older than their windows.

Why the workflow is shaped this way

Security notes

Storage guidance