Skip to content

Installation & configuration

The easiest way to run PulseDeck is Docker — one command, nothing to configure. That’s all most self-hosters need, and it’s covered first.

The sections after it — manual setup, production, and the environment reference — are optional. You only need them if you’re running without Docker or deploying to production. PulseDeck itself only requires PostgreSQL (Redis is optional, for multi-replica realtime), and the API applies its own migrations on startup, so there’s never a separate migration step.

You need Docker, plus make and openssl (preinstalled on macOS/Linux).

Terminal window
git clone https://github.com/me-shaon/pulsedeck.git pulsedeck
cd pulsedeck
make setup # writes .env and generates strong secrets (idempotent)
docker compose up # or `make up` to build + run detached

This starts the web app, the API, and PostgreSQL together. Open http://localhost:3000, create your account and workspace, and you’re running — then head to the Quickstart to connect an agent.

Prefer not to use Docker, or working on PulseDeck’s own code? Run it directly.

  • Node.js 22+
  • pnpm 9.15+corepack enable && corepack prepare pnpm@9.15.0 --activate
  • PostgreSQL 16+ — running and reachable
Terminal window
git clone https://github.com/me-shaon/pulsedeck.git pulsedeck
cd pulsedeck
pnpm install --frozen-lockfile
Terminal window
psql -U postgres -c "CREATE USER pulsedeck WITH PASSWORD 'pulsedeck';"
psql -U postgres -c "CREATE DATABASE pulsedeck OWNER pulsedeck;"
# Postgres 15+ locks down the public schema — grant it explicitly:
psql -U postgres -d pulsedeck -c "GRANT ALL ON SCHEMA public TO pulsedeck;"

The API reads apps/api/.env (real environment variables always win). The two required values:

Terminal window
cat > apps/api/.env <<'EOF'
DATABASE_URL=postgres://pulsedeck:pulsedeck@localhost:5432/pulsedeck
AUTH_SECRET=replace-with-32+-random-chars # e.g. `openssl rand -base64 48`
EOF
Terminal window
pnpm dev

This starts the API on :3001 and the web dev server on :3000 (Vite proxies /api → the API, keeping cookies first-party). Migrations run on API start. Open http://localhost:3000.

Terminal window
pnpm build

This emits the API bundle to apps/api/dist and the static web bundle to apps/web/dist.

It applies pending migrations, then listens:

Terminal window
cd apps/api
NODE_ENV=production \
DATABASE_URL=postgres://pulsedeck:pulsedeck@localhost:5432/pulsedeck \
AUTH_SECRET=your-32+-char-secret \
BETTER_AUTH_URL=https://your.domain \
PORT=3001 \
node dist/index.js

Keep it alive with a process manager (systemd, pm2, …).

apps/web/dist is static files. The SPA calls /api on its own origin, so put a reverse proxy in front that serves the static files and forwards /api to the API.

server {
listen 80;
server_name your.domain;
root /path/to/pulsedeck/apps/web/dist;
index index.html;
location /api/ {
proxy_pass http://127.0.0.1:3001; # keeps the /api prefix
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
# Server-Sent Events (live updates): flush immediately, don't time out.
proxy_set_header Connection '';
proxy_buffering off;
proxy_read_timeout 1h;
}
location / {
try_files $uri $uri/ /index.html; # SPA client-side routing
}
}

Caddy alternative:

