fix(supabase): comprehensive template update to match official self-hosting setup

- Update Kong to 3.9.1 with new awk-based entrypoint script (replaces fragile eval/echo)
- Add request-transformer plugin to all secure Kong routes for API key translation
- Fix hide_credentials: false on REST and GraphQL routes
- Add post-function plugin on storage route for S3 presigned URL compatibility
- Add opaque API key support (SUPABASE_PUBLISHABLE_KEY, SUPABASE_SECRET_KEY)
- Update Vector router to use contains() matching for Coolify container names
- Add auto-generated self-signed TLS cert for Supavisor (fixes Supabase CLI connectivity)
- Fix logs not queryable in Studio by separating public/private Logflare access tokens
- Update image versions: Kong 3.9.1, Studio 2026.03.16, PostgREST v14.6, Storage v1.44.2, Edge Runtime v1.71.2
- Fix IMGPROXY_ENABLE_WEBP_DETECTION -> IMGPROXY_AUTO_WEBP
- Add deno-cache volume for faster Edge Function cold starts
- Make POOLER_TENANT_ID configurable
- Add start_period to Realtime and Supavisor healthchecks
- Add KONG_PROXY_ACCESS_LOG configuration
- Update SQL init scripts to use $POSTGRES_USER instead of hardcoded supabase_admin
This commit is contained in:
Vadko 2026-04-02 17:25:36 +03:00
parent ecfbe35485
commit 39322dfbb9

View file

