diff --git a/templates/compose/supabase.yaml b/templates/compose/supabase.yaml index f0f2cdd31..ae3160b53 100644 --- a/templates/compose/supabase.yaml +++ b/templates/compose/supabase.yaml @@ -8,25 +8,35 @@ services: supabase-kong: - image: kong:2.8.1 - # https://unix.stackexchange.com/a/294837 - entrypoint: bash -c 'eval "echo \"$$(cat ~/temp.yml)\"" > ~/kong.yml && /docker-entrypoint.sh kong docker-start' + image: kong/kong:3.9.1 + entrypoint: /home/kong/kong-entrypoint.sh depends_on: supabase-analytics: condition: service_healthy + healthcheck: + test: ["CMD", "kong", "health"] + interval: 5s + timeout: 5s + retries: 5 environment: - SERVICE_URL_SUPABASEKONG_8000 - KONG_PORT_MAPS=443:8000 - JWT_SECRET=${SERVICE_PASSWORD_JWT} - KONG_DATABASE=off - - KONG_DECLARATIVE_CONFIG=/home/kong/kong.yml + - KONG_DECLARATIVE_CONFIG=/usr/local/kong/kong.yml # https://github.com/supabase/cli/issues/14 - KONG_DNS_ORDER=LAST,A,CNAME - - KONG_PLUGINS=request-transformer,cors,key-auth,acl,basic-auth,request-termination + - KONG_DNS_NOT_FOUND_TTL=1 + - KONG_PLUGINS=request-transformer,cors,key-auth,acl,basic-auth,request-termination,ip-restriction,post-function - KONG_NGINX_PROXY_PROXY_BUFFER_SIZE=160k - KONG_NGINX_PROXY_PROXY_BUFFERS=64 160k + - 'KONG_PROXY_ACCESS_LOG=/dev/stdout combined' - SUPABASE_ANON_KEY=${SERVICE_SUPABASEANON_KEY} - SUPABASE_SERVICE_KEY=${SERVICE_SUPABASESERVICE_KEY} + - SUPABASE_PUBLISHABLE_KEY=${SUPABASE_PUBLISHABLE_KEY:-} + - SUPABASE_SECRET_KEY=${SUPABASE_SECRET_KEY:-} + - ANON_KEY_ASYMMETRIC=${ANON_KEY_ASYMMETRIC:-} + - SERVICE_ROLE_KEY_ASYMMETRIC=${SERVICE_ROLE_KEY_ASYMMETRIC:-} - DASHBOARD_USERNAME=${SERVICE_USER_ADMIN} - DASHBOARD_PASSWORD=${SERVICE_PASSWORD_ADMIN} - 'KONG_STORAGE_CONNECT_TIMEOUT=${KONG_STORAGE_CONNECT_TIMEOUT:-60}' @@ -35,6 +45,40 @@ services: - 'KONG_STORAGE_REQUEST_BUFFERING=${KONG_STORAGE_REQUEST_BUFFERING:-false}' - 'KONG_STORAGE_RESPONSE_BUFFERING=${KONG_STORAGE_RESPONSE_BUFFERING:-false}' volumes: + - type: bind + source: ./volumes/api/kong-entrypoint.sh + target: /home/kong/kong-entrypoint.sh + content: | + #!/bin/bash + # Custom entrypoint for Kong that builds Lua expressions for request-transformer + # and performs environment variable substitution in the declarative config. + + if [ -n "$SUPABASE_SECRET_KEY" ] && [ -n "$SUPABASE_PUBLISHABLE_KEY" ]; then + export LUA_AUTH_EXPR="\$((headers.authorization ~= nil and headers.authorization:sub(1, 10) ~= 'Bearer sb_' and headers.authorization) or (headers.apikey == '$SUPABASE_SECRET_KEY' and 'Bearer $SERVICE_ROLE_KEY_ASYMMETRIC') or (headers.apikey == '$SUPABASE_PUBLISHABLE_KEY' and 'Bearer $ANON_KEY_ASYMMETRIC') or headers.apikey)" + export LUA_RT_WS_EXPR="\$((query_params.apikey == '$SUPABASE_SECRET_KEY' and '$SERVICE_ROLE_KEY_ASYMMETRIC') or (query_params.apikey == '$SUPABASE_PUBLISHABLE_KEY' and '$ANON_KEY_ASYMMETRIC') or query_params.apikey)" + else + export LUA_AUTH_EXPR="\$((headers.authorization ~= nil and headers.authorization:sub(1, 10) ~= 'Bearer sb_' and headers.authorization) or headers.apikey)" + export LUA_RT_WS_EXPR="\$(query_params.apikey)" + fi + + awk '{ + result = "" + rest = $0 + while (match(rest, /\$[A-Za-z_][A-Za-z_0-9]*/)) { + varname = substr(rest, RSTART + 1, RLENGTH - 1) + if (varname in ENVIRON) { + result = result substr(rest, 1, RSTART - 1) ENVIRON[varname] + } else { + result = result substr(rest, 1, RSTART + RLENGTH - 1) + } + rest = substr(rest, RSTART + RLENGTH) + } + print result rest + }' /home/kong/temp.yml > "$KONG_DECLARATIVE_CONFIG" + + sed -i '/^[[:space:]]*- key:[[:space:]]*$/d' "$KONG_DECLARATIVE_CONFIG" + + exec /entrypoint.sh kong docker-start # https://github.com/supabase/supabase/issues/12661 - type: bind source: ./volumes/api/kong.yml @@ -51,9 +95,11 @@ services: - username: anon keyauth_credentials: - key: $SUPABASE_ANON_KEY + - key: $SUPABASE_PUBLISHABLE_KEY - username: service_role keyauth_credentials: - key: $SUPABASE_SERVICE_KEY + - key: $SUPABASE_SECRET_KEY ### ### Access Control List @@ -69,8 +115,8 @@ services: ### basicauth_credentials: - consumer: DASHBOARD - username: $DASHBOARD_USERNAME - password: $DASHBOARD_PASSWORD + username: '$DASHBOARD_USERNAME' + password: '$DASHBOARD_PASSWORD' ### @@ -106,6 +152,36 @@ services: - /auth/v1/authorize plugins: - name: cors + - name: auth-v1-open-jwks + _comment: 'Auth: /auth/v1/.well-known/jwks.json -> http://supabase-auth:9999/.well-known/jwks.json' + url: http://supabase-auth:9999/.well-known/jwks.json + routes: + - name: auth-v1-open-jwks + strip_path: true + paths: + - /auth/v1/.well-known/jwks.json + plugins: + - name: cors + + - name: auth-v1-open-sso-acs + url: "http://supabase-auth:9999/sso/saml/acs" + routes: + - name: auth-v1-open-sso-acs + strip_path: true + paths: + - /sso/saml/acs + plugins: + - name: cors + + - name: auth-v1-open-sso-metadata + url: "http://supabase-auth:9999/sso/saml/metadata" + routes: + - name: auth-v1-open-sso-metadata + strip_path: true + paths: + - /sso/saml/metadata + plugins: + - name: cors ## Secure Auth routes - name: auth-v1 @@ -121,6 +197,14 @@ services: - name: key-auth config: hide_credentials: false + - name: request-transformer + config: + add: + headers: + - "Authorization: $LUA_AUTH_EXPR" + replace: + headers: + - "Authorization: $LUA_AUTH_EXPR" - name: acl config: hide_groups_header: true @@ -141,7 +225,15 @@ services: - name: cors - name: key-auth config: - hide_credentials: true + hide_credentials: false + - name: request-transformer + config: + add: + headers: + - "Authorization: $LUA_AUTH_EXPR" + replace: + headers: + - "Authorization: $LUA_AUTH_EXPR" - name: acl config: hide_groups_header: true @@ -162,12 +254,17 @@ services: - name: cors - name: key-auth config: - hide_credentials: true + hide_credentials: false - name: request-transformer config: add: headers: - - Content-Profile:graphql_public + - "Content-Profile: graphql_public" + - "Authorization: $LUA_AUTH_EXPR" + replace: + headers: + - "Content-Profile: graphql_public" + - "Authorization: $LUA_AUTH_EXPR" - name: acl config: hide_groups_header: true @@ -190,6 +287,14 @@ services: - name: key-auth config: hide_credentials: false + - name: request-transformer + config: + add: + querystring: + - "apikey: $LUA_RT_WS_EXPR" + replace: + querystring: + - "apikey: $LUA_RT_WS_EXPR" - name: acl config: hide_groups_header: true @@ -197,7 +302,7 @@ services: - admin - anon - name: realtime-v1-rest - _comment: 'Realtime: /realtime/v1/* -> ws://realtime:4000/socket/*' + _comment: 'Realtime: /realtime/v1/api/* -> http://realtime:4000/api/*' url: http://realtime-dev:4000/api protocol: http routes: @@ -210,6 +315,14 @@ services: - name: key-auth config: hide_credentials: false + - name: request-transformer + config: + add: + headers: + - "Authorization: $LUA_AUTH_EXPR" + replace: + headers: + - "Authorization: $LUA_AUTH_EXPR" - name: acl config: hide_groups_header: true @@ -217,7 +330,8 @@ services: - admin - anon - ## Storage routes: the storage server manages its own auth + ## Storage API endpoint + ## No key-auth - S3 protocol requests don't carry an apikey header. - name: storage-v1 _comment: 'Storage: /storage/v1/* -> http://supabase-storage:5000/*' connect_timeout: $KONG_STORAGE_CONNECT_TIMEOUT @@ -233,11 +347,20 @@ services: response_buffering: $KONG_STORAGE_RESPONSE_BUFFERING plugins: - name: cors + - name: post-function + config: + access: + - | + local auth = kong.request.get_header("authorization") + if auth == nil or auth == "" or auth:find("^%s*$") then + kong.service.request.clear_header("authorization") + end ## Edge Functions routes - name: functions-v1 _comment: 'Edge Functions: /functions/v1/* -> http://supabase-edge-functions:9000/*' url: http://supabase-edge-functions:9000/ + read_timeout: 150000 routes: - name: functions-v1-all strip_path: true @@ -246,15 +369,28 @@ services: plugins: - name: cors - ## Analytics routes - - name: analytics-v1 - _comment: 'Analytics: /analytics/v1/* -> http://logflare:4000/*' - url: http://supabase-analytics:4000/ + ## OAuth 2.0 Authorization Server Metadata (RFC 8414) + - name: well-known-oauth + _comment: 'Auth: /.well-known/oauth-authorization-server -> http://supabase-auth:9999/.well-known/oauth-authorization-server' + url: http://supabase-auth:9999/.well-known/oauth-authorization-server routes: - - name: analytics-v1-all + - name: well-known-oauth strip_path: true paths: - - /analytics/v1/ + - /.well-known/oauth-authorization-server + plugins: + - name: cors + + ## Analytics routes + ## Not used - Studio and Vector talk directly to analytics via Docker networking. + # - name: analytics-v1 + # _comment: 'Analytics: /analytics/v1/* -> http://logflare:4000/*' + # url: http://supabase-analytics:4000/ + # routes: + # - name: analytics-v1-all + # strip_path: true + # paths: + # - /analytics/v1/ ## Secure Database routes - name: meta @@ -300,10 +436,22 @@ services: paths: - /mcp plugins: + # Block access to /mcp by default - name: request-termination config: status_code: 403 message: "Access is forbidden." + # Enable local access (danger zone!) + # 1. Comment out the 'request-termination' section above + # 2. Uncomment the entire section below, including 'deny' + # 3. Add your local IPs to the 'allow' list + #- name: cors + #- name: ip-restriction + # config: + # allow: + # - 127.0.0.1 + # - ::1 + # deny: [] ## Protected Dashboard - catch all remaining routes - name: dashboard @@ -320,7 +468,7 @@ services: config: hide_credentials: true supabase-studio: - image: supabase/studio:2026.02.16-sha-26c615c + image: supabase/studio:2026.03.16-sha-5528817 healthcheck: test: [ @@ -342,6 +490,9 @@ services: - POSTGRES_HOST=${POSTGRES_HOST:-supabase-db} - POSTGRES_PORT=${POSTGRES_PORT:-5432} - POSTGRES_DB=${POSTGRES_DB:-postgres} + - 'PGRST_DB_SCHEMAS=${PGRST_DB_SCHEMAS:-public,storage,graphql_public}' + - PGRST_DB_MAX_ROWS=${PGRST_DB_MAX_ROWS:-1000} + - PGRST_DB_EXTRA_SEARCH_PATH=${PGRST_DB_EXTRA_SEARCH_PATH:-public} - DEFAULT_ORGANIZATION_NAME=${STUDIO_DEFAULT_ORGANIZATION:-Default Organization} - DEFAULT_PROJECT_NAME=${STUDIO_DEFAULT_PROJECT:-Default Project} @@ -355,7 +506,7 @@ services: - LOGFLARE_API_KEY=${SERVICE_PASSWORD_LOGFLARE} - LOGFLARE_PUBLIC_ACCESS_TOKEN=${SERVICE_PASSWORD_LOGFLARE} - - LOGFLARE_PRIVATE_ACCESS_TOKEN=${SERVICE_PASSWORD_LOGFLARE} + - LOGFLARE_PRIVATE_ACCESS_TOKEN=${SERVICE_PASSWORD_LOGFLAREPRIVATE} - LOGFLARE_URL=http://supabase-analytics:4000 # Next.js client-side environment variables (required for browser access) - 'NEXT_PUBLIC_SUPABASE_URL=${SERVICE_URL_SUPABASEKONG}' @@ -403,7 +554,7 @@ services: source: ./volumes/db/realtime.sql target: /docker-entrypoint-initdb.d/migrations/99-realtime.sql content: | - \set pguser `echo "supabase_admin"` + \set pguser `echo "$POSTGRES_USER"` create schema if not exists _realtime; alter schema _realtime owner to :pguser; @@ -418,7 +569,7 @@ services: source: ./volumes/db/pooler.sql target: /docker-entrypoint-initdb.d/migrations/99-pooler.sql content: | - \set pguser `echo "supabase_admin"` + \set pguser `echo "$POSTGRES_USER"` \c _supabase create schema if not exists _supavisor; alter schema _supavisor owner to :pguser; @@ -662,7 +813,7 @@ services: source: ./volumes/db/logs.sql target: /docker-entrypoint-initdb.d/migrations/99-logs.sql content: | - \set pguser `echo "supabase_admin"` + \set pguser `echo "$POSTGRES_USER"` \c _supabase create schema if not exists _analytics; alter schema _analytics owner to :pguser; @@ -694,7 +845,7 @@ services: - DB_PASSWORD=${SERVICE_PASSWORD_POSTGRES} - DB_SCHEMA=_analytics - LOGFLARE_PUBLIC_ACCESS_TOKEN=${SERVICE_PASSWORD_LOGFLARE} - - LOGFLARE_PRIVATE_ACCESS_TOKEN=${SERVICE_PASSWORD_LOGFLARE} + - LOGFLARE_PRIVATE_ACCESS_TOKEN=${SERVICE_PASSWORD_LOGFLAREPRIVATE} - LOGFLARE_SINGLE_TENANT=true - LOGFLARE_SUPABASE_MODE=true @@ -759,13 +910,13 @@ services: inputs: - project_logs route: - kong: 'starts_with(string!(.appname), "supabase-kong")' - auth: 'starts_with(string!(.appname), "supabase-auth")' - rest: 'starts_with(string!(.appname), "supabase-rest")' - realtime: 'starts_with(string!(.appname), "realtime-dev")' - storage: 'starts_with(string!(.appname), "supabase-storage")' - functions: 'starts_with(string!(.appname), "supabase-edge-functions")' - db: 'starts_with(string!(.appname), "supabase-db")' + kong: 'contains(string!(.appname), "supabase-kong")' + auth: 'contains(string!(.appname), "supabase-auth")' + rest: 'contains(string!(.appname), "supabase-rest")' + realtime: 'contains(string!(.appname), "supabase-realtime")' + storage: 'contains(string!(.appname), "supabase-storage")' + functions: 'contains(string!(.appname), "supabase-edge-functions")' + db: 'contains(string!(.appname), "supabase-db")' # Ignores non nginx errors since they are related with kong booting up kong_logs: type: remap @@ -912,7 +1063,7 @@ services: retry_max_duration_secs: 30 retry_initial_backoff_secs: 1 headers: - x-api-key: '${LOGFLARE_API_KEY?LOGFLARE_API_KEY is required}' + x-api-key: '${LOGFLARE_PUBLIC_ACCESS_TOKEN?LOGFLARE_PUBLIC_ACCESS_TOKEN is required}' uri: 'http://supabase-analytics:4000/api/logs?source_name=gotrue.logs.prod' logflare_realtime: type: 'http' @@ -925,7 +1076,7 @@ services: retry_max_duration_secs: 30 retry_initial_backoff_secs: 1 headers: - x-api-key: '${LOGFLARE_API_KEY?LOGFLARE_API_KEY is required}' + x-api-key: '${LOGFLARE_PUBLIC_ACCESS_TOKEN?LOGFLARE_PUBLIC_ACCESS_TOKEN is required}' uri: 'http://supabase-analytics:4000/api/logs?source_name=realtime.logs.prod' logflare_rest: type: 'http' @@ -938,7 +1089,7 @@ services: retry_max_duration_secs: 30 retry_initial_backoff_secs: 1 headers: - x-api-key: '${LOGFLARE_API_KEY?LOGFLARE_API_KEY is required}' + x-api-key: '${LOGFLARE_PUBLIC_ACCESS_TOKEN?LOGFLARE_PUBLIC_ACCESS_TOKEN is required}' uri: 'http://supabase-analytics:4000/api/logs?source_name=postgREST.logs.prod' logflare_db: type: 'http' @@ -951,7 +1102,7 @@ services: retry_max_duration_secs: 30 retry_initial_backoff_secs: 1 headers: - x-api-key: '${LOGFLARE_API_KEY?LOGFLARE_API_KEY is required}' + x-api-key: '${LOGFLARE_PUBLIC_ACCESS_TOKEN?LOGFLARE_PUBLIC_ACCESS_TOKEN is required}' uri: 'http://supabase-analytics:4000/api/logs?source_name=postgres.logs' logflare_functions: type: 'http' @@ -964,7 +1115,7 @@ services: retry_max_duration_secs: 30 retry_initial_backoff_secs: 1 headers: - x-api-key: '${LOGFLARE_API_KEY?LOGFLARE_API_KEY is required}' + x-api-key: '${LOGFLARE_PUBLIC_ACCESS_TOKEN?LOGFLARE_PUBLIC_ACCESS_TOKEN is required}' uri: 'http://supabase-analytics:4000/api/logs?source_name=deno-relay-logs' logflare_storage: type: 'http' @@ -977,7 +1128,7 @@ services: retry_max_duration_secs: 30 retry_initial_backoff_secs: 1 headers: - x-api-key: '${LOGFLARE_API_KEY?LOGFLARE_API_KEY is required}' + x-api-key: '${LOGFLARE_PUBLIC_ACCESS_TOKEN?LOGFLARE_PUBLIC_ACCESS_TOKEN is required}' uri: 'http://supabase-analytics:4000/api/logs?source_name=storage.logs.prod.2' logflare_kong: type: 'http' @@ -991,16 +1142,16 @@ services: retry_max_duration_secs: 30 retry_initial_backoff_secs: 1 headers: - x-api-key: '${LOGFLARE_API_KEY?LOGFLARE_API_KEY is required}' + x-api-key: '${LOGFLARE_PUBLIC_ACCESS_TOKEN?LOGFLARE_PUBLIC_ACCESS_TOKEN is required}' uri: 'http://supabase-analytics:4000/api/logs?source_name=cloudflare.logs.prod' - /var/run/docker.sock:/var/run/docker.sock:ro environment: - - LOGFLARE_API_KEY=${SERVICE_PASSWORD_LOGFLARE} + - LOGFLARE_PUBLIC_ACCESS_TOKEN=${SERVICE_PASSWORD_LOGFLARE} command: ["--config", "/etc/vector/vector.yml"] supabase-rest: - image: postgrest/postgrest:v14.5 + image: postgrest/postgrest:v14.6 depends_on: supabase-db: # Disable this if you are using an external Postgres database @@ -1010,6 +1161,8 @@ services: environment: - PGRST_DB_URI=postgres://authenticator:${SERVICE_PASSWORD_POSTGRES}@${POSTGRES_HOSTNAME:-supabase-db}:${POSTGRES_PORT:-5432}/${POSTGRES_DB:-postgres} - 'PGRST_DB_SCHEMAS=${PGRST_DB_SCHEMAS:-public,storage,graphql_public}' + - PGRST_DB_MAX_ROWS=${PGRST_DB_MAX_ROWS:-1000} + - PGRST_DB_EXTRA_SEARCH_PATH=${PGRST_DB_EXTRA_SEARCH_PATH:-public} - PGRST_DB_ANON_ROLE=anon - PGRST_JWT_SECRET=${SERVICE_PASSWORD_JWT} - PGRST_DB_USE_LEGACY_GUCS=false @@ -1133,6 +1286,7 @@ services: timeout: 5s interval: 5s retries: 3 + start_period: 10s environment: - PORT=4000 - DB_HOST=${POSTGRES_HOSTNAME:-supabase-db} @@ -1144,6 +1298,7 @@ services: - DB_ENC_KEY=supabaserealtime - API_JWT_SECRET=${SERVICE_PASSWORD_JWT} - SECRET_KEY_BASE=${SECRET_PASSWORD_REALTIME} + - METRICS_JWT_SECRET=${SERVICE_PASSWORD_JWT} - ERL_AFLAGS=-proto_dist inet_tcp - DNS_NODES='' - RLIMIT_NOFILE=10000 @@ -1190,7 +1345,7 @@ services: exit 0 supabase-storage: - image: supabase/storage-api:v1.37.8 + image: supabase/storage-api:v1.44.2 depends_on: supabase-db: # Disable this if you are using an external Postgres database @@ -1241,6 +1396,8 @@ services: - SERVICE_KEY=${SERVICE_SUPABASESERVICE_KEY} - POSTGREST_URL=http://supabase-rest:3000 - PGRST_JWT_SECRET=${SERVICE_PASSWORD_JWT} + - STORAGE_PUBLIC_URL=${SERVICE_URL_SUPABASEKONG} + - TENANT_ID=${STORAGE_TENANT_ID:-stub} volumes: - ./volumes/storage:/var/lib/storage imgproxy: @@ -1254,7 +1411,7 @@ services: - IMGPROXY_BIND=:8080 - IMGPROXY_LOCAL_FILESYSTEM_ROOT=/ - IMGPROXY_USE_ETAG=true - - IMGPROXY_ENABLE_WEBP_DETECTION=${IMGPROXY_ENABLE_WEBP_DETECTION:-true} + - IMGPROXY_AUTO_WEBP=${IMGPROXY_AUTO_WEBP:-true} - IMGPROXY_MAX_SRC_RESOLUTION=16.8 volumes: - ./volumes/storage:/var/lib/storage @@ -1277,7 +1434,7 @@ services: - CRYPTO_KEY=${SERVICE_PASSWORD_PGMETACRYPTO} supabase-edge-functions: - image: supabase/edge-runtime:v1.70.3 + image: supabase/edge-runtime:v1.71.2 depends_on: supabase-analytics: condition: service_healthy @@ -1288,14 +1445,16 @@ services: retries: 3 environment: - JWT_SECRET=${SERVICE_PASSWORD_JWT} - - SUPABASE_URL=${SERVICE_URL_SUPABASEKONG} + - SUPABASE_URL=http://supabase-kong:8000 + - SUPABASE_PUBLIC_URL=${SERVICE_URL_SUPABASEKONG} - SUPABASE_ANON_KEY=${SERVICE_SUPABASEANON_KEY} - SUPABASE_SERVICE_ROLE_KEY=${SERVICE_SUPABASESERVICE_KEY} - SUPABASE_DB_URL=postgresql://postgres:${SERVICE_PASSWORD_POSTGRES}@${POSTGRES_HOSTNAME:-supabase-db}:${POSTGRES_PORT:-5432}/${POSTGRES_DB:-postgres} - # TODO: Allow configuring VERIFY_JWT per function. This PR might help: https://github.com/supabase/cli/pull/786 + # TODO: Allow configuring VERIFY_JWT per function. - VERIFY_JWT=${FUNCTIONS_VERIFY_JWT:-false} volumes: - ./volumes/functions:/home/deno/functions + - deno-cache:/root/.cache/deno - type: bind source: ./volumes/functions/main/index.ts target: /home/deno/functions/main/index.ts @@ -1305,8 +1464,21 @@ services: console.log('main function started') const JWT_SECRET = Deno.env.get('JWT_SECRET') + const SUPABASE_URL = Deno.env.get('SUPABASE_URL') const VERIFY_JWT = Deno.env.get('VERIFY_JWT') === 'true' + // Create JWKS for ES256/RS256 tokens (newer tokens) + let SUPABASE_JWT_KEYS: ReturnType | null = null + if (SUPABASE_URL) { + try { + SUPABASE_JWT_KEYS = jose.createRemoteJWKSet( + new URL('/auth/v1/.well-known/jwks.json', SUPABASE_URL) + ) + } catch (e) { + console.error('Failed to fetch JWKS from SUPABASE_URL:', e) + } + } + function getAuthToken(req: Request) { const authHeader = req.headers.get('authorization') if (!authHeader) { @@ -1319,23 +1491,61 @@ services: return token } - async function verifyJWT(jwt: string): Promise { - const encoder = new TextEncoder() - const secretKey = encoder.encode(JWT_SECRET) - try { - await jose.jwtVerify(jwt, secretKey) - } catch (err) { - console.error(err) + async function isValidLegacyJWT(jwt: string): Promise { + if (!JWT_SECRET) { + console.error('JWT_SECRET not available for HS256 token verification') return false } - return true + + const encoder = new TextEncoder(); + const secretKey = encoder.encode(JWT_SECRET) + + try { + await jose.jwtVerify(jwt, secretKey); + } catch (e) { + console.error('Symmetric Legacy JWT verification error', e); + return false; + } + return true; + } + + async function isValidJWT(jwt: string): Promise { + if (!SUPABASE_JWT_KEYS) { + console.error('JWKS not available for ES256/RS256 token verification') + return false + } + + try { + await jose.jwtVerify(jwt, SUPABASE_JWT_KEYS) + } catch (e) { + console.error('Asymmetric JWT verification error', e); + return false + } + + return true; + } + + async function isValidHybridJWT(jwt: string): Promise { + const { alg: jwtAlgorithm } = jose.decodeProtectedHeader(jwt) + + if (jwtAlgorithm === 'HS256') { + console.log(`Legacy token type detected, attempting ${jwtAlgorithm} verification.`) + + return await isValidLegacyJWT(jwt) + } + + if (jwtAlgorithm === 'ES256' || jwtAlgorithm === 'RS256') { + return await isValidJWT(jwt) + } + + return false; } Deno.serve(async (req: Request) => { if (req.method !== 'OPTIONS' && VERIFY_JWT) { try { const token = getAuthToken(req) - const isValidJWT = await verifyJWT(token) + const isValidJWT = await isValidHybridJWT(token); if (!isValidJWT) { return new Response(JSON.stringify({ msg: 'Invalid JWT' }), { @@ -1430,13 +1640,14 @@ services: timeout: 5s interval: 5s retries: 10 + start_period: 30s depends_on: supabase-db: condition: service_healthy supabase-analytics: condition: service_healthy environment: - - POOLER_TENANT_ID=dev_tenant + - POOLER_TENANT_ID=${POOLER_TENANT_ID:-dev_tenant} - POOLER_POOL_MODE=transaction - POOLER_DEFAULT_POOL_SIZE=${POOLER_DEFAULT_POOL_SIZE:-20} - POOLER_MAX_CLIENT_CONN=${POOLER_MAX_CLIENT_CONN:-100} @@ -1453,10 +1664,20 @@ services: - 'METRICS_JWT_SECRET=${SERVICE_PASSWORD_JWT}' - REGION=local - 'ERL_AFLAGS=-proto_dist inet_tcp' + - 'DB_POOL_SIZE=${POOLER_DB_POOL_SIZE:-5}' + # TLS for downstream connections (fixes Supabase CLI TLS requirement) + - GLOBAL_DOWNSTREAM_CERT_PATH=/etc/ssl/server.crt + - GLOBAL_DOWNSTREAM_KEY_PATH=/etc/ssl/server.key command: - /bin/sh - "-c" - - '/app/bin/migrate && /app/bin/supavisor eval "$$(cat /etc/pooler/pooler.exs)" && /app/bin/server' + - | + if [ ! -f /etc/ssl/server.crt ]; then + openssl req -x509 -nodes -days 3650 -newkey rsa:2048 \ + -keyout /etc/ssl/server.key -out /etc/ssl/server.crt \ + -subj "/CN=supabase-pooler" + fi + /app/bin/migrate && /app/bin/supavisor eval "$$(cat /etc/pooler/pooler.exs)" && /app/bin/server volumes: - type: bind source: ./volumes/pooler/pooler.exs