| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127 |
- #!/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 <<JSON
- {
- "tenant": {
- "name": "${TENANT_EXTERNAL_ID}",
- "external_id": "${TENANT_EXTERNAL_ID}",
- "jwt_secret": "${JWT_SECRET}",
- "extensions": [{
- "type": "postgres_cdc_rls",
- "settings": {
- "db_host": "${DB_HOST}",
- "db_name": "${POSTGRES_DB}",
- "db_user": "${POSTGRES_USER}",
- "db_password": "${POSTGRES_PASSWORD}",
- "db_port": "${DB_PORT}",
- "region": "us-east-1",
- "ssl_enforced": false,
- "publication": "supabase_realtime"
- }
- }],
- "postgres_cdc_default": "postgres_cdc_rls"
- }
- }
- JSON
- )
- HTTP=$(curl -sS -o /tmp/tenant_post.json -w '%{http_code}' \
- -X POST "http://${REALTIME_HOST}:${REALTIME_PORT}/api/tenants" \
- -H "Authorization: Bearer ${JWT}" \
- -H "Content-Type: application/json" \
- -d "$BODY")
- if [ "$HTTP" != "201" ] && [ "$HTTP" != "200" ]; then
- log "tenant creation failed (HTTP ${HTTP}):"
- cat /tmp/tenant_post.json || true
- exit 1
- fi
- log "tenant ${TENANT_EXTERNAL_ID} created (HTTP ${HTTP})"
|