@ -8,25 +8,35 @@
services: services:
supabase-kong: supabase-kong:
image: kong:2.8.1 image: kong/kong:3.9.1
# https://unix.stackexchange.com/a/294837 entrypoint: /home/kong/kong-entrypoint.sh
entrypoint: bash -c 'eval "echo \"$$(cat ~/temp.yml)\"" > ~/kong.yml && /docker-entrypoint.sh kong docker-start'
depends_on: depends_on:
supabase-analytics: supabase-analytics:
condition: service_healthy condition: service_healthy
healthcheck:
test: ["CMD", "kong", "health"]
interval: 5s
timeout: 5s
retries: 5
environment: environment:
- SERVICE_URL_SUPABASEKONG_8000 - SERVICE_URL_SUPABASEKONG_8000
- KONG_PORT_MAPS=443:8000 - KONG_PORT_MAPS=443:8000
- JWT_SECRET=${SERVICE_PASSWORD_JWT} - JWT_SECRET=${SERVICE_PASSWORD_JWT}
- KONG_DATABASE=off - 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 # https://github.com/supabase/cli/issues/14
- KONG_DNS_ORDER=LAST,A,CNAME - 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_BUFFER_SIZE=160k
- KONG_NGINX_PROXY_PROXY_BUFFERS=64 160k - KONG_NGINX_PROXY_PROXY_BUFFERS=64 160k
- 'KONG_PROXY_ACCESS_LOG=/dev/stdout combined'
- SUPABASE_ANON_KEY=${SERVICE_SUPABASEANON_KEY} - SUPABASE_ANON_KEY=${SERVICE_SUPABASEANON_KEY}
- SUPABASE_SERVICE_KEY=${SERVICE_SUPABASESERVICE_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_USERNAME=${SERVICE_USER_ADMIN}
- DASHBOARD_PASSWORD=${SERVICE_PASSWORD_ADMIN} - DASHBOARD_PASSWORD=${SERVICE_PASSWORD_ADMIN}
- 'KONG_STORAGE_CONNECT_TIMEOUT=${KONG_STORAGE_CONNECT_TIMEOUT:-60}' - '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_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: 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 # https://github.com/supabase/supabase/issues/12661
- type: bind - type: bind
source: ./volumes/api/kong.yml source: ./volumes/api/kong.yml
@ -51,9 +95,11 @@ services:
- username: anon - username: anon
keyauth_credentials: keyauth_credentials:
- key: $SUPABASE_ANON_KEY - key: $SUPABASE_ANON_KEY
- key: $SUPABASE_PUBLISHABLE_KEY
- username: service_role - username: service_role
keyauth_credentials: keyauth_credentials:
- key: $SUPABASE_SERVICE_KEY - key: $SUPABASE_SERVICE_KEY
- key: $SUPABASE_SECRET_KEY
### ###
### Access Control List ### Access Control List
@ -69,8 +115,8 @@ services:
### ###
basicauth_credentials: basicauth_credentials:
- consumer: DASHBOARD - consumer: DASHBOARD
username: $DASHBOARD_USERNAME username: '$DASHBOARD_USERNAME'
password: $DASHBOARD_PASSWORD password: '$DASHBOARD_PASSWORD'
### ###
@ -106,6 +152,36 @@ services:
- /auth/v1/authorize - /auth/v1/authorize
plugins: plugins:
- name: cors - 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 ## Secure Auth routes
- name: auth-v1 - name: auth-v1
@ -121,6 +197,14 @@ services:
- name: key-auth - name: key-auth
config: config:
hide_credentials: false hide_credentials: false
- name: request-transformer
config:
add:
headers:
- "Authorization: $LUA_AUTH_EXPR"
replace:
headers:
- "Authorization: $LUA_AUTH_EXPR"
- name: acl - name: acl
config: config:
hide_groups_header: true hide_groups_header: true
@ -141,7 +225,15 @@ services:
- name: cors - name: cors
- name: key-auth - name: key-auth
config: 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 - name: acl
config: config:
hide_groups_header: true hide_groups_header: true
@ -162,12 +254,17 @@ services:
- name: cors - name: cors
- name: key-auth - name: key-auth
config: config:
hide_credentials: true hide_credentials: false
- name: request-transformer - name: request-transformer
config: config:
add: add:
headers: 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 - name: acl
config: config:
hide_groups_header: true hide_groups_header: true
@ -190,6 +287,14 @@ services:
- name: key-auth - name: key-auth
config: config:
hide_credentials: false hide_credentials: false
- name: request-transformer
config:
add:
querystring:
- "apikey: $LUA_RT_WS_EXPR"
replace:
querystring:
- "apikey: $LUA_RT_WS_EXPR"
- name: acl - name: acl
config: config:
hide_groups_header: true hide_groups_header: true
@ -197,7 +302,7 @@ services:
- admin - admin
- anon - anon
- name: realtime-v1-rest - 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 url: http://realtime-dev:4000/api
protocol: http protocol: http
routes: routes:
@ -210,6 +315,14 @@ services:
- name: key-auth - name: key-auth
config: config:
hide_credentials: false hide_credentials: false
- name: request-transformer
config:
add:
headers:
- "Authorization: $LUA_AUTH_EXPR"
replace:
headers:
- "Authorization: $LUA_AUTH_EXPR"
- name: acl - name: acl
config: config:
hide_groups_header: true hide_groups_header: true
@ -217,7 +330,8 @@ services:
- admin - admin
- anon - 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 - name: storage-v1
_comment: 'Storage: /storage/v1/* -> http://supabase-storage:5000/*' _comment: 'Storage: /storage/v1/* -> http://supabase-storage:5000/*'
connect_timeout: $KONG_STORAGE_CONNECT_TIMEOUT connect_timeout: $KONG_STORAGE_CONNECT_TIMEOUT
@ -233,11 +347,20 @@ services:
response_buffering: $KONG_STORAGE_RESPONSE_BUFFERING response_buffering: $KONG_STORAGE_RESPONSE_BUFFERING
plugins: plugins:
- name: cors - 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 ## Edge Functions routes
- name: functions-v1 - name: functions-v1
_comment: 'Edge Functions: /functions/v1/* -> http://supabase-edge-functions:9000/*' _comment: 'Edge Functions: /functions/v1/* -> http://supabase-edge-functions:9000/*'
url: http://supabase-edge-functions:9000/ url: http://supabase-edge-functions:9000/
read_timeout: 150000
routes: routes:
- name: functions-v1-all - name: functions-v1-all
strip_path: true strip_path: true
@ -246,15 +369,28 @@ services:
plugins: plugins:
- name: cors - name: cors
## Analytics routes ## OAuth 2.0 Authorization Server Metadata (RFC 8414)
- name: analytics-v1 - name: well-known-oauth
_comment: 'Analytics: /analytics/v1/* -> http://logflare:4000/*' _comment: 'Auth: /.well-known/oauth-authorization-server -> http://supabase-auth:9999/.well-known/oauth-authorization-server'
url: http://supabase-analytics:4000/ url: http://supabase-auth:9999/.well-known/oauth-authorization-server
routes: routes:
- name: analytics-v1-all - name: well-known-oauth
strip_path: true strip_path: true
paths: 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 ## Secure Database routes
- name: meta - name: meta
@ -300,10 +436,22 @@ services:
paths: paths:
- /mcp - /mcp
plugins: plugins:
# Block access to /mcp by default
- name: request-termination - name: request-termination
config: config:
status_code: 403 status_code: 403
message: "Access is forbidden." 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 ## Protected Dashboard - catch all remaining routes
- name: dashboard - name: dashboard
@ -320,7 +468,7 @@ services:
config: config:
hide_credentials: true hide_credentials: true
supabase-studio: supabase-studio:
image: supabase/studio:2026.02.16-sha-26c615c image: supabase/studio:2026.03.16-sha-5528817
healthcheck: healthcheck:
test: test:
[ [
@ -342,6 +490,9 @@ services:
- POSTGRES_HOST=${POSTGRES_HOST:-supabase-db} - POSTGRES_HOST=${POSTGRES_HOST:-supabase-db}
- POSTGRES_PORT=${POSTGRES_PORT:-5432} - POSTGRES_PORT=${POSTGRES_PORT:-5432}
- POSTGRES_DB=${POSTGRES_DB:-postgres} - 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_ORGANIZATION_NAME=${STUDIO_DEFAULT_ORGANIZATION:-Default Organization}
- DEFAULT_PROJECT_NAME=${STUDIO_DEFAULT_PROJECT:-Default Project} - DEFAULT_PROJECT_NAME=${STUDIO_DEFAULT_PROJECT:-Default Project}
@ -355,7 +506,7 @@ services:
- LOGFLARE_API_KEY=${SERVICE_PASSWORD_LOGFLARE} - LOGFLARE_API_KEY=${SERVICE_PASSWORD_LOGFLARE}
- LOGFLARE_PUBLIC_ACCESS_TOKEN=${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 - LOGFLARE_URL=http://supabase-analytics:4000
# Next.js client-side environment variables (required for browser access) # Next.js client-side environment variables (required for browser access)
- 'NEXT_PUBLIC_SUPABASE_URL=${SERVICE_URL_SUPABASEKONG}' - 'NEXT_PUBLIC_SUPABASE_URL=${SERVICE_URL_SUPABASEKONG}'
@ -403,7 +554,7 @@ services:
source: ./volumes/db/realtime.sql source: ./volumes/db/realtime.sql
target: /docker-entrypoint-initdb.d/migrations/99-realtime.sql target: /docker-entrypoint-initdb.d/migrations/99-realtime.sql
content: | content: |
\set pguser `echo "supabase_admin"` \set pguser `echo "$POSTGRES_USER"`
create schema if not exists _realtime; create schema if not exists _realtime;
alter schema _realtime owner to :pguser; alter schema _realtime owner to :pguser;
@ -418,7 +569,7 @@ services:
source: ./volumes/db/pooler.sql source: ./volumes/db/pooler.sql
target: /docker-entrypoint-initdb.d/migrations/99-pooler.sql target: /docker-entrypoint-initdb.d/migrations/99-pooler.sql
content: | content: |
\set pguser `echo "supabase_admin"` \set pguser `echo "$POSTGRES_USER"`
\c _supabase \c _supabase
create schema if not exists _supavisor; create schema if not exists _supavisor;
alter schema _supavisor owner to :pguser; alter schema _supavisor owner to :pguser;
@ -662,7 +813,7 @@ services:
source: ./volumes/db/logs.sql source: ./volumes/db/logs.sql
target: /docker-entrypoint-initdb.d/migrations/99-logs.sql target: /docker-entrypoint-initdb.d/migrations/99-logs.sql
content: | content: |
\set pguser `echo "supabase_admin"` \set pguser `echo "$POSTGRES_USER"`
\c _supabase \c _supabase
create schema if not exists _analytics; create schema if not exists _analytics;
alter schema _analytics owner to :pguser; alter schema _analytics owner to :pguser;
@ -694,7 +845,7 @@ services:
- DB_PASSWORD=${SERVICE_PASSWORD_POSTGRES} - DB_PASSWORD=${SERVICE_PASSWORD_POSTGRES}
- DB_SCHEMA=_analytics - DB_SCHEMA=_analytics
- LOGFLARE_PUBLIC_ACCESS_TOKEN=${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_SINGLE_TENANT=true - LOGFLARE_SINGLE_TENANT=true
- LOGFLARE_SUPABASE_MODE=true - LOGFLARE_SUPABASE_MODE=true
@ -759,13 +910,13 @@ services:
inputs: inputs:
- project_logs - project_logs
route: route:
kong: 'starts_with(string!(.appname), "supabase-kong")' kong: 'contains(string!(.appname), "supabase-kong")'
auth: 'starts_with(string!(.appname), "supabase-auth")' auth: 'contains(string!(.appname), "supabase-auth")'
rest: 'starts_with(string!(.appname), "supabase-rest")' rest: 'contains(string!(.appname), "supabase-rest")'
realtime: 'starts_with(string!(.appname), "realtime-dev")' realtime: 'contains(string!(.appname), "supabase-realtime")'
storage: 'starts_with(string!(.appname), "supabase-storage")' storage: 'contains(string!(.appname), "supabase-storage")'
functions: 'starts_with(string!(.appname), "supabase-edge-functions")' functions: 'contains(string!(.appname), "supabase-edge-functions")'
db: 'starts_with(string!(.appname), "supabase-db")' db: 'contains(string!(.appname), "supabase-db")'
# Ignores non nginx errors since they are related with kong booting up # Ignores non nginx errors since they are related with kong booting up
kong_logs: kong_logs:
type: remap type: remap
@ -912,7 +1063,7 @@ services:
retry_max_duration_secs: 30 retry_max_duration_secs: 30
retry_initial_backoff_secs: 1 retry_initial_backoff_secs: 1
headers: 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' uri: 'http://supabase-analytics:4000/api/logs?source_name=gotrue.logs.prod'
logflare_realtime: logflare_realtime:
type: 'http' type: 'http'
@ -925,7 +1076,7 @@ services:
retry_max_duration_secs: 30 retry_max_duration_secs: 30
retry_initial_backoff_secs: 1 retry_initial_backoff_secs: 1
headers: 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' uri: 'http://supabase-analytics:4000/api/logs?source_name=realtime.logs.prod'
logflare_rest: logflare_rest:
type: 'http' type: 'http'
@ -938,7 +1089,7 @@ services:
retry_max_duration_secs: 30 retry_max_duration_secs: 30
retry_initial_backoff_secs: 1 retry_initial_backoff_secs: 1
headers: 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' uri: 'http://supabase-analytics:4000/api/logs?source_name=postgREST.logs.prod'
logflare_db: logflare_db:
type: 'http' type: 'http'
@ -951,7 +1102,7 @@ services:
retry_max_duration_secs: 30 retry_max_duration_secs: 30
retry_initial_backoff_secs: 1 retry_initial_backoff_secs: 1
headers: 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' uri: 'http://supabase-analytics:4000/api/logs?source_name=postgres.logs'
logflare_functions: logflare_functions:
type: 'http' type: 'http'
@ -964,7 +1115,7 @@ services:
retry_max_duration_secs: 30 retry_max_duration_secs: 30
retry_initial_backoff_secs: 1 retry_initial_backoff_secs: 1
headers: 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' uri: 'http://supabase-analytics:4000/api/logs?source_name=deno-relay-logs'
logflare_storage: logflare_storage:
type: 'http' type: 'http'
@ -977,7 +1128,7 @@ services:
retry_max_duration_secs: 30 retry_max_duration_secs: 30
retry_initial_backoff_secs: 1 retry_initial_backoff_secs: 1
headers: 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' uri: 'http://supabase-analytics:4000/api/logs?source_name=storage.logs.prod.2'
logflare_kong: logflare_kong:
type: 'http' type: 'http'
@ -991,16 +1142,16 @@ services:
retry_max_duration_secs: 30 retry_max_duration_secs: 30
retry_initial_backoff_secs: 1 retry_initial_backoff_secs: 1
headers: 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' uri: 'http://supabase-analytics:4000/api/logs?source_name=cloudflare.logs.prod'
- /var/run/docker.sock:/var/run/docker.sock:ro - /var/run/docker.sock:/var/run/docker.sock:ro
environment: environment:
- LOGFLARE_API_KEY=${SERVICE_PASSWORD_LOGFLARE} - LOGFLARE_PUBLIC_ACCESS_TOKEN=${SERVICE_PASSWORD_LOGFLARE}
command: ["--config", "/etc/vector/vector.yml"] command: ["--config", "/etc/vector/vector.yml"]
supabase-rest: supabase-rest:
image: postgrest/postgrest:v14.5 image: postgrest/postgrest:v14.6
depends_on: depends_on:
supabase-db: supabase-db:
# Disable this if you are using an external Postgres database # Disable this if you are using an external Postgres database
@ -1010,6 +1161,8 @@ services:
environment: environment:
- PGRST_DB_URI=postgres://authenticator:${SERVICE_PASSWORD_POSTGRES}@${POSTGRES_HOSTNAME:-supabase-db}:${POSTGRES_PORT:-5432}/${POSTGRES_DB:-postgres} - 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_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_DB_ANON_ROLE=anon
- PGRST_JWT_SECRET=${SERVICE_PASSWORD_JWT} - PGRST_JWT_SECRET=${SERVICE_PASSWORD_JWT}
- PGRST_DB_USE_LEGACY_GUCS=false - PGRST_DB_USE_LEGACY_GUCS=false
@ -1133,6 +1286,7 @@ services:
timeout: 5s timeout: 5s
interval: 5s interval: 5s
retries: 3 retries: 3
start_period: 10s
environment: environment:
- PORT=4000 - PORT=4000
- DB_HOST=${POSTGRES_HOSTNAME:-supabase-db} - DB_HOST=${POSTGRES_HOSTNAME:-supabase-db}
@ -1144,6 +1298,7 @@ services:
- DB_ENC_KEY=supabaserealtime - DB_ENC_KEY=supabaserealtime
- API_JWT_SECRET=${SERVICE_PASSWORD_JWT} - API_JWT_SECRET=${SERVICE_PASSWORD_JWT}
- SECRET_KEY_BASE=${SECRET_PASSWORD_REALTIME} - SECRET_KEY_BASE=${SECRET_PASSWORD_REALTIME}
- METRICS_JWT_SECRET=${SERVICE_PASSWORD_JWT}
- ERL_AFLAGS=-proto_dist inet_tcp - ERL_AFLAGS=-proto_dist inet_tcp
- DNS_NODES='' - DNS_NODES=''
- RLIMIT_NOFILE=10000 - RLIMIT_NOFILE=10000
@ -1190,7 +1345,7 @@ services:
exit 0 exit 0
supabase-storage: supabase-storage:
image: supabase/storage-api:v1.37.8 image: supabase/storage-api:v1.44.2
depends_on: depends_on:
supabase-db: supabase-db:
# Disable this if you are using an external Postgres database # Disable this if you are using an external Postgres database
@ -1241,6 +1396,8 @@ services:
- SERVICE_KEY=${SERVICE_SUPABASESERVICE_KEY} - SERVICE_KEY=${SERVICE_SUPABASESERVICE_KEY}
- POSTGREST_URL=http://supabase-rest:3000 - POSTGREST_URL=http://supabase-rest:3000
- PGRST_JWT_SECRET=${SERVICE_PASSWORD_JWT} - PGRST_JWT_SECRET=${SERVICE_PASSWORD_JWT}
- STORAGE_PUBLIC_URL=${SERVICE_URL_SUPABASEKONG}
- TENANT_ID=${STORAGE_TENANT_ID:-stub}
volumes: volumes:
- ./volumes/storage:/var/lib/storage - ./volumes/storage:/var/lib/storage
imgproxy: imgproxy:
@ -1254,7 +1411,7 @@ services:
- IMGPROXY_BIND=:8080 - IMGPROXY_BIND=:8080
- IMGPROXY_LOCAL_FILESYSTEM_ROOT=/ - IMGPROXY_LOCAL_FILESYSTEM_ROOT=/
- IMGPROXY_USE_ETAG=true - 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 - IMGPROXY_MAX_SRC_RESOLUTION=16.8
volumes: volumes:
- ./volumes/storage:/var/lib/storage - ./volumes/storage:/var/lib/storage
@ -1277,7 +1434,7 @@ services:
- CRYPTO_KEY=${SERVICE_PASSWORD_PGMETACRYPTO} - CRYPTO_KEY=${SERVICE_PASSWORD_PGMETACRYPTO}
supabase-edge-functions: supabase-edge-functions:
image: supabase/edge-runtime:v1.70.3 image: supabase/edge-runtime:v1.71.2
depends_on: depends_on:
supabase-analytics: supabase-analytics:
condition: service_healthy condition: service_healthy
@ -1288,14 +1445,16 @@ services:
retries: 3 retries: 3
environment: environment:
- JWT_SECRET=${SERVICE_PASSWORD_JWT} - 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_ANON_KEY=${SERVICE_SUPABASEANON_KEY}
- SUPABASE_SERVICE_ROLE_KEY=${SERVICE_SUPABASESERVICE_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} - 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} - VERIFY_JWT=${FUNCTIONS_VERIFY_JWT:-false}
volumes: volumes:
- ./volumes/functions:/home/deno/functions - ./volumes/functions:/home/deno/functions
- deno-cache:/root/.cache/deno
- type: bind - type: bind
source: ./volumes/functions/main/index.ts source: ./volumes/functions/main/index.ts
target: /home/deno/functions/main/index.ts target: /home/deno/functions/main/index.ts
@ -1305,8 +1464,21 @@ services:
console.log('main function started') console.log('main function started')
const JWT_SECRET = Deno.env.get('JWT_SECRET') 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' const VERIFY_JWT = Deno.env.get('VERIFY_JWT') === 'true'
// Create JWKS for ES256/RS256 tokens (newer tokens)
let SUPABASE_JWT_KEYS: ReturnType<typeof jose.createRemoteJWKSet> | 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) { function getAuthToken(req: Request) {
const authHeader = req.headers.get('authorization') const authHeader = req.headers.get('authorization')
if (!authHeader) { if (!authHeader) {
@ -1319,23 +1491,61 @@ services:
return token return token
} }
async function verifyJWT(jwt: string): Promise<boolean> { async function isValidLegacyJWT(jwt: string): Promise<boolean> {
const encoder = new TextEncoder() if (!JWT_SECRET) {
const secretKey = encoder.encode(JWT_SECRET) console.error('JWT_SECRET not available for HS256 token verification')
try {
await jose.jwtVerify(jwt, secretKey)
} catch (err) {
console.error(err)
return false 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<boolean> {
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<boolean> {
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) => { Deno.serve(async (req: Request) => {
if (req.method !== 'OPTIONS' && VERIFY_JWT) { if (req.method !== 'OPTIONS' && VERIFY_JWT) {
try { try {
const token = getAuthToken(req) const token = getAuthToken(req)
const isValidJWT = await verifyJWT(token) const isValidJWT = await isValidHybridJWT(token);
if (!isValidJWT) { if (!isValidJWT) {
return new Response(JSON.stringify({ msg: 'Invalid JWT' }), { return new Response(JSON.stringify({ msg: 'Invalid JWT' }), {
@ -1430,13 +1640,14 @@ services:
timeout: 5s timeout: 5s
interval: 5s interval: 5s
retries: 10 retries: 10
start_period: 30s
depends_on: depends_on:
supabase-db: supabase-db:
condition: service_healthy condition: service_healthy
supabase-analytics: supabase-analytics:
condition: service_healthy condition: service_healthy
environment: environment:
- POOLER_TENANT_ID=dev_tenant - POOLER_TENANT_ID=${POOLER_TENANT_ID:-dev_tenant}
- POOLER_POOL_MODE=transaction - POOLER_POOL_MODE=transaction
- POOLER_DEFAULT_POOL_SIZE=${POOLER_DEFAULT_POOL_SIZE:-20} - POOLER_DEFAULT_POOL_SIZE=${POOLER_DEFAULT_POOL_SIZE:-20}
- POOLER_MAX_CLIENT_CONN=${POOLER_MAX_CLIENT_CONN:-100} - POOLER_MAX_CLIENT_CONN=${POOLER_MAX_CLIENT_CONN:-100}
@ -1453,10 +1664,20 @@ services:
- 'METRICS_JWT_SECRET=${SERVICE_PASSWORD_JWT}' - 'METRICS_JWT_SECRET=${SERVICE_PASSWORD_JWT}'
- REGION=local - REGION=local
- 'ERL_AFLAGS=-proto_dist inet_tcp' - '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: command:
- /bin/sh - /bin/sh
- "-c" - "-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: volumes:
- type: bind - type: bind
source: ./volumes/pooler/pooler.exs source: ./volumes/pooler/pooler.exs