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.
Run with Docker (recommended)
Section titled “Run with Docker (recommended)”All you need installed is Docker.
git clone https://github.com/me-shaon/pulsedeck.git pulsedeckcd pulsedeckdocker compose upThis 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.
Manual setup (without Docker)
Section titled “Manual setup (without Docker)”Prefer not to use Docker, or working on PulseDeck’s own code? Run it directly.
Prerequisites
Section titled “Prerequisites”- Node.js 22+
- pnpm 9.15+ —
corepack enable && corepack prepare pnpm@9.15.0 --activate - PostgreSQL 16+ — running and reachable
1. Install dependencies
Section titled “1. Install dependencies”git clone https://github.com/me-shaon/pulsedeck.git pulsedeckcd pulsedeckpnpm install --frozen-lockfile2. Create the database
Section titled “2. Create the database”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;"3. Configure the API
Section titled “3. Configure the API”The API reads apps/api/.env (real environment variables always win). The two
required values:
cat > apps/api/.env <<'EOF'DATABASE_URL=postgres://pulsedeck:pulsedeck@localhost:5432/pulsedeckAUTH_SECRET=replace-with-32+-random-chars # e.g. `openssl rand -base64 48`EOF4. Run it
Section titled “4. Run it”pnpm devThis 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.
Production
Section titled “Production”pnpm buildThis emits the API bundle to apps/api/dist and the static web bundle to
apps/web/dist.
Start the API
Section titled “Start the API”It applies pending migrations, then listens:
cd apps/apiNODE_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.jsKeep it alive with a process manager (systemd, pm2, …).
Serve the web bundle
Section titled “Serve the web bundle”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.
Environment reference
Section titled “Environment reference”All options below are read by the API. Only DATABASE_URL and AUTH_SECRET
are required; everything else has a sensible default.
| Variable | Purpose | Default |
|---|---|---|
DATABASE_URL | PostgreSQL connection string. Migrations run automatically on startup. | (required) |
AUTH_SECRET | Secret for signing auth sessions. Min 32 chars; openssl rand -base64 48. | (required) |
BETTER_AUTH_URL | Public origin users hit (for OAuth callbacks + request validation). No trailing slash. | inferred from request |
DEPLOYMENT_MODE | self-host or cloud. Drives the defaults for signup, billing, accounts, and retention. | self-host |
SIGNUP_MODE | setup (first-run wizard only), open (public sign-up), or invite (invite-only). | setup (self-host) |
BILLING_ENABLED | Whether billing routes/UI render. | false (self-host) |
RETENTION_DAYS | Days to keep reports. 0 = keep forever. >0 enables periodic deletion. | 0 |
RETENTION_SWEEP_INTERVAL_MS | How often the retention sweep runs (ms, min 1000). Only matters when RETENTION_DAYS > 0. | 3600000 (1h) |
INGEST_RATE_LIMIT | Max ingestion requests per window, per source (API key). | 120 |
INGEST_RATE_WINDOW | Rate-limit window, in ms or an ms-style string (e.g. "1 minute"). | 60000 |
REDIS_URL | Optional. Enables multi-replica SSE pub/sub fan-out. Omit for single-instance. | (unset) |
GITHUB_CLIENT_ID | Optional GitHub OAuth client id. Both GitHub vars must be set together. | (unset) |
GITHUB_CLIENT_SECRET | Optional GitHub OAuth client secret. | (unset) |
EMAIL_PROVIDER | Optional transactional-email provider for invites. Unset = invite URLs returned in the API response. | (unset) |
EMAIL_FROM | From-address used by the configured email provider. | (unset) |
BOOTSTRAP_EMAIL | Optional. Seeds the first admin headlessly (skips the /setup wizard) — for IaC. | (unset) |
BOOTSTRAP_PASSWORD | Password for the bootstrapped admin. Idempotent (no-op if a user exists). | (unset) |
PORT | Port the API listens on. | 3001 |
WEB_PORT | Docker Compose host port for the web app. | 3000 |
API_PORT | Docker Compose host port for the API. | 3001 |
POSTGRES_PORT | Docker Compose host port for Postgres. | 5432 |