Skip to content
92 changes: 92 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -228,6 +228,98 @@ GRAFANA_ADMIN_PASSWORD=
# deploy/litellm/config.yaml and restart LiteLLM.
OLLAMA_BASE_URL=

# ─── EnterpriseEdge: Self-hosted Supabase data plane (issue #231) ───────────
#
# These variables are ONLY needed when running --profile enterprise.
# Cloud and local profiles continue to use the hosted Supabase vars above.
#
# How to generate JWTs (anon + service_role) for a self-hosted GoTrue:
# Use the Supabase self-host key generator at:
# https://supabase.com/docs/guides/self-hosting/docker#generate-api-keys
# Set the JWT secret field to your ENTERPRISE_JWT_SECRET value, then copy
# the generated anon key into ENTERPRISE_ANON_KEY and the service_role key
# into ENTERPRISE_SERVICE_ROLE_KEY. Both are signed HS256 tokens.
#
# Quick-start secret generation:
# ENTERPRISE_DB_PASSWORD: openssl rand -base64 24
# ENTERPRISE_JWT_SECRET: openssl rand -base64 48 (min 32 chars; keep private)
# ENTERPRISE_ANON_KEY: sign with above (see helper above)
# ENTERPRISE_SERVICE_ROLE_KEY: sign with above (see helper above)

# Postgres credentials for the in-stack supabase-db service.
# Never set ENTERPRISE_DB_PASSWORD to an empty string; the :? operator in
# docker-compose.yml will refuse to start the database without it.
ENTERPRISE_DB_USER=postgres
ENTERPRISE_DB_PASSWORD=<generate: openssl rand -base64 24>
ENTERPRISE_DB_NAME=postgres

# JWT configuration for GoTrue (supabase-auth) and PostgREST (supabase-rest).
# ENTERPRISE_JWT_SECRET is the HS256 signing secret. Minimum 32 characters.
# ENTERPRISE_JWT_ISSUER must match what GoTrue uses to issue tokens so that
# apps/edge-api/internal/auth/jwt_supabase.go validates them correctly.
ENTERPRISE_JWT_SECRET=<generate: openssl rand -base64 48>
ENTERPRISE_JWT_ISSUER=http://supabase-auth:9999
ENTERPRISE_JWT_EXP=3600

# Supabase API keys derived from ENTERPRISE_JWT_SECRET (see helper above).
ENTERPRISE_ANON_KEY=<sign anon JWT with ENTERPRISE_JWT_SECRET>
ENTERPRISE_SERVICE_ROLE_KEY=<sign service_role JWT with ENTERPRISE_JWT_SECRET>

# Site URL GoTrue embeds in email confirmation links.
ENTERPRISE_SITE_URL=http://localhost:3000
# Comma-separated redirect allow-list for OAuth flows (leave empty for none).
ENTERPRISE_REDIRECT_ALLOW_LIST=
# Set to true to skip email confirmation (recommended for air-gapped installs).
ENTERPRISE_DISABLE_SIGNUP=false
ENTERPRISE_MAILER_AUTOCONFIRM=true

# SMTP settings for email delivery. Leave empty to disable email (autoconfirm
# above must be true when SMTP is not configured).
ENTERPRISE_SMTP_HOST=
ENTERPRISE_SMTP_PORT=587
ENTERPRISE_SMTP_USER=
ENTERPRISE_SMTP_PASS=
ENTERPRISE_SMTP_ADMIN_EMAIL=admin@example.com

# Storage file size limit in bytes. Default 50 MB.
ENTERPRISE_STORAGE_FILE_SIZE_LIMIT=52428800

