Deployment
Pulse is one long-running Node process and one database. This page covers the bundled TimescaleDB setup and the things any host needs to get right: a persistent archive, an always-running process, and access control.
What a deployment needs#
- Node 24 or newer, or a container image that has it.
- A process that stays up. The collector, the firehose and the newsletter schedule live inside
pulse start. If the process sleeps, collection stops. - A persistent archive. The default PGlite directory is a folder on local disk. Put it on a persistent volume, or set
DATABASE_URLto a Postgres server. - Outbound network to the data providers, to your RPC endpoints and, for the newsletter, to Telegram.
- Inbound access control if the API or dashboard is reachable by anyone but you.
TimescaleDB with Docker Compose#
The repository ships a docker-compose.yml with one service, the database. Pulse itself runs on the host.
services:
db:
image: timescale/timescaledb:latest-pg17
environment:
POSTGRES_USER: pulse
POSTGRES_PASSWORD: pulse
POSTGRES_DB: pulse
ports:
- "5544:5432"
volumes:
- pulse-db:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U pulse"]
interval: 5s
retries: 10
volumes:
pulse-db:
npm run db:up
echo 'DATABASE_URL=postgres://pulse:pulse@localhost:5544/pulse' >> .env
npm start
On first start Pulse applies the migration and converts the time-series tables to compressed hypertables, see Data model.
Change the password for anything shared. The compose file uses pulse as user, password and database, and publishes the port. That is fine on your own machine. On a server, set a real password, do not publish port 5432 to the internet, and update DATABASE_URL to match.
Run as a service#
A systemd unit for a source checkout or an install from npm. Adjust the paths and user.
[Unit]
Description=Pulse market intelligence
After=network-online.target
[Service]
User=pulse
WorkingDirectory=/opt/pulse
EnvironmentFile=/opt/pulse/.env
ExecStart=/usr/bin/node src/main.ts
Restart=always
RestartSec=5
[Install]
WantedBy=multi-user.target
sudo systemctl enable --now pulse
journalctl -u pulse -f
Restart=always matters: the firehose reconnects by itself, but a crashed process needs the supervisor.
Container#
The repository does not include a Dockerfile. This is an example that installs the published package. Build it yourself and test it before relying on it.
FROM node:24-slim
RUN npm install -g pulse-trenches
ENV PORT=8787
EXPOSE 8787
CMD ["pulse", "start"]
docker build -t pulse .
docker run -d --name pulse -p 8787:8787 --env-file .env \
-v pulse-data:/data -e PGLITE_DIR=/data/pglite pulse
With DATABASE_URL set to an external Postgres, the volume and PGLITE_DIR are not needed.
Cloud Run and other serverless hosts#
Pulse is a stateful worker, so scale-to-zero platforms need care. On Google Cloud Run:
- Use an external Postgres (Cloud SQL, or any reachable server) via
DATABASE_URL. Cloud Run's local disk is ephemeral, so the PGlite directory would be lost. - Set minimum instances to 1 and CPU always allocated. Otherwise the instance is throttled between requests and the collection loop, WebSocket stream and newsletter schedule stall.
- Keep it to one instance (maximum instances 1). Two instances would both collect and both send the newsletter.
- Cloud Run sets
PORTitself, and Pulse honours it.
gcloud run deploy pulse --image IMAGE \
--min-instances 1 --max-instances 1 --no-cpu-throttling \
--set-env-vars DATABASE_URL=...,PUBLIC_URL=https://...
A small always-on VM or any container host with a persistent disk is simpler and usually cheaper for a process that never sleeps.
Exposing the dashboard and API#
Pulse has no authentication and serves CORS for all origins. It binds to every network interface on PORT. Choose one:
- Private: keep the host firewalled and reach it over a VPN or SSH tunnel (
ssh -L 8787:localhost:8787 host). - Public with a gate: put it behind a reverse proxy (Caddy, nginx, a cloud load balancer) that adds TLS and HTTP basic auth or an identity-aware proxy. Set
PUBLIC_URLto the proxy's address so newsletter links point to it.
All routes are read-only, but the archive includes labelled wallet data and tracked-trade history you may not want public.
Backups and upgrades#
- Postgres: back up with
pg_dump, or snapshot the volume. Restoring into TimescaleDB follows Timescale's own pre-restore and post-restore procedure. - PGlite: stop Pulse and copy the
PGLITE_DIRfolder. Do not copy it while Pulse is running. - Upgrades: update the code or package and restart. Migrations run on start and each one is recorded in
schema_migrationsso it applies once. To apply them without starting the app, runpulse migrate. - Analysis copies: point your tools at a read-only replica or use
/api/export.
Running only part of it#
| Goal | How |
|---|---|
| Collector and API without the firehose | STREAMS=off or --no-stream. Wallet scores stay empty because no trades are recorded. |
| Firehose and collector without the web server | --no-server |
| Cron-style collection | pulse collect --jobs on a schedule. No firehose, but snapshots, statuses and classification all run. |
| Check health from a monitor | GET /api/health returns lastCycle. Alert when it is older than a few cycles. |