diff --git a/templates/compose/supabase.yaml b/templates/compose/supabase.yaml index fad059a08..df637d732 100644 --- a/templates/compose/supabase.yaml +++ b/templates/compose/supabase.yaml @@ -8,33 +8,77 @@ 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 + - 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}' - 'KONG_STORAGE_WRITE_TIMEOUT=${KONG_STORAGE_WRITE_TIMEOUT:-3600}' - 'KONG_STORAGE_READ_TIMEOUT=${KONG_STORAGE_READ_TIMEOUT:-3600}' - 'KONG_STORAGE_REQUEST_BUFFERING=${KONG_STORAGE_REQUEST_BUFFERING:-false}' - - 'KONG_STORAGE_RESPONSE_BUFFERING=${KONG_STORAGE_RESPONSE_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 @@ -275,6 +411,48 @@ services: allow: - admin + ## Block access to /api/mcp + - name: mcp-blocker + _comment: 'Block direct access to /api/mcp' + url: http://supabase-studio:3000/api/mcp + routes: + - name: mcp-blocker-route + strip_path: true + paths: + - /api/mcp + plugins: + - name: request-termination + config: + status_code: 403 + message: "Access is forbidden." + + ## MCP endpoint - local access + - name: mcp + _comment: 'MCP: /mcp -> http://supabase-studio:3000/api/mcp (local access)' + url: http://supabase-studio:3000/api/mcp + routes: + - name: mcp + strip_path: true + 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 _comment: 'Studio: /* -> http://studio:3000/*' @@ -290,7 +468,7 @@ services: config: hide_credentials: true supabase-studio: - image: supabase/studio:2026.01.07-sha-037e5f9 + image: supabase/studio:2026.03.16-sha-5528817 healthcheck: test: [ @@ -310,7 +488,11 @@ services: - STUDIO_PG_META_URL=http://supabase-meta:8080 - POSTGRES_PASSWORD=${SERVICE_PASSWORD_POSTGRES} - POSTGRES_HOST=${POSTGRES_HOST:-supabase-db} - - CURRENT_CLI_VERSION=2.67.1 + - 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} @@ -320,10 +502,12 @@ services: - SUPABASE_ANON_KEY=${SERVICE_SUPABASEANON_KEY} - SUPABASE_SERVICE_KEY=${SERVICE_SUPABASESERVICE_KEY} - AUTH_JWT_SECRET=${SERVICE_PASSWORD_JWT} + - PG_META_CRYPTO_KEY=${SERVICE_PASSWORD_PGMETACRYPTO} - LOGFLARE_API_KEY=${SERVICE_PASSWORD_LOGFLARE} + - LOGFLARE_PUBLIC_ACCESS_TOKEN=${SERVICE_PASSWORD_LOGFLARE} + - LOGFLARE_PRIVATE_ACCESS_TOKEN=${SERVICE_PASSWORD_LOGFLAREPRIVATE} - LOGFLARE_URL=http://supabase-analytics:4000 - - 'SUPABASE_PUBLIC_API=${SERVICE_URL_SUPABASEKONG}' # Next.js client-side environment variables (required for browser access) - 'NEXT_PUBLIC_SUPABASE_URL=${SERVICE_URL_SUPABASEKONG}' - NEXT_PUBLIC_SUPABASE_ANON_KEY=${SERVICE_SUPABASEANON_KEY} @@ -333,8 +517,13 @@ services: # Uncomment to use Big Query backend for analytics # NEXT_ANALYTICS_BACKEND_PROVIDER=bigquery - 'OPENAI_API_KEY=${OPENAI_API_KEY}' + - SNIPPETS_MANAGEMENT_FOLDER=/app/snippets + - EDGE_FUNCTIONS_MANAGEMENT_FOLDER=/app/edge-functions + volumes: + - ./volumes/snippets:/app/snippets + - ./volumes/functions:/app/edge-functions supabase-db: - image: supabase/postgres:15.8.1.048 + image: supabase/postgres:15.8.1.085 healthcheck: test: pg_isready -U postgres -h 127.0.0.1 interval: 5s @@ -365,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; @@ -380,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; @@ -624,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; @@ -633,7 +822,7 @@ services: - supabase-db-config:/etc/postgresql-custom supabase-analytics: - image: supabase/logflare:1.4.0 + image: supabase/logflare:1.31.2 healthcheck: test: ["CMD", "curl", "http://127.0.0.1:4000/health"] timeout: 5s @@ -655,11 +844,10 @@ services: - DB_PORT=${POSTGRES_PORT:-5432} - DB_PASSWORD=${SERVICE_PASSWORD_POSTGRES} - DB_SCHEMA=_analytics - - LOGFLARE_API_KEY=${SERVICE_PASSWORD_LOGFLARE} + - LOGFLARE_PUBLIC_ACCESS_TOKEN=${SERVICE_PASSWORD_LOGFLARE} + - LOGFLARE_PRIVATE_ACCESS_TOKEN=${SERVICE_PASSWORD_LOGFLAREPRIVATE} - LOGFLARE_SINGLE_TENANT=true - - LOGFLARE_SINGLE_TENANT_MODE=true - LOGFLARE_SUPABASE_MODE=true - - LOGFLARE_MIN_CLUSTER_SIZE=1 # Comment variables to use Big Query backend for analytics - POSTGRES_BACKEND_URL=postgresql://supabase_admin:${SERVICE_PASSWORD_POSTGRES}@${POSTGRES_HOSTNAME:-supabase-db}:${POSTGRES_PORT:-5432}/_supabase @@ -670,7 +858,7 @@ services: # GOOGLE_PROJECT_ID=${GOOGLE_PROJECT_ID} # GOOGLE_PROJECT_NUMBER=${GOOGLE_PROJECT_NUMBER} supabase-vector: - image: timberio/vector:0.28.1-alpine + image: timberio/vector:0.53.0-alpine healthcheck: test: [ @@ -722,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-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 @@ -741,10 +929,13 @@ services: .metadata.request.headers.referer = req.referer .metadata.request.headers.user_agent = req.agent .metadata.request.headers.cf_connecting_ip = req.client - .metadata.request.method = req.method - .metadata.request.path = req.path - .metadata.request.protocol = req.protocol .metadata.response.status_code = req.status + url, split_err = split(req.request, " ") + if split_err == null { + .metadata.request.method = url[0] + .metadata.request.path = url[1] + .metadata.request.protocol = url[2] + } } if err != null { abort @@ -793,14 +984,20 @@ services: parsed, err = parse_regex(.event_message, r'^(?P