# ── Rewire core vars to in-box services for the enterprise profile ─────────
# When running --profile enterprise, set these vars to point at the in-stack
# Supabase services instead of the hosted Supabase project values above.
#
# Uncomment and set these values for the enterprise profile:
#
# SUPABASE_URL must point at GoTrue (not PostgREST): control-plane calls
# GET /auth/v1/user on this URL. PostgREST cannot serve auth/v1 routes.
# SUPABASE_URL=http://supabase-auth:9999
# SUPABASE_ANON_KEY=<ENTERPRISE_ANON_KEY>
# SUPABASE_SERVICE_ROLE_KEY=<ENTERPRISE_SERVICE_ROLE_KEY>
# SUPABASE_DB_URL=postgres://postgres:<ENTERPRISE_DB_PASSWORD>@supabase-db:5432/postgres
# SUPABASE_JWT_ISSUER=http://supabase-auth:9999
#
# JWKS NOTE: edge-api/cmd/server/main.go rejects http:// JWKS URLs as insecure.
# For a production enterprise box, terminate TLS in Caddy and set:
# SUPABASE_JWKS_URL=https://<your-domain>/auth/v1/.well-known/jwks.json
# For a LAN-only or air-gapped dev box where TLS is not available, set
# SUPABASE_JWKS_URL to the internal http URL only after confirming the edge-api
# HTTPS guard is relaxed in config or the service is behind an internal TLS proxy.
# SUPABASE_JWKS_URL=http://supabase-auth:9999/.well-known/jwks.json
Comment thread
sakibsadmanshajib marked this conversation as resolved.
#
# Storage: Supabase Storage API with local filesystem backend (no external S3).
# The S3-compatible endpoint is at /storage/v1/s3 (matches hosted Supabase path).
# S3_ENDPOINT=http://supabase-storage:5000/storage/v1/s3
# S3_ACCESS_KEY=<ENTERPRISE_SERVICE_ROLE_KEY>
# S3_SECRET_KEY=<ENTERPRISE_SERVICE_ROLE_KEY>
# S3_REGION=local
# S3_USE_SSL=false
# S3_BUCKET_FILES=hive-files
# S3_BUCKET_IMAGES=hive-images
#
# Open WebUI OIDC: point at the local GoTrue issuer.
# OPENID_PROVIDER_URL=http://supabase-auth:9999/.well-known/openid-configuration
# NEXT_PUBLIC_SUPABASE_URL=http://localhost:9999

Comment thread
coderabbitai[bot] marked this conversation as resolved.
# === LiteLLM config generation (Phase 20 Plan 03) ===
# LITELLM_CONTAINER_NAME: Docker container name to restart after config write.
# Default: litellm (matches the service name in docker-compose.yml).
Expand Down
5 changes: 4 additions & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,10 @@ docker compose --env-file ../../.env --profile cloud --profile chat up --build
# Hive EnterpriseEdge (self-hosted single box): core + in-stack Redis + OWUI + Caddy.
# Optional Ollama: set OLLAMA_BASE_URL=http://ollama:11434 in .env and
# uncomment the ollama model entries in deploy/litellm/config.yaml.
docker compose --env-file ../../.env --profile enterprise up --build
docker compose \
-f docker-compose.yml \
-f docker-compose.enterprise.yml \
--env-file ../../.env --profile enterprise up --build

# Add monitoring to any profile (Prometheus, Grafana, Alertmanager):
docker compose --env-file ../../.env --profile local --profile monitoring up --build
Expand Down
230 changes: 230 additions & 0 deletions deploy/docker/docker-compose.enterprise.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,230 @@
# docker-compose.enterprise.yml
#
# EnterpriseEdge self-hosted Supabase data plane override (issue #231).
#
# This file is intentionally separate from docker-compose.yml because Docker
# Compose evaluates ALL environment interpolations for ALL services regardless
# of which --profile is active. Placing the enterprise-only Supabase services
# here means that operators running --profile local or --profile cloud never
# see :? errors for ENTERPRISE_* vars they have not set.
#
# Usage (always pass both -f flags for enterprise):
# docker compose \
# -f docker-compose.yml \
# -f docker-compose.enterprise.yml \
# --profile enterprise up --build
#
# The installer (scripts/install.sh) does this automatically.
#
# Required vars (set in .env before running):
# ENTERPRISE_DB_PASSWORD openssl rand -base64 24
# ENTERPRISE_JWT_SECRET openssl rand -base64 48 (min 32 chars)
# ENTERPRISE_ANON_KEY sign {"role":"anon"} JWT with ENTERPRISE_JWT_SECRET
# ENTERPRISE_SERVICE_ROLE_KEY sign {"role":"service_role"} JWT with ENTERPRISE_JWT_SECRET
#
# See .env.example for the full ENTERPRISE_* block and generation instructions.

