buildd
Deployment

Self-Hosting Buildd

Complete guide for self-hosting Buildd on your own infrastructure

Self-Hosting Buildd

Complete guide for self-hosting Buildd on your own infrastructure.

Overview

Buildd consists of:

  • Web server (Next.js) - Dashboard, API, and authentication
  • Database (PostgreSQL) - Neon or any Postgres instance
  • Cron trigger (External) - Calls /api/cron/schedules hourly, on the hour
  • Workers (External) - Run on laptops, VMs, or CI runners

Prerequisites

  • Node.js 18+ or Bun
  • PostgreSQL database (Neon, local, or Docker)
  • Domain name (optional, but recommended)
  • SSL certificate (Let's Encrypt recommended)

Quick Start with Docker Compose

1. Clone and Configure

git clone https://github.com/buildd-ai/buildd.git
cd buildd
cp .env.example .env.local

2. Edit .env.local

.env.example in the repo is not a complete list. Several variables the app reads are absent from it — most importantly ENCRYPTION_KEY, which the entire secrets system derives its key from. Without it the app boots and looks healthy, and then credential delivery silently no-ops. Set the variables below, not just the ones in the example file.

# Database
DATABASE_URL=postgresql://postgres:postgres@db:5432/buildd

# Auth (generate with: openssl rand -base64 32)
AUTH_SECRET=your-secret-here

# Secret encryption — REQUIRED, min 32 characters, and NOT in .env.example.
# Generate with: openssl rand -base64 48
# Store it verbatim. Any transformation (a stray trailing newline, a shell
# escape) changes the scrypt input and makes every existing secret
# undecryptable, with no error at startup.
ENCRYPTION_KEY=your-32-plus-char-key

# Public base URL. Every link the app generates falls back to
# https://buildd.dev when this is unset.
NEXT_PUBLIC_APP_URL=https://your-domain.com

# Google OAuth
GOOGLE_CLIENT_ID=your-client-id
GOOGLE_CLIENT_SECRET=your-client-secret

# Cron (generate with: openssl rand -base64 32)
CRON_SECRET=your-cron-secret

# Pusher (optional - for realtime updates)
PUSHER_APP_ID=
PUSHER_KEY=
PUSHER_SECRET=
PUSHER_CLUSTER=us2
NEXT_PUBLIC_PUSHER_KEY=
NEXT_PUBLIC_PUSHER_CLUSTER=us2

3. Create docker-compose.yml

version: '3.8'

services:
  db:
    image: postgres:16-alpine
    environment:
      POSTGRES_DB: buildd
      POSTGRES_USER: postgres
      POSTGRES_PASSWORD: postgres
    volumes:
      - postgres_data:/var/lib/postgresql/data
    ports:
      - "5432:5432"

  web:
    build: .
    ports:
      - "3000:3000"
    environment:
      - DATABASE_URL=postgresql://postgres:postgres@db:5432/buildd
      - AUTH_SECRET=${AUTH_SECRET}
      - ENCRYPTION_KEY=${ENCRYPTION_KEY}
      - NEXT_PUBLIC_APP_URL=${NEXT_PUBLIC_APP_URL}
      - GOOGLE_CLIENT_ID=${GOOGLE_CLIENT_ID}
      - GOOGLE_CLIENT_SECRET=${GOOGLE_CLIENT_SECRET}
      - CRON_SECRET=${CRON_SECRET}
    depends_on:
      - db
    restart: unless-stopped

volumes:
  postgres_data:

4. Start Services

docker-compose up -d

Access at http://localhost:3000

Database Setup

  1. Sign up at neon.tech
  2. Create database
  3. Copy connection string to DATABASE_URL
  4. Migrations run automatically on deploy

Option 2: Local PostgreSQL

# Install PostgreSQL
sudo apt install postgresql postgresql-contrib

# Create database
sudo -u postgres createdb buildd

# Run migrations
cd packages/core
bun run db:migrate

Alternative Cron Trigger Services

If you don't want to manage cron yourself:

Hourly is deliberate, not a minimum viable setting. The tick only needs to be as fine as the finest schedule you actually run, and a finer tick wakes the database on every call for nothing. If you add a schedule finer than hourly, make the tick at least that fine or it will fire late — see Task Schedules.

Option 1: cron-job.org (Free)

  1. Sign up at cron-job.org
  2. Create new job:
    • URL: https://your-domain.com/api/cron/schedules
    • Interval: Hourly, on the hour (0 * * * *)
    • HTTP Method: GET
    • Headers: Authorization: Bearer YOUR_CRON_SECRET

Option 2: System Crontab

crontab -e

Add entry:

0 * * * * curl -s -H "Authorization: Bearer YOUR_CRON_SECRET" http://localhost:3000/api/cron/schedules

Environment Variables Reference

Required

VariableDescription
DATABASE_URLPostgreSQL connection string. The first DB access throws without it
AUTH_SECRETNextAuth secret (32+ random chars). Read implicitly by NextAuth
ENCRYPTION_KEYMaster key for the secrets system — AES-256-GCM via scrypt. Minimum 32 characters; encrypt()/decrypt() throw below that. Not present in .env.example
GOOGLE_CLIENT_IDGoogle OAuth client ID (required for Google sign-in)
GOOGLE_CLIENT_SECRETGoogle OAuth secret

Effectively required for a real deployment

VariableDescription
NEXT_PUBLIC_APP_URLPublic base URL. Nothing throws without it, but every generated link falls back to VERCEL_URL and then to https://buildd.dev — so share links, callbacks and dashboard URLs point at the wrong host
CRON_SECRETAuthenticates /api/cron/*. The routes return 500 CRON_SECRET not configured without it, so all scheduled work stops

Optional — each gates one feature

VariableWithout it
AUTH_URL / NEXTAUTH_URLNothing breaks. auth.ts sets trustHost: true, so NextAuth infers the host from the request; every other read is a fallback chain ending in http://localhost:3000. NEXTAUTH_URL is checked first where both are read
PUSHER_APP_ID, PUSHER_KEY, PUSHER_SECRET, PUSHER_CLUSTER, NEXT_PUBLIC_PUSHER_KEY, NEXT_PUBLIC_PUSHER_CLUSTERRealtime dashboard updates are disabled; the Pusher client is null unless the set is complete
VOYAGE_API_KEYSemantic embedding and reranking are unavailable — knowledge retrieval degrades to a lexical-only fallback with no error
GITHUB_APP_ID, GITHUB_APP_CLIENT_ID, GITHUB_APP_PRIVATE_KEY (or GITHUB_APP_PRIVATE_KEY_BASE64)The whole GitHub integration is off — isGitHubAppConfigured() is false, so no PR creation, no merges, no repository_dispatch
GITHUB_APP_CLIENT_SECRETGitHub sign-in fails. There is no AUTH_GITHUB_ID/AUTH_GITHUB_SECRET — the GitHub App's OAuth credentials double as the login provider's
GITHUB_APP_WEBHOOK_SECRETFails open. verifyWebhookSignature() logs a warning and returns true, so any unauthenticated caller can forge a GitHub webhook. Set it
GITHUB_APP_SLUGThe install URL defaults to the buildd app slug — wrong for your own app
SLACK_SIGNING_SECRETFails closed — the Slack integration route rejects every request
DISCORD_PUBLIC_KEYFails closed — the Discord integration route rejects every request
ANTHROPIC_API_KEYOptional for the web app; workers supply their own credentials. Only the task classifier throws without it
STORAGE_ENDPOINT, STORAGE_BUCKET, STORAGE_ACCESS_KEY, STORAGE_SECRET_KEY, STORAGE_PUBLIC_URLFile-artifact upload and download are disabled (isStorageConfigured() gates them). STORAGE_REGION defaults to auto
BUILDD_SIGNING_KEY_TEAM_IDJWKS key storage and rotation throw. Only needed if you use signed tokens
ALLOWED_EMAILSSign-in is open to any email that completes OAuth

Two footguns in the private-key handling. GITHUB_APP_PRIVATE_KEY is read with .replace(/\\n/g, '\n'), so it must contain literal backslash-n sequences, not real newlines — GITHUB_APP_PRIVATE_KEY_BASE64 avoids the question and is preferred. And ENCRYPTION_KEY is only length-checked (< 32 throws): a same-length corruption passes the check and then decrypts nothing, which surfaces as "no credential stored" rather than an error.

packages/core/config.ts defines its required() helper as process.env[key] || '' so that Vercel builds don't fail. A missing value becomes an empty string, not an error — do not rely on startup to tell you what you forgot.

Troubleshooting

Migrations won't run

# Check database connection
psql $DATABASE_URL -c "SELECT 1"

# Run migrations manually
cd packages/core
bun run db:migrate

Schedules not triggering

  1. Check CRON_SECRET is set correctly
  2. Verify cron job is running: curl -H "Authorization: Bearer SECRET" URL
  3. Ensure schedule is enabled=true

Security Checklist

  • Use strong AUTH_SECRET, ENCRYPTION_KEY and CRON_SECRET (32+ chars)
  • Set GITHUB_APP_WEBHOOK_SECRET — webhook signature verification is skipped without it
  • Back up ENCRYPTION_KEY outside the deployment; losing it means every stored secret is unrecoverable
  • Enable SSL/TLS with valid certificate
  • Use firewall to restrict database access
  • Keep dependencies updated: bun update
  • Monitor logs for unauthorized access attempts

On this page