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.

All you need installed is Docker.

Terminal window
git clone https://github.com/me-shaon/pulsedeck.git pulsedeck
cd pulsedeck
docker compose up

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.

All options below are read by the API. Only DATABASE_URL and AUTH_SECRET are required; everything else has a sensible default.

VariablePurposeDefault
DATABASE_URLPostgreSQL connection string. Migrations run automatically on startup.(required)
AUTH_SECRETSecret for signing auth sessions. Min 32 chars; openssl rand -base64 48.(required)
BETTER_AUTH_URLPublic origin users hit (for OAuth callbacks + request validation). No trailing slash.inferred from request
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
REDIS_URLOptional. Enables multi-replica SSE pub/sub fan-out. Omit for single-instance.(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_PROVIDEROptional transactional-email provider for invites. Unset = invite URLs returned in the API response.(unset)
EMAIL_FROMFrom-address used by the configured email provider.(unset)
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