your.domain {
root * /path/to/pulsedeck/apps/web/dist
handle /api/* {
reverse_proxy 127.0.0.1:3001
}
handle {
try_files {path} /index.html
file_server
}
}

Put TLS in front (Caddy does this automatically; for nginx use certbot) and set BETTER_AUTH_URL=https://your.domain to match.

PulseDeck fails fast on weak secrets so a known value can never reach production.

  • POSTGRES_PASSWORD — the one place the database secret lives. The Docker stack builds the API’s DATABASE_URL from it (host postgres), so you set it once. With NODE_ENV=production the API refuses to boot on a default like pulsedeck, postgres, or password.
  • AUTH_SECRET — signs auth sessions; min 32 chars. With NODE_ENV=production the API rejects placeholder values (change-me, insecure, dev-only, …). Rotating it invalidates existing sessions (users re-login); no data is lost.

make setup generates both for you. For a non-Docker or managed setup, generate them by hand — openssl rand -base64 48 for AUTH_SECRET, openssl rand -hex 24 for POSTGRES_PASSWORD.

The bundled Postgres no longer publishes a host port in the production compose file (it’s reachable only on the internal Docker network). The dev overlay (make dev) re-exposes it on 127.0.0.1:${POSTGRES_PORT} for local tooling.

All options below are read by the API. Only POSTGRES_PASSWORD (Docker) or DATABASE_URL (non-Docker) and AUTH_SECRET are required; everything else has a sensible default.

VariablePurposeDefault
POSTGRES_PASSWORDPassword for the bundled Postgres; the Docker stack builds DATABASE_URL from it. Production rejects defaults.(required for Docker)
DATABASE_URLPostgreSQL connection string. Migrations run automatically on startup. Commented out for Docker (built from POSTGRES_PASSWORD).built from POSTGRES_PASSWORD
AUTH_SECRETSecret for signing auth sessions. Min 32 chars; openssl rand -base64 48. Production rejects placeholders.(required)
NODE_ENVdevelopment, test, or production. Production enables the weak-secret guards above.development
BETTER_AUTH_URLPublic origin users hit (for OAuth callbacks + request validation). No trailing slash.inferred from request
TRUST_PROXYReverse-proxy trust: true/false, a hop count (1), or an IP/CIDR. Set to 1 behind the bundled nginx so rate limits key off the real client IP.false
DEPLOYMENT_MODEself-host or cloud. Drives the defaults for signup, billing, accounts, and retention.self-host
SIGNUP_MODEsetup (first-run wizard only), open (public sign-up), or invite (invite-only).setup (self-host)
BILLING_ENABLEDWhether billing routes/UI render.false (self-host)
RETENTION_DAYSDays to keep reports. 0 = keep forever. >0 enables periodic deletion.0
RETENTION_SWEEP_INTERVAL_MSHow often the retention sweep runs (ms, min 1000). Only matters when RETENTION_DAYS > 0.3600000 (1h)
INGEST_RATE_LIMITMax ingestion requests per window, per source (API key).120
INGEST_RATE_WINDOWRate-limit window, in ms or an ms-style string (e.g. "1 minute").60000
AUTH_RATE_LIMITMax attempts per window on sensitive auth endpoints (sign-in/up, password reset), per client IP. Raise it if many users share one NAT’d IP.20
AUTH_RATE_WINDOWAuth rate-limit window, in ms or an ms-style string.60000
REDIS_URLOptional. Backs multi-replica SSE fan-out and the auth/ingest rate-limit store. Omit for single-instance (both work in-memory per node).(unset)
GITHUB_CLIENT_IDOptional GitHub OAuth client id. Both GitHub vars must be set together.(unset)
GITHUB_CLIENT_SECRETOptional GitHub OAuth client secret.(unset)
EMAIL_PROVIDERSet to smtp to deliver password-reset + invite emails. Unset = nothing sent; invite URLs returned in the API response. See Accounts & password reset.(unset)
EMAIL_FROMFrom-address for sent email, e.g. "PulseDeck <no-reply@yourdomain.com>".(unset)
SMTP_HOSTSMTP server host. The trigger that activates delivery (with EMAIL_PROVIDER=smtp).(unset)
SMTP_PORTSMTP port. 587 for STARTTLS; 465 with SMTP_SECURE=true.587
SMTP_USER / SMTP_PASSOptional SMTP auth credentials.(unset)
SMTP_SECUREtrue for implicit TLS (port 465); false/unset → STARTTLS.false
WEBHOOK_ALLOW_PRIVATE_IPSAllow webhook URLs that resolve to loopback/private ranges (reach internal Slack/services). SSRF defense when off.true (self-host)
WEBHOOK_MAX_ATTEMPTSPer-delivery retry ceiling before a webhook gives up.5
BOOTSTRAP_EMAILOptional. Seeds the first admin headlessly (skips the /setup wizard) — for IaC.(unset)
BOOTSTRAP_PASSWORDPassword for the bootstrapped admin. Idempotent (no-op if a user exists).(unset)
PORTPort the API listens on.3001
WEB_PORTDocker Compose host port for the web app.3000
API_PORTDocker Compose host port for the API.3001
POSTGRES_PORTDocker Compose host port for Postgres.5432