seed-realtime-tenant.sh 4.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127
  1. #!/bin/sh
  2. # seed-realtime-tenant.sh
  3. #
  4. # One-shot initializer for the self-hosted Supabase Realtime tenant.
  5. #
  6. # Realtime v2.x is multi-tenant. Each "project" needs a row in
  7. # `_realtime.tenants` (with a paired `_realtime.extensions` row for
  8. # postgres_cdc_rls). When the realtime container is reached via Kong, the
  9. # upstream Host header becomes `supabase-realtime:4000`, which Realtime maps
  10. # to external_id=`supabase-realtime`. Without a matching tenant row, every WS
  11. # connect fails with `TenantNotFound` and no postgres_changes propagate.
  12. #
  13. # Despite SEED_SELF_HOST=true, the auto-seed inserts external_id=`realtime-dev`
  14. # (wrong slug for our setup) and never creates `supabase-realtime`. This
  15. # script is the deterministic fix.
  16. #
  17. # Steps:
  18. # 1. Ensure `_realtime` schema exists (Realtime's Ecto migrations target it
  19. # and bail with `no schema has been selected` if absent).
  20. # 2. Wait for the realtime HTTP API to come up.
  21. # 3. If a tenant with external_id=`supabase-realtime` already exists, exit 0.
  22. # 4. Otherwise mint a short-lived HS256 JWT signed with $JWT_SECRET (which
  23. # Realtime accepts as API_JWT_SECRET) and POST /api/tenants.
  24. #
  25. # Idempotent: safe to run on every `docker compose up`. Runs to completion
  26. # and exits; the container is `restart: "no"`.
  27. #
  28. # Required env: JWT_SECRET, POSTGRES_PASSWORD, (optionals with defaults)
  29. # POSTGRES_USER, POSTGRES_DB, DB_HOST, REALTIME_HOST, REALTIME_PORT.
  30. set -eu
  31. POSTGRES_USER="${POSTGRES_USER:-supabase_admin}"
  32. POSTGRES_DB="${POSTGRES_DB:-supabase}"
  33. DB_HOST="${DB_HOST:-supabase-db}"
  34. DB_PORT="${DB_PORT:-5432}"
  35. REALTIME_HOST="${REALTIME_HOST:-supabase-realtime}"
  36. REALTIME_PORT="${REALTIME_PORT:-4000}"
  37. TENANT_EXTERNAL_ID="${TENANT_EXTERNAL_ID:-supabase-realtime}"
  38. : "${JWT_SECRET:?JWT_SECRET must be set}"
  39. : "${POSTGRES_PASSWORD:?POSTGRES_PASSWORD must be set}"
  40. log() { printf '[seed-realtime] %s\n' "$*"; }
  41. # --- 1. Ensure _realtime schema exists ----------------------------------
  42. log "ensuring _realtime schema exists on ${DB_HOST}:${DB_PORT}/${POSTGRES_DB}"
  43. PGPASSWORD="$POSTGRES_PASSWORD" psql \
  44. -h "$DB_HOST" -p "$DB_PORT" -U "$POSTGRES_USER" -d "$POSTGRES_DB" \
  45. -v ON_ERROR_STOP=1 \
  46. -c "CREATE SCHEMA IF NOT EXISTS _realtime;" \
  47. -c "GRANT ALL ON SCHEMA _realtime TO ${POSTGRES_USER};" >/dev/null
  48. # --- 2. Wait for realtime API ------------------------------------------
  49. log "waiting for realtime API at http://${REALTIME_HOST}:${REALTIME_PORT}"
  50. i=0
  51. 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
  52. i=$((i + 1))
  53. if [ "$i" -gt 60 ]; then
  54. log "realtime API did not become reachable within 60s; aborting"
  55. exit 1
  56. fi
  57. sleep 1
  58. done
  59. log "realtime API reachable"
  60. # --- 3. Mint short-lived API JWT (HS256) -------------------------------
  61. # Header/payload base64url, HMAC-SHA256 signature with $JWT_SECRET.
  62. b64url() { openssl base64 -A | tr '+/' '-_' | tr -d '='; }
  63. NOW=$(date -u +%s)
  64. EXP=$((NOW + 300))
  65. HEADER='{"alg":"HS256","typ":"JWT"}'
  66. PAYLOAD="{\"iss\":\"supabase\",\"role\":\"supabase_admin\",\"iat\":${NOW},\"exp\":${EXP}}"
  67. H_B64=$(printf '%s' "$HEADER" | b64url)
  68. P_B64=$(printf '%s' "$PAYLOAD" | b64url)
  69. SIG=$(printf '%s' "${H_B64}.${P_B64}" \
  70. | openssl dgst -sha256 -hmac "$JWT_SECRET" -binary \
  71. | b64url)
  72. JWT="${H_B64}.${P_B64}.${SIG}"
  73. # --- 4. Check existing tenant; create if absent ------------------------
  74. TENANT_URL="http://${REALTIME_HOST}:${REALTIME_PORT}/api/tenants/${TENANT_EXTERNAL_ID}"
  75. HTTP=$(curl -sS -o /tmp/tenant_get.json -w '%{http_code}' \
  76. -H "Authorization: Bearer ${JWT}" \
  77. "$TENANT_URL" || echo "000")
  78. if [ "$HTTP" = "200" ]; then
  79. log "tenant ${TENANT_EXTERNAL_ID} already exists; nothing to do"
  80. exit 0
  81. fi
  82. log "tenant ${TENANT_EXTERNAL_ID} not found (GET=${HTTP}); creating"
  83. BODY=$(cat <<JSON
  84. {
  85. "tenant": {
  86. "name": "${TENANT_EXTERNAL_ID}",
  87. "external_id": "${TENANT_EXTERNAL_ID}",
  88. "jwt_secret": "${JWT_SECRET}",
  89. "extensions": [{
  90. "type": "postgres_cdc_rls",
  91. "settings": {
  92. "db_host": "${DB_HOST}",
  93. "db_name": "${POSTGRES_DB}",
  94. "db_user": "${POSTGRES_USER}",
  95. "db_password": "${POSTGRES_PASSWORD}",
  96. "db_port": "${DB_PORT}",
  97. "region": "us-east-1",
  98. "ssl_enforced": false,
  99. "publication": "supabase_realtime"
  100. }
  101. }],
  102. "postgres_cdc_default": "postgres_cdc_rls"
  103. }
  104. }
  105. JSON
  106. )
  107. HTTP=$(curl -sS -o /tmp/tenant_post.json -w '%{http_code}' \
  108. -X POST "http://${REALTIME_HOST}:${REALTIME_PORT}/api/tenants" \
  109. -H "Authorization: Bearer ${JWT}" \
  110. -H "Content-Type: application/json" \
  111. -d "$BODY")
  112. if [ "$HTTP" != "201" ] && [ "$HTTP" != "200" ]; then
  113. log "tenant creation failed (HTTP ${HTTP}):"
  114. cat /tmp/tenant_post.json || true
  115. exit 1
  116. fi
  117. log "tenant ${TENANT_EXTERNAL_ID} created (HTTP ${HTTP})"