diff --git a/.env.example b/.env.example index 34c62adfb..1e32defaf 100644 --- a/.env.example +++ b/.env.example @@ -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= +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= +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= +ENTERPRISE_SERVICE_ROLE_KEY= + +# 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= +# SUPABASE_SERVICE_ROLE_KEY= +# SUPABASE_DB_URL=postgres://postgres:@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:///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 +# +# 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= +# S3_SECRET_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 + # === 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). diff --git a/CLAUDE.md b/CLAUDE.md index b95f8ccff..53f8756e7 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -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 diff --git a/deploy/docker/docker-compose.enterprise.yml b/deploy/docker/docker-compose.enterprise.yml new file mode 100644 index 000000000..e027ae96f --- /dev/null +++ b/deploy/docker/docker-compose.enterprise.yml @@ -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)} + 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: + 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} + 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)} + 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" + 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: diff --git a/deploy/docker/docker-compose.yml b/deploy/docker/docker-compose.yml index 66267d075..939ad5fcf 100644 --- a/deploy/docker/docker-compose.yml +++ b/deploy/docker/docker-compose.yml @@ -20,6 +20,9 @@ services: LITELLM_BASE_URL: ${LITELLM_BASE_URL:-http://litellm:4000} LITELLM_MASTER_KEY: ${LITELLM_MASTER_KEY:-litellm-dev-key} REDIS_URL: ${REDIS_URL:-redis://redis:6379/0} + # SUPABASE_DB_URL: For the enterprise profile set this to the local DSN: + # postgres://postgres:@supabase-db:5432/postgres + # For cloud/local profiles set this to the hosted Supabase DB URL. SUPABASE_DB_URL: ${SUPABASE_DB_URL} S3_ENDPOINT: ${S3_ENDPOINT} S3_ACCESS_KEY: ${S3_ACCESS_KEY} @@ -28,6 +31,11 @@ services: S3_USE_SSL: ${S3_USE_SSL:-true} S3_BUCKET_IMAGES: ${S3_BUCKET_IMAGES:-hive-images} S3_BUCKET_FILES: ${S3_BUCKET_FILES:-hive-files} + # SUPABASE_JWT_ISSUER / SUPABASE_JWKS_URL: For the enterprise profile set + # these to the local GoTrue service: + # SUPABASE_JWT_ISSUER=http://supabase-auth:9999 + # SUPABASE_JWKS_URL=http://supabase-auth:9999/.well-known/jwks.json + # For cloud/local profiles set these to the hosted Supabase auth URLs. SUPABASE_JWT_ISSUER: ${SUPABASE_JWT_ISSUER:-} SUPABASE_JWT_AUDIENCE: ${SUPABASE_JWT_AUDIENCE:-authenticated} SUPABASE_JWKS_URL: ${SUPABASE_JWKS_URL:-} @@ -164,11 +172,26 @@ services: # Unix socket directly via net/http; no docker CLI binary is needed. - /var/run/docker.sock:/var/run/docker.sock environment: + # SUPABASE_URL / SUPABASE_ANON_KEY / SUPABASE_SERVICE_ROLE_KEY: + # Enterprise profile: SUPABASE_URL must point at GoTrue, not PostgREST. + # control-plane calls GET /auth/v1/user on this URL; PostgREST cannot serve auth routes. + # SUPABASE_URL=http://supabase-auth:9999 + # SUPABASE_ANON_KEY= + # SUPABASE_SERVICE_ROLE_KEY= + # Cloud/local profiles: set these to the hosted Supabase project values. SUPABASE_URL: ${SUPABASE_URL} SUPABASE_ANON_KEY: ${SUPABASE_ANON_KEY} SUPABASE_SERVICE_ROLE_KEY: ${SUPABASE_SERVICE_ROLE_KEY} + # Enterprise profile: set to local Postgres DSN: + # postgres://postgres:@supabase-db:5432/postgres SUPABASE_DB_URL: ${SUPABASE_DB_URL} REDIS_URL: ${REDIS_URL} + # Enterprise profile: set to local Storage endpoint: + # S3_ENDPOINT=http://supabase-storage:5000/storage/v1/s3 + # S3_ACCESS_KEY= + # S3_SECRET_KEY= + # S3_REGION=local + # S3_USE_SSL=false (internal compose network, no TLS) S3_ENDPOINT: ${S3_ENDPOINT} S3_ACCESS_KEY: ${S3_ACCESS_KEY} S3_SECRET_KEY: ${S3_SECRET_KEY} @@ -204,6 +227,27 @@ services: retries: 5 start_period: 120s + # ────────────────────────────────────────────────────────────────────────── + # EnterpriseEdge: Self-hosted Supabase data plane (issue #231) + # + # The five Supabase services (supabase-db, supabase-auth, supabase-rest, + # supabase-storage, supabase-init) are defined in the companion override + # file docker-compose.enterprise.yml. They are kept in a separate file + # because Docker Compose evaluates ALL environment variable interpolations + # in ALL services regardless of which profile is active, so :? guards in + # enterprise-only services would break `--profile local` and `--profile cloud` + # config validation for operators who have not set ENTERPRISE_* vars. + # + # To bring up the full enterprise stack: + # docker compose \ + # -f docker-compose.yml \ + # -f docker-compose.enterprise.yml \ + # --profile enterprise up --build + # + # The installer (scripts/install.sh) passes both -f flags automatically when + # the enterprise profile is selected. + # ────────────────────────────────────────────────────────────────────────── + # In-stack Redis. Active for local dev, SDK tests, and self-hosted # enterprise deployments. Cloud deployments use managed Upstash via # REDIS_URL and do NOT activate this service. @@ -441,3 +485,5 @@ volumes: # and writes it atomically; litellm reads it at startup and on restart. # Phase 20 Plan 03. litellm-config: + # supabase-db-data and supabase-storage-data are declared in + # docker-compose.enterprise.yml and only exist when that override is loaded. diff --git a/deploy/supabase/init/00-extensions.sql b/deploy/supabase/init/00-extensions.sql new file mode 100644 index 000000000..16983f0d7 --- /dev/null +++ b/deploy/supabase/init/00-extensions.sql @@ -0,0 +1,55 @@ +-- Bootstrap extensions and schemas required by the self-hosted Supabase stack. +-- This file runs inside docker-entrypoint-initdb.d on first Postgres startup. +-- It is idempotent: all statements use IF NOT EXISTS. +-- +-- Required by: +-- supabase-auth (GoTrue) - needs the `auth` schema +-- supabase-rest (PostgREST) - needs the `storage` and `graphql_public` schemas +-- supabase-storage - needs the `storage` schema +-- RAG migration (#232) - needs the `vector` extension + +-- Extensions +CREATE EXTENSION IF NOT EXISTS "uuid-ossp"; +CREATE EXTENSION IF NOT EXISTS "pgcrypto"; +CREATE EXTENSION IF NOT EXISTS "vector"; + +-- Schemas consumed by the Supabase self-host components +CREATE SCHEMA IF NOT EXISTS auth; +CREATE SCHEMA IF NOT EXISTS storage; +CREATE SCHEMA IF NOT EXISTS graphql_public; +CREATE SCHEMA IF NOT EXISTS extensions; + +-- Roles required by GoTrue and PostgREST self-host configurations. +-- These are created by the official Supabase self-host init scripts; we +-- replicate only what the enterprise edge stack actually needs. +DO $$ +BEGIN + IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'anon') THEN + CREATE ROLE anon NOLOGIN NOINHERIT; + END IF; + IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'authenticated') THEN + CREATE ROLE authenticated NOLOGIN NOINHERIT; + END IF; + IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'service_role') THEN + CREATE ROLE service_role NOLOGIN NOINHERIT BYPASSRLS; + END IF; + IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'supabase_admin') THEN + CREATE ROLE supabase_admin NOLOGIN NOINHERIT BYPASSRLS; + END IF; + -- hive_app is the application role used by control-plane and edge-api. + -- RLS policies in supabase/migrations/20260529_01_rls_tenant_tables.sql + -- grant full access to this role (NOLOGIN, non-BYPASSRLS so RLS still applies). + IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'hive_app') THEN + CREATE ROLE hive_app NOLOGIN; + END IF; +END +$$; + +-- Grant hive_app usage on schemas it needs to read and write application data. +GRANT USAGE ON SCHEMA public TO hive_app; +GRANT USAGE ON SCHEMA storage TO hive_app; + +-- Grant usage on schemas to application roles +GRANT USAGE ON SCHEMA public TO anon, authenticated, service_role; +GRANT USAGE ON SCHEMA storage TO anon, authenticated, service_role; +GRANT USAGE ON SCHEMA extensions TO anon, authenticated, service_role; diff --git a/scripts/install.sh b/scripts/install.sh index 97f29b015..07fcffa4b 100755 --- a/scripts/install.sh +++ b/scripts/install.sh @@ -133,7 +133,10 @@ do_uninstall() { status "Stopping Hive EnterpriseEdge stack..." if [ -d "$HIVE_HOME/deploy/docker" ]; then cd "$HIVE_HOME/deploy/docker" - $SUDO docker compose --env-file "$HIVE_HOME/.env" --profile enterprise down 2>/dev/null || true + $SUDO docker compose \ + -f "$HIVE_HOME/deploy/docker/docker-compose.yml" \ + -f "$HIVE_HOME/deploy/docker/docker-compose.enterprise.yml" \ + --env-file "$HIVE_HOME/.env" --profile enterprise down 2>/dev/null || true fi printf '\n' printf '%s>>> Uninstall complete.%s\n' "${GREEN}" "${RESET}" @@ -647,7 +650,10 @@ wait_healthy() { start_stack() { status "Starting Hive EnterpriseEdge stack (docker compose --profile enterprise)..." cd "$HIVE_HOME/deploy/docker" - $SUDO docker compose --env-file "$HIVE_HOME/.env" --profile enterprise up -d --build + $SUDO docker compose \ + -f "$HIVE_HOME/deploy/docker/docker-compose.yml" \ + -f "$HIVE_HOME/deploy/docker/docker-compose.enterprise.yml" \ + --env-file "$HIVE_HOME/.env" --profile enterprise up -d --build success "Stack started." } @@ -673,7 +679,7 @@ verify_and_banner() { printf '\n' printf ' Run with monitoring:\n' printf ' cd %s/deploy/docker\n' "$HIVE_HOME" - printf ' docker compose --env-file %s/.env --profile enterprise --profile monitoring up -d\n' "$HIVE_HOME" + printf ' docker compose -f %s/deploy/docker/docker-compose.yml -f %s/deploy/docker/docker-compose.enterprise.yml --env-file %s/.env --profile enterprise --profile monitoring up -d\n' "$HIVE_HOME" "$HIVE_HOME" "$HIVE_HOME" printf '\n' if [ "$WITH_OLLAMA" = "true" ]; then printf ' Ollama: http://localhost:11434 (in-stack)\n' @@ -698,7 +704,7 @@ verify_and_banner() { printf '%s>>> Some services did not become healthy within the timeout.%s\n' "${RED}" "${RESET}" printf '\nDiagnostics:\n' printf ' cd %s/deploy/docker\n' "$HIVE_HOME" - printf ' docker compose --env-file %s/.env --profile enterprise logs --tail=50\n' "$HIVE_HOME" + printf ' docker compose -f %s/deploy/docker/docker-compose.yml -f %s/deploy/docker/docker-compose.enterprise.yml --env-file %s/.env --profile enterprise logs --tail=50\n' "$HIVE_HOME" "$HIVE_HOME" "$HIVE_HOME" printf '\nCommon causes:\n' printf ' - .env is missing required values (check %s/.env)\n' "$HIVE_HOME" printf ' - Supabase Storage buckets hive-files / hive-images do not exist yet\n' diff --git a/supabase/migrations/20260625_01_enable_pgvector.sql b/supabase/migrations/20260625_01_enable_pgvector.sql new file mode 100644 index 000000000..e55317214 --- /dev/null +++ b/supabase/migrations/20260625_01_enable_pgvector.sql @@ -0,0 +1,11 @@ +-- Enable the pgvector extension for the enterprise edge profile. +-- This migration is idempotent: IF NOT EXISTS means it is safe to run +-- on hosted Supabase (where the extension may already be present) and +-- on the self-hosted Postgres that ships with the enterprise compose profile. +-- +-- Required by: RAG vector storage (#232), HNSW index on rag_chunks.embedding. +-- Dependency: Postgres image must include the vector extension library. +-- The enterprise compose profile uses pgvector/pgvector:pg16 which ships +-- the extension pre-installed. + +CREATE EXTENSION IF NOT EXISTS vector;