Short answer: this project is deployment-ready as a directly-hosted Node app
(what’s actually running right now, on this RDP box). For a containerized
deploy, a multi-stage Dockerfile and a docker compose --profile full up -d
path exist — see §0/§3.5/§4.1 below. On a host without Docker, use the Node
path; with Docker, either works.
This file is the “get it live on a brand new machine” checklist. For
day-to-day commands and architecture notes, see HANDOVER.md
and README.md — this file only covers moving/redeploying.
If you received this as a zip, it deliberately excludes:
node_modules/ — reinstall with npm install on the target machine (native
binaries in sharp and Prisma’s query engine are platform-specific; copying
a Windows node_modules to Linux, or vice versa, will not work)..next/ — rebuild with npm run build..env — your real secrets are not in this zip. Copy them across
out-of-band (password manager, not email/chat/a file share you don’t
control) and recreate .env by hand on the new machine using
.env.example as the template. See §2 for the full list of what needs to
be in it.*.tsbuildinfo, src/generated/prisma/ — build caches, regenerated
automatically.Everything else — source code, prisma/migrations/, .claude/ skills,
scripts, git history — is included.
| Requirement | Notes |
|---|---|
| Node.js 20.9+ | This project was built and run on Node 24. |
| PostgreSQL 17 | Native install, or docker compose up -d postgres (the postgres/redis services run standalone without the app). |
| Redis-compatible server | Memurai on Windows, real Redis on Linux/Mac, or docker compose up -d redis. |
npm |
Ships with Node. |
If you’re moving to a real VPS/cloud host (not another Windows RDP box),
everything here works identically on Linux — the Windows-specific notes in
HANDOVER.md §7 (no winget, PATH prepending) won’t apply.
.envCopy .env.example to .env and fill in:
DATABASE_URL= # postgres connection string
REDIS_URL= # redis connection string
VIRUSTOTAL_API_KEY=
ABUSEIPDB_API_KEY=
ABUSEIPDB_API_KEYS= # optional: comma-separated list for key rotation
OTX_API_KEY=
NVD_API_KEY= # optional but strongly recommended, see HANDOVER §4.5
SESSION_SECRET= # any random 32+ char string
SEED_PASSWORD= # only used the first time you run db:seed
Do not reuse the exact keys from a previous deployment if that deployment’s
.env was ever shared over an insecure channel (chat, email). Rotate them —
they’re all free-tier and instant to regenerate (see the links in
HANDOVER.md §1.2).
npm install
npx prisma generate # generates src/generated/prisma
npm run db:migrate # applies prisma/migrations/ — use this,
# NOT `prisma migrate dev` (see HANDOVER §4.1/§4.2)
npm run db:seed # 3 base users (admin/analyst/viewer)
npm run db:seed:demo # optional: real APTs, campaigns, sample IOCs — idempotent
npm run attack:sync -- --all # MITRE ATT&CK (~58MB download, ~1 min)
npm run feeds:install # registers the 18-source feed catalogue
npm run cve:catchup -- --days 90 # backfill CVE data so it isn't stale on day one
Then recreate any accounts you need beyond the seed defaults (e.g. the admin
account this session created for sukeshkumartkd@gmail.com — that only
exists in this deployment’s database; re-run the same
hashPassword/db.user.upsert pattern, or just log in with the seed admin
and use /register + a manual role bump).
npm run build
npm run start # production server, http://localhost:3000
In a second process/terminal, alongside it:
npm run worker # feeds, enrichment, hunts, scheduled reports
Both need to run continuously. On a real host, use a process manager instead of a bare terminal:
npm run start,
one for npm run worker, both Restart=always.pm2 start npm --name pulse-app -- start and
pm2 start npm --name pulse-worker -- run worker, then pm2 save +
pm2 startup so they survive a reboot.Same two processes, same image. The full compose profile builds the app and
worker from the multi-stage Dockerfile and brings up PostgreSQL + Redis
alongside:
docker compose up -d postgres redis # infra only (no app)
docker compose --profile full up -d --build # everything
The image runs prisma migrate deploy on every container start (via
docker-entrypoint.sh), so first boot applies prisma/migrations/ automatically
— no manual npm run db:migrate. Still run the data-loading steps once after the
stack is healthy (docker compose --profile full exec app npm run db:seed, then
attack:sync -- --all, feeds:install, and cve:catchup -- --days 90 as in §3).
Secrets come from the host .env file or the compose environment block —
SESSION_SECRET is required; the enrichment provider keys are optional.
To run a one-off maintenance job inside the running container:
docker compose --profile full exec app npm run cve:catchup -- --days 90
docker compose --profile full exec app npm run verify:enrichment -- --live
Notes:
full profile is the whole stack on one host — for a split deploy, run the
same image twice with different command: values (npm run start vs
npm run worker), which is exactly what the profile does.tsx (a devDependency shipped
in the image), so the container is self-contained for both processes.CREDENTIAL_ENC_KEY is optional in the container — provider keys set through
the Settings UI (ProviderCredential table) are encrypted with it. Without
it, Settings cannot store keys and the env vars below are the only source,
which keeps the OCI image and .env provider-key-only deployments working.Two options, depending on what you’re moving to:
A. You control DNS for a domain (what this session set up: pulseintel.online
via Cloudflare) — use a named Cloudflare Tunnel, not the quick/ephemeral one:
cloudflared tunnel login # once, authorizes your Cloudflare account
cloudflared tunnel create pulse-intel # once
cloudflared tunnel route dns pulse-intel <yourdomain> # once
cloudflared tunnel run --url http://localhost:3000 pulse-intel # every time you bring it up
This survives a cloudflared restart (unlike the quick-tunnel random URL) as
long as you keep reusing the same tunnel name/credentials file
(~/.cloudflared/<tunnel-id>.json — back this up if migrating hosts, or just
re-run tunnel create on the new machine and re-point the DNS route).
B. A real VPS with a public IP — you don’t need cloudflared at all; put
a reverse proxy (nginx/Caddy) in front of port 3000 for TLS, point your
domain’s A record at the VPS IP, and skip straight to a normal HTTPS setup.
This is generally the more “production” path than a tunnel — a tunnel is
convenient when you don’t have a static IP or don’t want to open inbound
ports, which is exactly the RDP-box situation this was built for.
npm run typecheck
npm run test # 143 tests, no DB/network needed
npm run verify:enrichment -- --live # proves API keys + rate limiter work
npm run build
Sign in, change the seed passwords (topbar key icon → Change password — self-service for
any signed-in user; see HANDOVER.md §2.4), and confirm the worker log shows
worker ready — enrichment + feeds + hunts + reports.