#!/bin/sh # seed-realtime-tenant.sh # # One-shot initializer for the self-hosted Supabase Realtime tenant. # # Realtime v2.x is multi-tenant. Each "project" needs a row in # `_realtime.tenants` (with a paired `_realtime.extensions` row for # postgres_cdc_rls). When the realtime container is reached via Kong, the # upstream Host header becomes `supabase-realtime:4000`, which Realtime maps # to external_id=`supabase-realtime`. Without a matching tenant row, every WS # connect fails with `TenantNotFound` and no postgres_changes propagate. # # Despite SEED_SELF_HOST=true, the auto-seed inserts external_id=`realtime-dev` # (wrong slug for our setup) and never creates `supabase-realtime`. This # script is the deterministic fix. # # Steps: # 1. Ensure `_realtime` schema exists (Realtime's Ecto migrations target it # and bail with `no schema has been selected` if absent). # 2. Wait for the realtime HTTP API to come up. # 3. If a tenant with external_id=`supabase-realtime` already exists, exit 0. # 4. Otherwise mint a short-lived HS256 JWT signed with $JWT_SECRET (which # Realtime accepts as API_JWT_SECRET) and POST /api/tenants. # # Idempotent: safe to run on every `docker compose up`. Runs to completion # and exits; the container is `restart: "no"`. # # Required env: JWT_SECRET, POSTGRES_PASSWORD, (optionals with defaults) # POSTGRES_USER, POSTGRES_DB, DB_HOST, REALTIME_HOST, REALTIME_PORT. set -eu POSTGRES_USER="${POSTGRES_USER:-supabase_admin}" POSTGRES_DB="${POSTGRES_DB:-supabase}" DB_HOST="${DB_HOST:-supabase-db}" DB_PORT="${DB_PORT:-5432}" REALTIME_HOST="${REALTIME_HOST:-supabase-realtime}" REALTIME_PORT="${REALTIME_PORT:-4000}" TENANT_EXTERNAL_ID="${TENANT_EXTERNAL_ID:-supabase-realtime}" : "${JWT_SECRET:?JWT_SECRET must be set}" : "${POSTGRES_PASSWORD:?POSTGRES_PASSWORD must be set}" log() { printf '[seed-realtime] %s\n' "$*"; } # --- 1. Ensure _realtime schema exists ---------------------------------- log "ensuring _realtime schema exists on ${DB_HOST}:${DB_PORT}/${POSTGRES_DB}" PGPASSWORD="$POSTGRES_PASSWORD" psql \ -h "$DB_HOST" -p "$DB_PORT" -U "$POSTGRES_USER" -d "$POSTGRES_DB" \ -v ON_ERROR_STOP=1 \ -c "CREATE SCHEMA IF NOT EXISTS _realtime;" \ -c "GRANT ALL ON SCHEMA _realtime TO ${POSTGRES_USER};" >/dev/null # --- 2. Wait for realtime API ------------------------------------------ log "waiting for realtime API at http://${REALTIME_HOST}:${REALTIME_PORT}" i=0 until curl -sS -o /dev/null -w '%{http_code}' "http://${REALTIME_HOST}:${REALTIME_PORT}/api/health" 2>/dev/null | grep -qE '^(200|401|403|404)$'; do i=$((i + 1)) if [ "$i" -gt 60 ]; then log "realtime API did not become reachable within 60s; aborting" exit 1 fi sleep 1 done log "realtime API reachable" # --- 3. Mint short-lived API JWT (HS256) ------------------------------- # Header/payload base64url, HMAC-SHA256 signature with $JWT_SECRET. b64url() { openssl base64 -A | tr '+/' '-_' | tr -d '='; } NOW=$(date -u +%s) EXP=$((NOW + 300)) HEADER='{"alg":"HS256","typ":"JWT"}' PAYLOAD="{\"iss\":\"supabase\",\"role\":\"supabase_admin\",\"iat\":${NOW},\"exp\":${EXP}}" H_B64=$(printf '%s' "$HEADER" | b64url) P_B64=$(printf '%s' "$PAYLOAD" | b64url) SIG=$(printf '%s' "${H_B64}.${P_B64}" \ | openssl dgst -sha256 -hmac "$JWT_SECRET" -binary \ | b64url) JWT="${H_B64}.${P_B64}.${SIG}" # --- 4. Check existing tenant; create if absent ------------------------ TENANT_URL="http://${REALTIME_HOST}:${REALTIME_PORT}/api/tenants/${TENANT_EXTERNAL_ID}" HTTP=$(curl -sS -o /tmp/tenant_get.json -w '%{http_code}' \ -H "Authorization: Bearer ${JWT}" \ "$TENANT_URL" || echo "000") if [ "$HTTP" = "200" ]; then log "tenant ${TENANT_EXTERNAL_ID} already exists; nothing to do" exit 0 fi log "tenant ${TENANT_EXTERNAL_ID} not found (GET=${HTTP}); creating" BODY=$(cat <