name: hive

services:

# ── supabase-db ────────────────────────────────────────────────────────────
# Postgres 16 with the pgvector extension pre-installed.
# No host port binding: all access is via the internal compose network.
# The init script at deploy/supabase/init/00-extensions.sql runs once on
# first startup and creates the required extensions, schemas, and roles
# (including hive_app which RLS policies reference).
supabase-db:
image: pgvector/pgvector:pg16
profiles:
- enterprise
environment:
POSTGRES_USER: ${ENTERPRISE_DB_USER:-postgres}
POSTGRES_PASSWORD: ${ENTERPRISE_DB_PASSWORD:?set ENTERPRISE_DB_PASSWORD in .env (openssl rand -base64 24)}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
POSTGRES_DB: ${ENTERPRISE_DB_NAME:-postgres}
volumes:
- supabase-db-data:/var/lib/postgresql/data
- ../../deploy/supabase/init:/docker-entrypoint-initdb.d:ro
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${ENTERPRISE_DB_USER:-postgres} -d ${ENTERPRISE_DB_NAME:-postgres}"]
interval: 5s
timeout: 3s
retries: 10
start_period: 30s
restart: unless-stopped

# ── supabase-auth (GoTrue) ─────────────────────────────────────────────────
# Local JWT issuer. edge-api validates tokens against this service via
# SUPABASE_JWKS_URL=http://supabase-auth:9999/.well-known/jwks.json.
# Set ENTERPRISE_MAILER_AUTOCONFIRM=true for air-gapped installs without SMTP.
supabase-auth:
Comment thread
sakibsadmanshajib marked this conversation as resolved.
image: supabase/gotrue:v2.170.0
profiles:
- enterprise
depends_on:
supabase-db:
condition: service_healthy
environment:
GOTRUE_API_HOST: "0.0.0.0"
GOTRUE_API_PORT: "9999"
# Required by GoTrue v2: the public-facing base URL for auth links in emails.
API_EXTERNAL_URL: ${ENTERPRISE_SITE_URL:-http://localhost:9999}
GOTRUE_DB_DRIVER: postgres
GOTRUE_DB_DATABASE_URL: "postgres://${ENTERPRISE_DB_USER:-postgres}:${ENTERPRISE_DB_PASSWORD:?set ENTERPRISE_DB_PASSWORD in .env}@supabase-db:5432/${ENTERPRISE_DB_NAME:-postgres}?search_path=auth"
GOTRUE_SITE_URL: ${ENTERPRISE_SITE_URL:-http://localhost:3000}
Comment thread
sakibsadmanshajib marked this conversation as resolved.
GOTRUE_URI_ALLOW_LIST: ${ENTERPRISE_REDIRECT_ALLOW_LIST:-}
GOTRUE_DISABLE_SIGNUP: ${ENTERPRISE_DISABLE_SIGNUP:-false}
GOTRUE_JWT_SECRET: ${ENTERPRISE_JWT_SECRET:?set ENTERPRISE_JWT_SECRET in .env (openssl rand -base64 48)}
Comment thread
sakibsadmanshajib marked this conversation as resolved.
GOTRUE_JWT_EXP: ${ENTERPRISE_JWT_EXP:-3600}
GOTRUE_JWT_DEFAULT_GROUP_NAME: authenticated
GOTRUE_JWT_ADMIN_ROLES: service_role
GOTRUE_JWT_AUD: authenticated
# Must match what edge-api expects in SUPABASE_JWT_ISSUER / SUPABASE_JWKS_URL.
# Default is the in-stack service name, not localhost.
GOTRUE_JWT_ISSUER: ${ENTERPRISE_JWT_ISSUER:-http://supabase-auth:9999}
GOTRUE_EXTERNAL_EMAIL_ENABLED: "true"
GOTRUE_MAILER_AUTOCONFIRM: ${ENTERPRISE_MAILER_AUTOCONFIRM:-true}
GOTRUE_SMTP_HOST: ${ENTERPRISE_SMTP_HOST:-}
GOTRUE_SMTP_PORT: ${ENTERPRISE_SMTP_PORT:-587}
GOTRUE_SMTP_USER: ${ENTERPRISE_SMTP_USER:-}
GOTRUE_SMTP_PASS: ${ENTERPRISE_SMTP_PASS:-}
GOTRUE_SMTP_ADMIN_EMAIL: ${ENTERPRISE_SMTP_ADMIN_EMAIL:-admin@example.com}
GOTRUE_MAILER_URLPATHS_INVITE: /auth/v1/verify
GOTRUE_MAILER_URLPATHS_CONFIRMATION: /auth/v1/verify
GOTRUE_MAILER_URLPATHS_RECOVERY: /auth/v1/verify
GOTRUE_MAILER_URLPATHS_EMAIL_CHANGE: /auth/v1/verify
# Enable the custom access token hook so JWTs include tenant_id, tenants,
# and role claims. Migration 20260516_07 installs public.custom_access_token_hook;
# without this flag GoTrue issues plain tokens and all RLS/authz middleware breaks.
GOTRUE_HOOK_CUSTOM_ACCESS_TOKEN_ENABLED: "true"
GOTRUE_HOOK_CUSTOM_ACCESS_TOKEN_URI: "pg-functions://postgres/public/custom_access_token_hook"
Comment thread
sakibsadmanshajib marked this conversation as resolved.
healthcheck:
test: ["CMD-SHELL", "wget --no-verbose --tries=1 --spider http://localhost:9999/health || exit 1"]
interval: 5s
timeout: 3s
retries: 10
start_period: 30s
restart: unless-stopped

# ── supabase-rest (PostgREST) ──────────────────────────────────────────────
# Auto REST API over local Postgres. control-plane uses this as SUPABASE_URL.
supabase-rest:
image: postgrest/postgrest:v12.2.3
profiles:
- enterprise
depends_on:
supabase-db:
condition: service_healthy
environment:
PGRST_DB_URI: "postgres://${ENTERPRISE_DB_USER:-postgres}:${ENTERPRISE_DB_PASSWORD:?set ENTERPRISE_DB_PASSWORD in .env}@supabase-db:5432/${ENTERPRISE_DB_NAME:-postgres}"
PGRST_DB_SCHEMAS: "public,storage,graphql_public"
PGRST_DB_ANON_ROLE: anon
PGRST_JWT_SECRET: ${ENTERPRISE_JWT_SECRET:?set ENTERPRISE_JWT_SECRET in .env}
PGRST_DB_USE_LEGACY_GUCS: "false"
PGRST_APP_SETTINGS_JWT_SECRET: ${ENTERPRISE_JWT_SECRET:?set ENTERPRISE_JWT_SECRET in .env}
PGRST_APP_SETTINGS_JWT_EXP: ${ENTERPRISE_JWT_EXP:-3600}
healthcheck:
# ponytail: postgrest image has no wget/curl; /dev/tcp is a bash builtin TCP probe.
test: ["CMD-SHELL", "bash -c 'echo > /dev/tcp/localhost/3000' 2>/dev/null || exit 1"]
interval: 5s
timeout: 3s
retries: 10
start_period: 15s
restart: unless-stopped

# ── supabase-storage ───────────────────────────────────────────────────────
# Supabase Storage API with local filesystem backend.
# No MinIO: STORAGE_BACKEND=file writes object data to the supabase-storage-data
# volume. No S3 call leaves the box.
# S3_ENDPOINT for edge-api and control-plane: http://supabase-storage:5000/object/s3
supabase-storage:
image: supabase/storage-api:v1.11.13
profiles:
- enterprise
depends_on:
supabase-db:
condition: service_healthy
supabase-rest:
condition: service_healthy
environment:
ANON_KEY: ${ENTERPRISE_ANON_KEY:?set ENTERPRISE_ANON_KEY in .env (sign anon JWT with ENTERPRISE_JWT_SECRET)}
SERVICE_KEY: ${ENTERPRISE_SERVICE_ROLE_KEY:?set ENTERPRISE_SERVICE_ROLE_KEY in .env (sign service_role JWT)}
POSTGREST_URL: http://supabase-rest:3000
PGRST_JWT_SECRET: ${ENTERPRISE_JWT_SECRET:?set ENTERPRISE_JWT_SECRET in .env}
DATABASE_URL: "postgres://${ENTERPRISE_DB_USER:-postgres}:${ENTERPRISE_DB_PASSWORD:?set ENTERPRISE_DB_PASSWORD in .env}@supabase-db:5432/${ENTERPRISE_DB_NAME:-postgres}"
FILE_SIZE_LIMIT: ${ENTERPRISE_STORAGE_FILE_SIZE_LIMIT:-52428800}
STORAGE_BACKEND: file
FILE_STORAGE_BACKEND_PATH: /var/lib/storage
TENANT_ID: stub
REGION: local
GLOBAL_S3_BUCKET: stub
ENABLE_IMAGE_TRANSFORMATION: "true"
IMGPROXY_URL: ""
volumes:
- supabase-storage-data:/var/lib/storage
healthcheck:
test: ["CMD-SHELL", "wget --no-verbose --tries=1 --spider http://localhost:5000/status || exit 1"]
interval: 5s
timeout: 3s
retries: 10
start_period: 15s
restart: unless-stopped

# ── supabase-init ──────────────────────────────────────────────────────────
# One-shot container: creates the hive-files and hive-images buckets via the
# Storage API. Runs once per compose up; restart: "no" prevents re-runs.
# Bucket creation is idempotent: a 409 from an existing bucket is treated as
# success (|| true) so re-runs after a container recreate do not fail.
supabase-init:
image: curlimages/curl:8.7.1
profiles:
- enterprise
depends_on:
supabase-storage:
condition: service_healthy
environment:
SERVICE_KEY: ${ENTERPRISE_SERVICE_ROLE_KEY:?set ENTERPRISE_SERVICE_ROLE_KEY in .env}
entrypoint:
- /bin/sh
- -c
- |
set -e
STORAGE_URL="http://supabase-storage:5000"
# ponytail: $${VAR} escapes compose interpolation; shell expands at runtime.
AUTH_HEADER="Authorization: Bearer $${SERVICE_KEY}"
for BUCKET in hive-files hive-images; do
echo "Creating bucket: $${BUCKET}"
HTTP_STATUS=$(curl -s -o /dev/null -w "%{http_code}" -X POST "$${STORAGE_URL}/bucket" \
-H "Content-Type: application/json" \
-H "$${AUTH_HEADER}" \
-d "{\"id\":\"$${BUCKET}\",\"name\":\"$${BUCKET}\",\"public\":false}")
case "$${HTTP_STATUS}" in
2*|409) echo " bucket $${BUCKET}: ok (status $${HTTP_STATUS})" ;;
*) echo " bucket $${BUCKET}: FAILED (status $${HTTP_STATUS})"; exit 1 ;;
esac
done
echo "Bucket init complete."
restart: "no"

# ── depends_on overrides for edge-api and control-plane ───────────────────
# The base docker-compose.yml already declares required:false stubs for
# supabase-auth and supabase-storage. This override adds supabase-init so
# neither application service starts until buckets exist. Kept here (not in
# the base file) so --profile local and --profile cloud are unaffected.
edge-api:
depends_on:
supabase-init:
condition: service_completed_successfully

control-plane:
depends_on:
supabase-init:
condition: service_completed_successfully

volumes:
# Postgres WAL and data files. Persists the full schema, all migrations, and
# all tenant data across container recreates.
supabase-db-data:
# Object files written by the Storage API local filesystem backend.
# Holds hive-files and hive-images bucket contents on the box filesystem.
supabase-storage-data:
Loading
Loading