diff --git a/templates/compose/supabase.yaml b/templates/compose/supabase.yaml index fad059a08..86268158b 100644 --- a/templates/compose/supabase.yaml +++ b/templates/compose/supabase.yaml @@ -22,7 +22,7 @@ services: - KONG_DECLARATIVE_CONFIG=/home/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_PLUGINS=request-transformer,cors,key-auth,acl,basic-auth,request-termination - KONG_NGINX_PROXY_PROXY_BUFFER_SIZE=160k - KONG_NGINX_PROXY_PROXY_BUFFERS=64 160k - SUPABASE_ANON_KEY=${SERVICE_SUPABASEANON_KEY} @@ -33,7 +33,7 @@ services: - '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: # https://github.com/supabase/supabase/issues/12661 - type: bind @@ -275,6 +275,36 @@ 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: + - name: request-termination + config: + status_code: 403 + message: "Access is forbidden." + ## Protected Dashboard - catch all remaining routes - name: dashboard _comment: 'Studio: /* -> http://studio:3000/*' @@ -290,7 +320,7 @@ services: config: hide_credentials: true supabase-studio: - image: supabase/studio:2026.01.07-sha-037e5f9 + image: supabase/studio:2026.02.16-sha-26c615c healthcheck: test: [ @@ -310,7 +340,8 @@ 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} - DEFAULT_ORGANIZATION_NAME=${STUDIO_DEFAULT_ORGANIZATION:-Default Organization} - DEFAULT_PROJECT_NAME=${STUDIO_DEFAULT_PROJECT:-Default Project} @@ -320,10 +351,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_LOGFLARE_PRIVATE} - 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 +366,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 @@ -359,6 +397,7 @@ services: - POSTGRES_DB=${POSTGRES_DB:-postgres} - JWT_SECRET=${SERVICE_PASSWORD_JWT} - JWT_EXP=${JWT_EXPIRY:-3600} + - ANALYTICS_RETENTION_DAYS=${ANALYTICS_RETENTION_DAYS:-7} volumes: - supabase-db-data:/var/lib/postgresql/data - type: bind @@ -628,12 +667,55 @@ services: \c _supabase create schema if not exists _analytics; alter schema _analytics owner to :pguser; + + -- Analytics log retention cleanup function + -- Deletes log_events older than app.analytics_retention_days (default: 7 days) + CREATE OR REPLACE FUNCTION _analytics.cleanup_old_logs() + RETURNS void AS $$ + DECLARE + tbl text; + days int; + BEGIN + days := COALESCE(nullif(current_setting('app.analytics_retention_days', true), '')::int, 7); + FOR tbl IN + SELECT tablename FROM pg_tables + WHERE schemaname = '_analytics' AND tablename LIKE 'log_events_%' + LOOP + EXECUTE format( + 'DELETE FROM _analytics.%I WHERE timestamp < NOW() - INTERVAL ''%s days''', + tbl, days + ); + END LOOP; + END; + $$ LANGUAGE plpgsql SECURITY DEFINER; + \c postgres + - type: bind + source: ./volumes/db/analytics-retention.sh + target: /docker-entrypoint-initdb.d/init-scripts/99-analytics-retention.sh + content: | + #!/bin/bash + set -e + DAYS=${ANALYTICS_RETENTION_DAYS:-7} + + # Set retention days as a Postgres custom setting + psql -v ON_ERROR_STOP=1 -U "$POSTGRES_USER" -d "_supabase" \ + -c "ALTER DATABASE _supabase SET app.analytics_retention_days TO '$DAYS';" + + # Schedule nightly cleanup at 03:00 UTC using pg_cron + psql -v ON_ERROR_STOP=1 -U "$POSTGRES_USER" -d "postgres" -c " + SELECT cron.schedule_in_database( + 'cleanup-analytics-logs', + '0 3 * * *', + 'SELECT _analytics.cleanup_old_logs()', + '_supabase' + ) ON CONFLICT DO NOTHING; + " || true # Use named volume to persist pgsodium decryption key between restarts - 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 +737,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_LOGFLARE_PRIVATE} - 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 +751,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: [ @@ -727,7 +808,7 @@ services: 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")' + functions: 'starts_with(string!(.appname), "supabase-edge-functions")' db: 'starts_with(string!(.appname), "supabase-db")' # Ignores non nginx errors since they are related with kong booting up kong_logs: @@ -741,10 +822,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 +877,20 @@ services: parsed, err = parse_regex(.event_message, r'^(?P