Skip to main content
Documentation

Self-Hosting Guide

HPI can be self-hosted on your own infrastructure. It requires two backing services: an S3-compatible object store and a database (SQLite or PostgreSQL). Optionally, you can add NATS JetStream for external integrations (event streaming, cancel requests, NATS notification sinks).

Prerequisites

  • Node.js 20+ or Deno 2+
  • Docker (for backing services, or use managed equivalents)

Infrastructure

S3-Compatible Object Storage

Any S3-compatible service works: AWS S3, MinIO, RustFS, Cloudflare R2, etc. The app auto-creates the bucket on first file upload.

docker run -d \
  --name hpi-s3 \
  -p 9000:9000 \
  -p 9001:9001 \
  -v hpi-s3-data:/data \
  -e RUSTFS_ACCESS_KEY=minioadmin \
  -e RUSTFS_SECRET_KEY=minioadmin \
  --restart unless-stopped \
  rustfs/rustfs:latest

Database

SQLite (default) requires no additional setup — the database file is created automatically. For production deployments with multiple app instances, use PostgreSQL.

Environment Variables

Required

VariableDescription
BETTER_AUTH_SECRETSecret key for session signing. Generate a strong random string.

Application

VariableDefaultDescription
MANAGED_CLOUDOnly set to true on the official cloud platform. Self-hosted deployments should leave this unset.
PUBLIC_BASE_URLhttp://localhost:5173Public URL where the app is accessible. Used for task links and emails.
ORIGINFalls back to PUBLIC_BASE_URLOrigin URL for auth redirects and CORS

Database

VariableDefaultDescription
DB_BACKENDsqlitesqlite or postgres
AUTH_DB_PATH./data/auth.dbPath to SQLite file (SQLite only)
DATABASE_URLPostgreSQL connection string (PostgreSQL only, required)

Object Storage

VariableDefaultDescription
S3_ENDPOINThttp://localhost:9000S3-compatible endpoint URL
S3_BUCKEThuman-tasksBucket name
S3_ACCESS_KEYminioadminAccess key
S3_SECRET_KEYminioadminSecret key
S3_REGIONus-east-1AWS region (ignored by most S3-compatible services)
S3_PUBLIC_URLPublic URL for direct file access. If unset, files are served through the app.

NATS (Optional)

NATS JetStream enables external integrations: event streaming for external subscribers, inbound cancel requests, and NATS notification sinks. The app runs fully standalone without it.

VariableDefaultDescription
NATS_URLNATS connection URL (e.g. nats://localhost:4222). Leave unset to run without NATS.

To enable NATS, start a NATS server with JetStream enabled:

docker run -d \
  --name hpi-nats \
  -p 4222:4222 \
  -p 8222:8222 \
  --restart unless-stopped \
  nats:2.10-alpine -js

The app auto-creates six JetStream streams on startup (HUMAN_REQUESTS, HUMAN_COMPLETED, HUMAN_CANCEL, HUMAN_CANCELLED, HUMAN_FAILED, HUMAN_PROCESS) with 7-day retention.

Admin User

VariableDefaultDescription
ADMIN_EMAILSeed an admin user on first startup
ADMIN_PASSWORDPassword for the admin user
ADMIN_NAMEAdministratorDisplay name

Email (Optional)

Email is required for password resets, email verification, and invitation notifications. If not configured, these actions log to the console instead.

VariableDefaultDescription
EMAIL_PROVIDERsmtpsmtp or brevo
SMTP_HOSTSMTP server hostname
SMTP_PORT587SMTP port
SMTP_SECUREfalseSet to true for TLS
SMTP_USERSMTP username
SMTP_PASSSMTP password
SMTP_FROMHPI <noreply@example.com>Sender address
BREVO_API_KEYBrevo API key (when using Brevo provider)

Quick Start

1. Start backing services

docker run -d --name hpi-s3 -p 9000:9000 -v hpi-s3-data:/data \
  -e RUSTFS_ACCESS_KEY=minioadmin -e RUSTFS_SECRET_KEY=minioadmin \
  --restart unless-stopped rustfs/rustfs:latest

2. Configure environment

Create a .env file in the project root:

BETTER_AUTH_SECRET=change-me-to-a-random-string
PUBLIC_BASE_URL=http://localhost:3000

S3_ENDPOINT=http://localhost:9000
S3_ACCESS_KEY=minioadmin
S3_SECRET_KEY=minioadmin

ADMIN_EMAIL=admin@example.com
ADMIN_PASSWORD=change-me

3. Build and run

npm install
npm run build
node build/index.js

Or with Deno:

deno install
deno task build
deno run -A build/index.js

The app will start, run database migrations, seed the admin user, and connect to S3. If NATS_URL is set, it will also connect to NATS.

Docker Compose

A complete stack for production-like deployments:

services:
  # Optional: uncomment to enable NATS for external integrations
  # nats:
  #   image: nats:2.10-alpine
  #   command: ["-js", "-m", "8222"]
  #   ports:
  #     - "4222:4222"
  #   volumes:
  #     - nats_data:/data
  #   restart: unless-stopped

  s3:
    image: rustfs/rustfs:latest
    environment:
      RUSTFS_ACCESS_KEY: ${S3_ACCESS_KEY:-minioadmin}
      RUSTFS_SECRET_KEY: ${S3_SECRET_KEY:-minioadmin}
    ports:
      - '9000:9000'
    volumes:
      - s3_data:/data
    restart: unless-stopped

  postgres:
    image: postgres:16-alpine
    environment:
      POSTGRES_USER: human
      POSTGRES_PASSWORD: ${DB_PASSWORD:-changeme}
      POSTGRES_DB: hpi
    volumes:
      - db_data:/var/lib/postgresql/data
    restart: unless-stopped

  app:
    build: .
    environment:
      BETTER_AUTH_SECRET: ${BETTER_AUTH_SECRET}
      DB_BACKEND: postgres
      DATABASE_URL: postgresql://human:${DB_PASSWORD:-changeme}@postgres:5432/hpi
      S3_ENDPOINT: http://s3:9000
      S3_ACCESS_KEY: ${S3_ACCESS_KEY:-minioadmin}
      S3_SECRET_KEY: ${S3_SECRET_KEY:-minioadmin}
      # NATS_URL: nats://nats:4222  # Uncomment to enable NATS
      PUBLIC_BASE_URL: ${PUBLIC_BASE_URL:-http://localhost:3000}
      ADMIN_EMAIL: ${ADMIN_EMAIL}
      ADMIN_PASSWORD: ${ADMIN_PASSWORD}
    ports:
      - '3000:8000'
    depends_on:
      # - nats  # Uncomment if using NATS
      - s3
      - postgres
    restart: unless-stopped

volumes:
  # nats_data:  # Uncomment if using NATS
  s3_data:
  db_data:

Production Considerations

Database — Use PostgreSQL for multi-instance deployments. SQLite works well for single-instance setups but does not support concurrent writes from multiple processes.

NATS — NATS is optional. Enable it by setting NATS_URL when you need external event streaming, inbound cancel requests via NATS, or NATS notification sinks. All subjects use the human.* prefix (e.g. human.request.>, human.completed.>).

File storage — Set S3_PUBLIC_URL to serve uploaded files directly from your CDN or S3 endpoint rather than proxying through the app.

Email — Configure an email provider for password resets and invitations. Without email, these features fall back to console logging.

HTTPS — Place the app behind a reverse proxy (nginx, Caddy, Traefik) that handles TLS termination. Set PUBLIC_BASE_URL to your https:// domain.

Scaling — The app is stateless. Run multiple instances behind a load balancer. All coordination happens through the shared database.