mirror of
https://github.com/Dokploy/templates.git
synced 2026-07-09 16:05:23 +02:00
* Update Supabase Docker configuration and template - Updated Supabase Studio and GoTrue images to the latest versions. - Modified healthcheck commands for the Studio service. - Enhanced environment variable configuration in docker-compose.yml for better flexibility. - Updated Logflare API key references and added new S3 protocol access keys in template.toml. - Introduced custom entrypoint script for Kong to handle Lua expressions and environment variable substitution. - Added new routes and plugins for enhanced authentication and authorization in Kong configuration. * Refactor Supabase Docker Compose and Template Configuration - Removed container name prefixes from services in docker-compose.yml for consistency. - Increased healthcheck retries and start periods for several services to improve stability. - Updated healthcheck timeout and retry configurations for the analytics service. - Adjusted appname generation logic in template.toml to ensure proper naming for services. - Cleaned up commented sections in template.toml for clarity.
1315 lines
39 KiB
TOML
1315 lines
39 KiB
TOML
[variables]
|
|
main_domain = "${domain}"
|
|
postgres_password = "${password:32}"
|
|
dashboard_password = "${password:32}"
|
|
logflare_public_access_token = "${password:32}"
|
|
logflare_private_access_token = "${password:32}"
|
|
pg_meta_crypto_key = "${password:32}"
|
|
s3_protocol_access_key_id = "${password:24}"
|
|
s3_protocol_access_key_secret = "${password:48}"
|
|
secret_key_base = "${password:64}"
|
|
vault_enc_key = "${password:32}"
|
|
jwt_secret = "${password:32}"
|
|
pooler_tenant_id = "${uuid}"
|
|
anon_key_payload = """{
|
|
"role": "anon",
|
|
"iss": "supabase",
|
|
"exp": ${timestamps:2030-01-01T00:00:00Z}
|
|
}
|
|
"""
|
|
service_role_key_payload = """{
|
|
"role": "service_role",
|
|
"iss": "supabase",
|
|
"exp": ${timestamps:2030-01-01T00:00:00Z}
|
|
}
|
|
"""
|
|
|
|
[[config.domains]]
|
|
serviceName = "kong"
|
|
port = 8_000
|
|
host = "${main_domain}"
|
|
|
|
[config]
|
|
env = [
|
|
'############',
|
|
'# To get a proper working configuration you should at least take a look at:',
|
|
'# - SUPABASE_PUBLIC_URL, API_EXTERNAL_URL should point to your supabase domain with correct http/https scheme',
|
|
'# - SMTP_* are required for auth mail sending',
|
|
'# - ADDITIONAL_REDIRECT_URLS, SITE_URL should point to application using supabase for authentication',
|
|
'# They are used for redirecting after login/signup and gotrue will check them before sending emails',
|
|
'# - POSTGRES_PORT, POOLER_PROXY_PORT_TRANSACTION should be changed if you are already running other instances of supabase',
|
|
'############',
|
|
'',
|
|
'############',
|
|
'# Secrets',
|
|
'# YOU MUST CHANGE THESE BEFORE GOING INTO PRODUCTION',
|
|
'# https://supabase.com/docs/guides/self-hosting/docker#securing-your-services',
|
|
'# In this version of the template they are generated randomly by dokploy helpers',
|
|
'# so you do not need to change them manually',
|
|
'# Go to https://supabase.com/docs/guides/self-hosting for more information',
|
|
'############',
|
|
'',
|
|
'SUPABASE_HOST=${main_domain}',
|
|
'POSTGRES_PASSWORD=${postgres_password}',
|
|
'JWT_SECRET=${jwt_secret}',
|
|
'ANON_KEY=${jwt:jwt_secret:anon_key_payload}',
|
|
'SERVICE_ROLE_KEY=${jwt:jwt_secret:service_role_key_payload}',
|
|
'DASHBOARD_USERNAME=supabase',
|
|
'DASHBOARD_PASSWORD=${dashboard_password}',
|
|
'SECRET_KEY_BASE=${secret_key_base}',
|
|
'VAULT_ENC_KEY=${vault_enc_key}',
|
|
'PG_META_CRYPTO_KEY=${pg_meta_crypto_key}',
|
|
'LOGFLARE_PUBLIC_ACCESS_TOKEN=${logflare_public_access_token}',
|
|
'LOGFLARE_PRIVATE_ACCESS_TOKEN=${logflare_private_access_token}',
|
|
'',
|
|
'',
|
|
'############',
|
|
'# Database - You can change these to any PostgreSQL database that has logical replication enabled.',
|
|
'############',
|
|
'',
|
|
'POSTGRES_HOST=db',
|
|
'POSTGRES_DB=postgres',
|
|
'POSTGRES_PORT=5432',
|
|
'POSTGRES_USER=postgres',
|
|
'',
|
|
'',
|
|
'############',
|
|
'# Supavisor -- Database pooler',
|
|
'############',
|
|
'POOLER_PROXY_PORT_TRANSACTION=6543',
|
|
'POOLER_DEFAULT_POOL_SIZE=20',
|
|
'POOLER_MAX_CLIENT_CONN=100',
|
|
'POOLER_TENANT_ID=${pooler_tenant_id}',
|
|
'POOLER_DB_POOL_SIZE=5',
|
|
'',
|
|
'',
|
|
'############',
|
|
'# API Proxy - Configuration for the Kong Reverse proxy.',
|
|
'# Following ports should not be changed for a dokploy config unless you know what you are doing.',
|
|
'############',
|
|
'',
|
|
'KONG_HTTP_PORT=8000',
|
|
'KONG_HTTPS_PORT=8443',
|
|
'',
|
|
'',
|
|
'############',
|
|
'# API - Configuration for PostgREST.',
|
|
'############',
|
|
'',
|
|
'PGRST_DB_SCHEMAS=public,storage,graphql_public',
|
|
'PGRST_DB_MAX_ROWS=1000',
|
|
'PGRST_DB_EXTRA_SEARCH_PATH=public',
|
|
'',
|
|
'',
|
|
'############',
|
|
'# Auth - Configuration for the GoTrue authentication server.',
|
|
'############',
|
|
'',
|
|
'## General',
|
|
'SITE_URL=http://localhost:3000',
|
|
'ADDITIONAL_REDIRECT_URLS=https://${main_domain}/*,http://localhost:3000/*',
|
|
'JWT_EXPIRY=3600',
|
|
'DISABLE_SIGNUP=false',
|
|
'API_EXTERNAL_URL=https://${main_domain}',
|
|
'JWT_KEYS=[]',
|
|
'',
|
|
'## Mailer Config',
|
|
'MAILER_URLPATHS_CONFIRMATION="/auth/v1/verify"',
|
|
'MAILER_URLPATHS_INVITE="/auth/v1/verify"',
|
|
'MAILER_URLPATHS_RECOVERY="/auth/v1/verify"',
|
|
'MAILER_URLPATHS_EMAIL_CHANGE="/auth/v1/verify"',
|
|
'',
|
|
'## Email auth',
|
|
'ENABLE_EMAIL_SIGNUP=true',
|
|
'ENABLE_EMAIL_AUTOCONFIRM=false',
|
|
'SMTP_ADMIN_EMAIL=admin@example.com',
|
|
'SMTP_HOST=supabase-mail',
|
|
'SMTP_PORT=2500',
|
|
'SMTP_USER=fake_mail_user',
|
|
'SMTP_PASS=fake_mail_password',
|
|
'SMTP_SENDER_NAME=fake_sender',
|
|
'ENABLE_ANONYMOUS_USERS=false',
|
|
'',
|
|
'## Phone auth',
|
|
'ENABLE_PHONE_SIGNUP=true',
|
|
'ENABLE_PHONE_AUTOCONFIRM=true',
|
|
'',
|
|
'',
|
|
'############',
|
|
'# Studio - Configuration for the Dashboard',
|
|
'############',
|
|
'',
|
|
'STUDIO_DEFAULT_ORGANIZATION=Default Organization',
|
|
'STUDIO_DEFAULT_PROJECT=Default Project',
|
|
'',
|
|
'SUPABASE_PUBLIC_URL=https://${main_domain}',
|
|
'',
|
|
'############',
|
|
'# Storage - local file backend (default). Edit docker-compose.yml storage env for S3/MinIO.',
|
|
'############',
|
|
'',
|
|
'GLOBAL_S3_BUCKET=stub',
|
|
'REGION=stub',
|
|
'STORAGE_TENANT_ID=stub',
|
|
'S3_PROTOCOL_ACCESS_KEY_ID=${s3_protocol_access_key_id}',
|
|
'S3_PROTOCOL_ACCESS_KEY_SECRET=${s3_protocol_access_key_secret}',
|
|
'',
|
|
'IMGPROXY_AUTO_WEBP=true',
|
|
'',
|
|
'# Add your OpenAI API key to enable SQL Editor Assistant',
|
|
'OPENAI_API_KEY=',
|
|
'',
|
|
'',
|
|
'############',
|
|
'# Functions - Configuration for Functions',
|
|
'############',
|
|
'# NOTE: VERIFY_JWT applies to all functions. Per-function VERIFY_JWT is not supported yet.',
|
|
'FUNCTIONS_VERIFY_JWT=false',
|
|
'',
|
|
'',
|
|
'############',
|
|
'# Logs - Configuration for Logflare',
|
|
'############',
|
|
'',
|
|
'# Docker socket location - this value will differ depending on your OS',
|
|
'DOCKER_SOCKET_LOCATION=/var/run/docker.sock',
|
|
'',
|
|
'# Google Cloud Project details',
|
|
'GOOGLE_PROJECT_ID=GOOGLE_PROJECT_ID',
|
|
'GOOGLE_PROJECT_NUMBER=GOOGLE_PROJECT_NUMBER']
|
|
|
|
[[config.mounts]]
|
|
filePath = "/volumes/api/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
|
|
"""
|
|
|
|
[[config.mounts]]
|
|
filePath = "/volumes/api/kong.yml"
|
|
content = """_format_version: '2.1'
|
|
_transform: true
|
|
|
|
###
|
|
### Consumers / Users
|
|
###
|
|
consumers:
|
|
- username: DASHBOARD
|
|
- 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
|
|
###
|
|
acls:
|
|
- consumer: anon
|
|
group: anon
|
|
- consumer: service_role
|
|
group: admin
|
|
|
|
###
|
|
### Dashboard credentials
|
|
###
|
|
basicauth_credentials:
|
|
- consumer: DASHBOARD
|
|
username: '$DASHBOARD_USERNAME'
|
|
password: '$DASHBOARD_PASSWORD'
|
|
|
|
###
|
|
### API Routes
|
|
###
|
|
services:
|
|
## Open Auth routes
|
|
- name: auth-v1-open
|
|
url: http://auth:9999/verify
|
|
routes:
|
|
- name: auth-v1-open
|
|
strip_path: true
|
|
paths:
|
|
- /auth/v1/verify
|
|
plugins:
|
|
- name: cors
|
|
- name: auth-v1-open-callback
|
|
url: http://auth:9999/callback
|
|
routes:
|
|
- name: auth-v1-open-callback
|
|
strip_path: true
|
|
paths:
|
|
- /auth/v1/callback
|
|
plugins:
|
|
- name: cors
|
|
- name: auth-v1-open-authorize
|
|
url: http://auth:9999/authorize
|
|
routes:
|
|
- name: auth-v1-open-authorize
|
|
strip_path: true
|
|
paths:
|
|
- /auth/v1/authorize
|
|
plugins:
|
|
- name: cors
|
|
- name: auth-v1-open-jwks
|
|
url: http://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://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://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
|
|
_comment: 'GoTrue: /auth/v1/* -> http://auth:9999/*'
|
|
url: http://auth:9999/
|
|
routes:
|
|
- name: auth-v1-all
|
|
strip_path: true
|
|
paths:
|
|
- /auth/v1/
|
|
plugins:
|
|
- name: cors
|
|
- 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
|
|
allow:
|
|
- admin
|
|
- anon
|
|
|
|
## Secure REST routes
|
|
- name: rest-v1
|
|
_comment: 'PostgREST: /rest/v1/* -> http://rest:3000/*'
|
|
url: http://rest:3000/
|
|
routes:
|
|
- name: rest-v1-all
|
|
strip_path: true
|
|
paths:
|
|
- /rest/v1/
|
|
plugins:
|
|
- name: cors
|
|
- 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
|
|
allow:
|
|
- admin
|
|
- anon
|
|
|
|
## Secure GraphQL routes
|
|
- name: graphql-v1
|
|
_comment: 'PostgREST: /graphql/v1/* -> http://rest:3000/rpc/graphql'
|
|
url: http://rest:3000/rpc/graphql
|
|
routes:
|
|
- name: graphql-v1-all
|
|
strip_path: true
|
|
paths:
|
|
- /graphql/v1
|
|
plugins:
|
|
- name: cors
|
|
- name: key-auth
|
|
config:
|
|
hide_credentials: true
|
|
- name: request-transformer
|
|
config:
|
|
add:
|
|
headers:
|
|
- "Content-Profile: graphql_public"
|
|
- "Authorization: $LUA_AUTH_EXPR"
|
|
replace:
|
|
headers:
|
|
- "Authorization: $LUA_AUTH_EXPR"
|
|
- name: acl
|
|
config:
|
|
hide_groups_header: true
|
|
allow:
|
|
- admin
|
|
- anon
|
|
|
|
## Secure Realtime routes
|
|
- name: realtime-v1-ws
|
|
_comment: 'Realtime: /realtime/v1/* -> ws://realtime:4000/socket/*'
|
|
url: http://realtime:4000/socket
|
|
protocol: ws
|
|
routes:
|
|
- name: realtime-v1-ws
|
|
strip_path: true
|
|
paths:
|
|
- /realtime/v1/
|
|
plugins:
|
|
- name: cors
|
|
- name: key-auth
|
|
config:
|
|
hide_credentials: false
|
|
- name: request-transformer
|
|
config:
|
|
add:
|
|
headers:
|
|
- "x-api-key:$LUA_RT_WS_EXPR"
|
|
replace:
|
|
querystring:
|
|
- "apikey:$LUA_RT_WS_EXPR"
|
|
- name: acl
|
|
config:
|
|
hide_groups_header: true
|
|
allow:
|
|
- admin
|
|
- anon
|
|
- name: realtime-v1-rest
|
|
_comment: 'Realtime: /realtime/v1/api/* -> http://realtime:4000/api/*'
|
|
url: http://realtime:4000/api
|
|
protocol: http
|
|
routes:
|
|
- name: realtime-v1-rest
|
|
strip_path: true
|
|
paths:
|
|
- /realtime/v1/api
|
|
plugins:
|
|
- name: cors
|
|
- 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
|
|
allow:
|
|
- admin
|
|
- anon
|
|
|
|
## Storage routes
|
|
- name: storage-v1
|
|
_comment: 'Storage: /storage/v1/* -> http://storage:5000/*'
|
|
url: http://storage:5000/
|
|
routes:
|
|
- name: storage-v1-all
|
|
strip_path: true
|
|
paths:
|
|
- /storage/v1/
|
|
plugins:
|
|
- name: cors
|
|
- name: request-transformer
|
|
config:
|
|
add:
|
|
headers:
|
|
- "Authorization: $LUA_AUTH_EXPR"
|
|
replace:
|
|
headers:
|
|
- "Authorization: $LUA_AUTH_EXPR"
|
|
- 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://functions:9000/*'
|
|
url: http://functions:9000/
|
|
read_timeout: 150000
|
|
routes:
|
|
- name: functions-v1-all
|
|
strip_path: true
|
|
paths:
|
|
- /functions/v1/
|
|
plugins:
|
|
- name: cors
|
|
|
|
## OAuth 2.0 Authorization Server Metadata
|
|
- name: well-known-oauth
|
|
url: http://auth:9999/.well-known/oauth-authorization-server
|
|
routes:
|
|
- name: well-known-oauth
|
|
strip_path: true
|
|
paths:
|
|
- /.well-known/oauth-authorization-server
|
|
plugins:
|
|
- name: cors
|
|
|
|
## Secure Database routes
|
|
- name: meta
|
|
_comment: 'pg-meta: /pg/* -> http://pg-meta:8080/*'
|
|
url: http://meta:8080/
|
|
routes:
|
|
- name: meta-all
|
|
strip_path: true
|
|
paths:
|
|
- /pg/
|
|
plugins:
|
|
- name: key-auth
|
|
config:
|
|
hide_credentials: false
|
|
- name: acl
|
|
config:
|
|
hide_groups_header: true
|
|
allow:
|
|
- admin
|
|
|
|
- name: mcp-blocker
|
|
url: http://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."
|
|
|
|
- name: mcp
|
|
url: http://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/*'
|
|
url: http://studio:3000/
|
|
routes:
|
|
- name: dashboard-all
|
|
strip_path: true
|
|
paths:
|
|
- /
|
|
plugins:
|
|
- name: cors
|
|
- name: basic-auth
|
|
config:
|
|
hide_credentials: true
|
|
"""
|
|
|
|
[[config.mounts]]
|
|
filePath = "/volumes/snippets/.gitkeep"
|
|
content = ""
|
|
|
|
[[config.mounts]]
|
|
filePath = "/volumes/db/init/data.sql"
|
|
content = ""
|
|
|
|
[[config.mounts]]
|
|
filePath = "/volumes/db/_supabase.sql"
|
|
content = """\\set pguser `echo "$POSTGRES_USER"`
|
|
|
|
CREATE DATABASE _supabase WITH OWNER :pguser;
|
|
"""
|
|
|
|
[[config.mounts]]
|
|
filePath = "/volumes/db/jwt.sql"
|
|
content = """
|
|
\\set jwt_secret `echo "$JWT_SECRET"`
|
|
\\set jwt_exp `echo "$JWT_EXP"`
|
|
|
|
ALTER DATABASE postgres SET "app.settings.jwt_secret" TO :'jwt_secret';
|
|
ALTER DATABASE postgres SET "app.settings.jwt_exp" TO :'jwt_exp';
|
|
"""
|
|
|
|
[[config.mounts]]
|
|
filePath = "/volumes/db/logs.sql"
|
|
content = """
|
|
\\set pguser `echo "$POSTGRES_USER"`
|
|
|
|
\\c _supabase
|
|
create schema if not exists _analytics;
|
|
alter schema _analytics owner to :pguser;
|
|
\\c postgres
|
|
"""
|
|
|
|
[[config.mounts]]
|
|
filePath = "/volumes/db/pooler.sql"
|
|
content = """
|
|
\\set pguser `echo "$POSTGRES_USER"`
|
|
|
|
\\c _supabase
|
|
create schema if not exists _supavisor;
|
|
alter schema _supavisor owner to :pguser;
|
|
\\c postgres
|
|
"""
|
|
|
|
[[config.mounts]]
|
|
filePath = "/volumes/db/realtime.sql"
|
|
content = """
|
|
\\set pguser `echo "$POSTGRES_USER"`
|
|
|
|
create schema if not exists _realtime;
|
|
alter schema _realtime owner to :pguser;
|
|
"""
|
|
|
|
[[config.mounts]]
|
|
filePath = "/volumes/db/roles.sql"
|
|
content = """
|
|
-- NOTE: change to your own passwords for production environments
|
|
\\set pgpass `echo "$POSTGRES_PASSWORD"`
|
|
|
|
ALTER USER authenticator WITH PASSWORD :'pgpass';
|
|
ALTER USER pgbouncer WITH PASSWORD :'pgpass';
|
|
ALTER USER supabase_auth_admin WITH PASSWORD :'pgpass';
|
|
ALTER USER supabase_functions_admin WITH PASSWORD :'pgpass';
|
|
ALTER USER supabase_storage_admin WITH PASSWORD :'pgpass';
|
|
"""
|
|
|
|
[[config.mounts]]
|
|
filePath = "/volumes/db/webhooks.sql"
|
|
content = """
|
|
BEGIN;
|
|
-- Create pg_net extension
|
|
CREATE EXTENSION IF NOT EXISTS pg_net SCHEMA extensions;
|
|
-- Create supabase_functions schema
|
|
CREATE SCHEMA supabase_functions AUTHORIZATION supabase_admin;
|
|
GRANT USAGE ON SCHEMA supabase_functions TO postgres, anon, authenticated, service_role;
|
|
ALTER DEFAULT PRIVILEGES IN SCHEMA supabase_functions GRANT ALL ON TABLES TO postgres, anon, authenticated, service_role;
|
|
ALTER DEFAULT PRIVILEGES IN SCHEMA supabase_functions GRANT ALL ON FUNCTIONS TO postgres, anon, authenticated, service_role;
|
|
ALTER DEFAULT PRIVILEGES IN SCHEMA supabase_functions GRANT ALL ON SEQUENCES TO postgres, anon, authenticated, service_role;
|
|
-- supabase_functions.migrations definition
|
|
CREATE TABLE supabase_functions.migrations (
|
|
version text PRIMARY KEY,
|
|
inserted_at timestamptz NOT NULL DEFAULT NOW()
|
|
);
|
|
-- Initial supabase_functions migration
|
|
INSERT INTO supabase_functions.migrations (version) VALUES ('initial');
|
|
-- supabase_functions.hooks definition
|
|
CREATE TABLE supabase_functions.hooks (
|
|
id bigserial PRIMARY KEY,
|
|
hook_table_id integer NOT NULL,
|
|
hook_name text NOT NULL,
|
|
created_at timestamptz NOT NULL DEFAULT NOW(),
|
|
request_id bigint
|
|
);
|
|
CREATE INDEX supabase_functions_hooks_request_id_idx ON supabase_functions.hooks USING btree (request_id);
|
|
CREATE INDEX supabase_functions_hooks_h_table_id_h_name_idx ON supabase_functions.hooks USING btree (hook_table_id, hook_name);
|
|
COMMENT ON TABLE supabase_functions.hooks IS 'Supabase Functions Hooks: Audit trail for triggered hooks.';
|
|
CREATE FUNCTION supabase_functions.http_request()
|
|
RETURNS trigger
|
|
LANGUAGE plpgsql
|
|
AS $function$
|
|
DECLARE
|
|
request_id bigint;
|
|
payload jsonb;
|
|
url text := TG_ARGV[0]::text;
|
|
method text := TG_ARGV[1]::text;
|
|
headers jsonb DEFAULT '{}'::jsonb;
|
|
params jsonb DEFAULT '{}'::jsonb;
|
|
timeout_ms integer DEFAULT 1000;
|
|
BEGIN
|
|
IF url IS NULL OR url = 'null' THEN
|
|
RAISE EXCEPTION 'url argument is missing';
|
|
END IF;
|
|
|
|
IF method IS NULL OR method = 'null' THEN
|
|
RAISE EXCEPTION 'method argument is missing';
|
|
END IF;
|
|
|
|
IF TG_ARGV[2] IS NULL OR TG_ARGV[2] = 'null' THEN
|
|
headers = '{"Content-Type": "application/json"}'::jsonb;
|
|
ELSE
|
|
headers = TG_ARGV[2]::jsonb;
|
|
END IF;
|
|
|
|
IF TG_ARGV[3] IS NULL OR TG_ARGV[3] = 'null' THEN
|
|
params = '{}'::jsonb;
|
|
ELSE
|
|
params = TG_ARGV[3]::jsonb;
|
|
END IF;
|
|
|
|
IF TG_ARGV[4] IS NULL OR TG_ARGV[4] = 'null' THEN
|
|
timeout_ms = 1000;
|
|
ELSE
|
|
timeout_ms = TG_ARGV[4]::integer;
|
|
END IF;
|
|
|
|
CASE
|
|
WHEN method = 'GET' THEN
|
|
SELECT http_get INTO request_id FROM net.http_get(
|
|
url,
|
|
params,
|
|
headers,
|
|
timeout_ms
|
|
);
|
|
WHEN method = 'POST' THEN
|
|
payload = jsonb_build_object(
|
|
'old_record', OLD,
|
|
'record', NEW,
|
|
'type', TG_OP,
|
|
'table', TG_TABLE_NAME,
|
|
'schema', TG_TABLE_SCHEMA
|
|
);
|
|
|
|
SELECT http_post INTO request_id FROM net.http_post(
|
|
url,
|
|
payload,
|
|
params,
|
|
headers,
|
|
timeout_ms
|
|
);
|
|
ELSE
|
|
RAISE EXCEPTION 'method argument % is invalid', method;
|
|
END CASE;
|
|
|
|
INSERT INTO supabase_functions.hooks
|
|
(hook_table_id, hook_name, request_id)
|
|
VALUES
|
|
(TG_RELID, TG_NAME, request_id);
|
|
|
|
RETURN NEW;
|
|
END
|
|
$function$;
|
|
-- Supabase super admin
|
|
DO
|
|
$$
|
|
BEGIN
|
|
IF NOT EXISTS (
|
|
SELECT 1
|
|
FROM pg_roles
|
|
WHERE rolname = 'supabase_functions_admin'
|
|
)
|
|
THEN
|
|
CREATE USER supabase_functions_admin NOINHERIT CREATEROLE LOGIN NOREPLICATION;
|
|
END IF;
|
|
END
|
|
$$;
|
|
GRANT ALL PRIVILEGES ON SCHEMA supabase_functions TO supabase_functions_admin;
|
|
GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA supabase_functions TO supabase_functions_admin;
|
|
GRANT ALL PRIVILEGES ON ALL SEQUENCES IN SCHEMA supabase_functions TO supabase_functions_admin;
|
|
ALTER USER supabase_functions_admin SET search_path = "supabase_functions";
|
|
ALTER table "supabase_functions".migrations OWNER TO supabase_functions_admin;
|
|
ALTER table "supabase_functions".hooks OWNER TO supabase_functions_admin;
|
|
ALTER function "supabase_functions".http_request() OWNER TO supabase_functions_admin;
|
|
GRANT supabase_functions_admin TO postgres;
|
|
-- Remove unused supabase_pg_net_admin role
|
|
DO
|
|
$$
|
|
BEGIN
|
|
IF EXISTS (
|
|
SELECT 1
|
|
FROM pg_roles
|
|
WHERE rolname = 'supabase_pg_net_admin'
|
|
)
|
|
THEN
|
|
REASSIGN OWNED BY supabase_pg_net_admin TO supabase_admin;
|
|
DROP OWNED BY supabase_pg_net_admin;
|
|
DROP ROLE supabase_pg_net_admin;
|
|
END IF;
|
|
END
|
|
$$;
|
|
-- pg_net grants when extension is already enabled
|
|
DO
|
|
$$
|
|
BEGIN
|
|
IF EXISTS (
|
|
SELECT 1
|
|
FROM pg_extension
|
|
WHERE extname = 'pg_net'
|
|
)
|
|
THEN
|
|
GRANT USAGE ON SCHEMA net TO supabase_functions_admin, postgres, anon, authenticated, service_role;
|
|
ALTER function net.http_get(url text, params jsonb, headers jsonb, timeout_milliseconds integer) SECURITY DEFINER;
|
|
ALTER function net.http_post(url text, body jsonb, params jsonb, headers jsonb, timeout_milliseconds integer) SECURITY DEFINER;
|
|
ALTER function net.http_get(url text, params jsonb, headers jsonb, timeout_milliseconds integer) SET search_path = net;
|
|
ALTER function net.http_post(url text, body jsonb, params jsonb, headers jsonb, timeout_milliseconds integer) SET search_path = net;
|
|
REVOKE ALL ON FUNCTION net.http_get(url text, params jsonb, headers jsonb, timeout_milliseconds integer) FROM PUBLIC;
|
|
REVOKE ALL ON FUNCTION net.http_post(url text, body jsonb, params jsonb, headers jsonb, timeout_milliseconds integer) FROM PUBLIC;
|
|
GRANT EXECUTE ON FUNCTION net.http_get(url text, params jsonb, headers jsonb, timeout_milliseconds integer) TO supabase_functions_admin, postgres, anon, authenticated, service_role;
|
|
GRANT EXECUTE ON FUNCTION net.http_post(url text, body jsonb, params jsonb, headers jsonb, timeout_milliseconds integer) TO supabase_functions_admin, postgres, anon, authenticated, service_role;
|
|
END IF;
|
|
END
|
|
$$;
|
|
-- Event trigger for pg_net
|
|
CREATE OR REPLACE FUNCTION extensions.grant_pg_net_access()
|
|
RETURNS event_trigger
|
|
LANGUAGE plpgsql
|
|
AS $$
|
|
BEGIN
|
|
IF EXISTS (
|
|
SELECT 1
|
|
FROM pg_event_trigger_ddl_commands() AS ev
|
|
JOIN pg_extension AS ext
|
|
ON ev.objid = ext.oid
|
|
WHERE ext.extname = 'pg_net'
|
|
)
|
|
THEN
|
|
GRANT USAGE ON SCHEMA net TO supabase_functions_admin, postgres, anon, authenticated, service_role;
|
|
ALTER function net.http_get(url text, params jsonb, headers jsonb, timeout_milliseconds integer) SECURITY DEFINER;
|
|
ALTER function net.http_post(url text, body jsonb, params jsonb, headers jsonb, timeout_milliseconds integer) SECURITY DEFINER;
|
|
ALTER function net.http_get(url text, params jsonb, headers jsonb, timeout_milliseconds integer) SET search_path = net;
|
|
ALTER function net.http_post(url text, body jsonb, params jsonb, headers jsonb, timeout_milliseconds integer) SET search_path = net;
|
|
REVOKE ALL ON FUNCTION net.http_get(url text, params jsonb, headers jsonb, timeout_milliseconds integer) FROM PUBLIC;
|
|
REVOKE ALL ON FUNCTION net.http_post(url text, body jsonb, params jsonb, headers jsonb, timeout_milliseconds integer) FROM PUBLIC;
|
|
GRANT EXECUTE ON FUNCTION net.http_get(url text, params jsonb, headers jsonb, timeout_milliseconds integer) TO supabase_functions_admin, postgres, anon, authenticated, service_role;
|
|
GRANT EXECUTE ON FUNCTION net.http_post(url text, body jsonb, params jsonb, headers jsonb, timeout_milliseconds integer) TO supabase_functions_admin, postgres, anon, authenticated, service_role;
|
|
END IF;
|
|
END;
|
|
$$;
|
|
COMMENT ON FUNCTION extensions.grant_pg_net_access IS 'Grants access to pg_net';
|
|
DO
|
|
$$
|
|
BEGIN
|
|
IF NOT EXISTS (
|
|
SELECT 1
|
|
FROM pg_event_trigger
|
|
WHERE evtname = 'issue_pg_net_access'
|
|
) THEN
|
|
CREATE EVENT TRIGGER issue_pg_net_access ON ddl_command_end WHEN TAG IN ('CREATE EXTENSION')
|
|
EXECUTE PROCEDURE extensions.grant_pg_net_access();
|
|
END IF;
|
|
END
|
|
$$;
|
|
INSERT INTO supabase_functions.migrations (version) VALUES ('20210809183423_update_grants');
|
|
ALTER function supabase_functions.http_request() SECURITY DEFINER;
|
|
ALTER function supabase_functions.http_request() SET search_path = supabase_functions;
|
|
REVOKE ALL ON FUNCTION supabase_functions.http_request() FROM PUBLIC;
|
|
GRANT EXECUTE ON FUNCTION supabase_functions.http_request() TO postgres, anon, authenticated, service_role;
|
|
COMMIT;
|
|
"""
|
|
|
|
[[config.mounts]]
|
|
filePath = "/volumes/functions/hello/index.ts"
|
|
content = """
|
|
// Follow this setup guide to integrate the Deno language server with your editor:
|
|
// https://deno.land/manual/getting_started/setup_your_environment
|
|
// This enables autocomplete, go to definition, etc.
|
|
|
|
import { serve } from "https://deno.land/std@0.177.1/http/server.ts"
|
|
|
|
serve(async () => {
|
|
return new Response(
|
|
`"Hello from Edge Functions!"`,
|
|
{ headers: { "Content-Type": "application/json" } },
|
|
)
|
|
})
|
|
|
|
// To invoke:
|
|
// curl 'http://localhost:<KONG_HTTP_PORT>/functions/v1/hello' \
|
|
// --header 'Authorization: Bearer <anon/service_role API key>'
|
|
"""
|
|
|
|
[[config.mounts]]
|
|
filePath = "/volumes/functions/main/index.ts"
|
|
content = """import * as jose from 'https://deno.land/x/jose@v4.14.4/index.ts'
|
|
|
|
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'
|
|
|
|
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) {
|
|
const authHeader = req.headers.get('authorization')
|
|
if (!authHeader) {
|
|
throw new Error('Missing authorization header')
|
|
}
|
|
const [bearer, token] = authHeader.split(' ')
|
|
if (bearer !== 'Bearer') {
|
|
throw new Error(`Auth header is not 'Bearer {token}'`)
|
|
}
|
|
return token
|
|
}
|
|
|
|
async function isValidLegacyJWT(jwt: string): Promise<boolean> {
|
|
if (!JWT_SECRET) {
|
|
console.error('JWT_SECRET not available for HS256 token verification')
|
|
return false
|
|
}
|
|
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') {
|
|
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 isValid = await isValidHybridJWT(token)
|
|
if (!isValid) {
|
|
return new Response(JSON.stringify({ msg: 'Invalid JWT' }), {
|
|
status: 401,
|
|
headers: { 'Content-Type': 'application/json' },
|
|
})
|
|
}
|
|
} catch (e) {
|
|
console.error(e)
|
|
return new Response(JSON.stringify({ msg: e.toString() }), {
|
|
status: 401,
|
|
headers: { 'Content-Type': 'application/json' },
|
|
})
|
|
}
|
|
}
|
|
|
|
const url = new URL(req.url)
|
|
const { pathname } = url
|
|
const path_parts = pathname.split('/')
|
|
const service_name = path_parts[1]
|
|
|
|
if (!service_name || service_name === '') {
|
|
const error = { msg: 'missing function name in request' }
|
|
return new Response(JSON.stringify(error), {
|
|
status: 400,
|
|
headers: { 'Content-Type': 'application/json' },
|
|
})
|
|
}
|
|
|
|
const servicePath = `/home/deno/functions/${service_name}`
|
|
console.error(`serving the request with ${servicePath}`)
|
|
|
|
const memoryLimitMb = 150
|
|
const workerTimeoutMs = 1 * 60 * 1000
|
|
const noModuleCache = false
|
|
const importMapPath = null
|
|
const envVarsObj = Deno.env.toObject()
|
|
const envVars = Object.keys(envVarsObj).map((k) => [k, envVarsObj[k]])
|
|
|
|
try {
|
|
const worker = await EdgeRuntime.userWorkers.create({
|
|
servicePath,
|
|
memoryLimitMb,
|
|
workerTimeoutMs,
|
|
noModuleCache,
|
|
importMapPath,
|
|
envVars,
|
|
})
|
|
return await worker.fetch(req)
|
|
} catch (e) {
|
|
const error = { msg: e.toString() }
|
|
return new Response(JSON.stringify(error), {
|
|
status: 500,
|
|
headers: { 'Content-Type': 'application/json' },
|
|
})
|
|
}
|
|
})
|
|
"""
|
|
|
|
[[config.mounts]]
|
|
filePath = "/volumes/logs/vector.yml"
|
|
content = """api:
|
|
enabled: true
|
|
address: 0.0.0.0:9001
|
|
|
|
sources:
|
|
docker_host:
|
|
type: docker_logs
|
|
|
|
transforms:
|
|
project_logs:
|
|
type: remap
|
|
inputs:
|
|
- docker_host
|
|
source: |-
|
|
.project = "default"
|
|
.event_message = del(.message)
|
|
compose_service, label_err = get(.label, ["com.docker.compose.service"])
|
|
if label_err != null || compose_service == null {
|
|
abort
|
|
}
|
|
compose_service = to_string!(compose_service)
|
|
if compose_service == "vector" {
|
|
abort
|
|
}
|
|
.appname = "supabase-" + compose_service
|
|
del(.container_created_at)
|
|
del(.container_id)
|
|
del(.source_type)
|
|
del(.stream)
|
|
del(.label)
|
|
del(.image)
|
|
del(.host)
|
|
del(.stream)
|
|
router:
|
|
type: route
|
|
inputs:
|
|
- project_logs
|
|
route:
|
|
kong: '.appname == "supabase-kong" || .appname == "supabase-envoy"'
|
|
auth: '.appname == "supabase-auth"'
|
|
rest: '.appname == "supabase-rest"'
|
|
realtime: '.appname == "supabase-realtime"'
|
|
storage: '.appname == "supabase-storage"'
|
|
functions: '.appname == "supabase-functions"'
|
|
db: '.appname == "supabase-db"'
|
|
# Ignores non nginx errors since they are related with kong booting up
|
|
kong_logs:
|
|
type: remap
|
|
inputs:
|
|
- router.kong
|
|
source: |-
|
|
req, err = parse_nginx_log(.event_message, "combined")
|
|
if err == null {
|
|
.timestamp = req.timestamp
|
|
.metadata.request.headers.referer = req.referer
|
|
.metadata.request.headers.user_agent = req.agent
|
|
.metadata.request.headers.cf_connecting_ip = req.client
|
|
.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
|
|
}
|
|
kong_err:
|
|
type: remap
|
|
inputs:
|
|
- router.kong
|
|
source: |-
|
|
.metadata.request.method = "GET"
|
|
.metadata.response.status_code = 200
|
|
parsed, err = parse_nginx_log(.event_message, "error")
|
|
if err == null {
|
|
.timestamp = parsed.timestamp
|
|
.severity = parsed.severity
|
|
.metadata.request.host = parsed.host
|
|
.metadata.request.headers.cf_connecting_ip = parsed.client
|
|
url, err = split(parsed.request, " ")
|
|
if err == null {
|
|
.metadata.request.method = url[0]
|
|
.metadata.request.path = url[1]
|
|
.metadata.request.protocol = url[2]
|
|
}
|
|
}
|
|
if err != null {
|
|
abort
|
|
}
|
|
# Gotrue logs are structured json strings which frontend parses directly. But we keep metadata for consistency.
|
|
auth_logs:
|
|
type: remap
|
|
inputs:
|
|
- router.auth
|
|
source: |-
|
|
parsed, err = parse_json(.event_message)
|
|
if err == null {
|
|
.metadata.timestamp = parsed.time
|
|
.metadata = merge!(.metadata, parsed)
|
|
}
|
|
# PostgREST logs are structured so we separate timestamp from message using regex
|
|
rest_logs:
|
|
type: remap
|
|
inputs:
|
|
- router.rest
|
|
source: |-
|
|
parsed, err = parse_regex(.event_message, r'^(?P<time>.*): (?P<msg>.*)$')
|
|
if err == null {
|
|
.event_message = parsed.msg
|
|
.timestamp = parse_timestamp!(value: parsed.time,format: "%d/%b/%Y:%H:%M:%S %z")
|
|
.metadata.host = .project
|
|
}
|
|
realtime_logs_filtered:
|
|
type: filter
|
|
inputs:
|
|
- router.realtime
|
|
condition: '!contains(string!(.event_message), "/health")'
|
|
realtime_logs:
|
|
type: remap
|
|
inputs:
|
|
- realtime_logs_filtered
|
|
source: |-
|
|
.metadata.project = del(.project)
|
|
.metadata.external_id = .metadata.project
|
|
parsed, err = parse_regex(.event_message, r'^(?P<time>\\d+:\\d+:\\d+\\.\\d+) \\[(?P<level>\\w+)\\] (?P<msg>.*)$')
|
|
if err == null {
|
|
.event_message = parsed.msg
|
|
.metadata.level = parsed.level
|
|
}
|
|
functions_logs:
|
|
type: remap
|
|
inputs:
|
|
- router.functions
|
|
source: |-
|
|
.metadata.project_ref = del(.project)
|
|
# Storage logs may contain json objects so we parse them for completeness
|
|
storage_logs:
|
|
type: remap
|
|
inputs:
|
|
- router.storage
|
|
source: |-
|
|
.metadata.project = del(.project)
|
|
.metadata.tenantId = .metadata.project
|
|
parsed, err = parse_json(.event_message)
|
|
if err == null {
|
|
.event_message = parsed.msg
|
|
.metadata.level = parsed.level
|
|
.metadata.timestamp = parsed.time
|
|
.metadata.context[0].host = parsed.hostname
|
|
.metadata.context[0].pid = parsed.pid
|
|
}
|
|
# Postgres logs some messages to stderr which we map to warning severity level
|
|
db_logs:
|
|
type: remap
|
|
inputs:
|
|
- router.db
|
|
source: |-
|
|
.metadata.host = "db-default"
|
|
.metadata.parsed.timestamp = .timestamp
|
|
|
|
parsed, err = parse_regex(.event_message, r'.*(?P<level>INFO|NOTICE|WARNING|ERROR|LOG|FATAL|PANIC?):.*', numeric_groups: true)
|
|
|
|
if err != null || parsed == null {
|
|
.metadata.parsed.error_severity = "info"
|
|
}
|
|
if parsed.level != null {
|
|
.metadata.parsed.error_severity = parsed.level
|
|
}
|
|
if .metadata.parsed.error_severity == "info" {
|
|
.metadata.parsed.error_severity = "log"
|
|
}
|
|
.metadata.parsed.error_severity = upcase!(.metadata.parsed.error_severity)
|
|
|
|
sinks:
|
|
logflare_auth:
|
|
type: 'http'
|
|
inputs:
|
|
- auth_logs
|
|
encoding:
|
|
codec: 'json'
|
|
method: 'post'
|
|
request:
|
|
retry_max_duration_secs: 30
|
|
retry_initial_backoff_secs: 1
|
|
headers:
|
|
x-api-key: ${LOGFLARE_PUBLIC_ACCESS_TOKEN?LOGFLARE_PUBLIC_ACCESS_TOKEN is required}
|
|
uri: 'http://analytics:4000/api/logs?source_name=gotrue.logs.prod'
|
|
logflare_realtime:
|
|
type: 'http'
|
|
inputs:
|
|
- realtime_logs
|
|
encoding:
|
|
codec: 'json'
|
|
method: 'post'
|
|
request:
|
|
retry_max_duration_secs: 30
|
|
retry_initial_backoff_secs: 1
|
|
headers:
|
|
x-api-key: ${LOGFLARE_PUBLIC_ACCESS_TOKEN?LOGFLARE_PUBLIC_ACCESS_TOKEN is required}
|
|
uri: 'http://analytics:4000/api/logs?source_name=realtime.logs.prod'
|
|
logflare_rest:
|
|
type: 'http'
|
|
inputs:
|
|
- rest_logs
|
|
encoding:
|
|
codec: 'json'
|
|
method: 'post'
|
|
request:
|
|
retry_max_duration_secs: 30
|
|
retry_initial_backoff_secs: 1
|
|
headers:
|
|
x-api-key: ${LOGFLARE_PUBLIC_ACCESS_TOKEN?LOGFLARE_PUBLIC_ACCESS_TOKEN is required}
|
|
uri: 'http://analytics:4000/api/logs?source_name=postgREST.logs.prod'
|
|
logflare_db:
|
|
type: 'http'
|
|
inputs:
|
|
- db_logs
|
|
encoding:
|
|
codec: 'json'
|
|
method: 'post'
|
|
request:
|
|
retry_max_duration_secs: 30
|
|
retry_initial_backoff_secs: 1
|
|
headers:
|
|
x-api-key: ${LOGFLARE_PUBLIC_ACCESS_TOKEN?LOGFLARE_PUBLIC_ACCESS_TOKEN is required}
|
|
uri: 'http://analytics:4000/api/logs?source_name=postgres.logs'
|
|
logflare_functions:
|
|
type: 'http'
|
|
inputs:
|
|
- functions_logs
|
|
encoding:
|
|
codec: 'json'
|
|
method: 'post'
|
|
request:
|
|
retry_max_duration_secs: 30
|
|
retry_initial_backoff_secs: 1
|
|
headers:
|
|
x-api-key: ${LOGFLARE_PUBLIC_ACCESS_TOKEN?LOGFLARE_PUBLIC_ACCESS_TOKEN is required}
|
|
uri: 'http://analytics:4000/api/logs?source_name=deno-relay-logs'
|
|
logflare_storage:
|
|
type: 'http'
|
|
inputs:
|
|
- storage_logs
|
|
encoding:
|
|
codec: 'json'
|
|
method: 'post'
|
|
request:
|
|
retry_max_duration_secs: 30
|
|
retry_initial_backoff_secs: 1
|
|
headers:
|
|
x-api-key: ${LOGFLARE_PUBLIC_ACCESS_TOKEN?LOGFLARE_PUBLIC_ACCESS_TOKEN is required}
|
|
uri: 'http://analytics:4000/api/logs?source_name=storage.logs.prod.2'
|
|
logflare_kong:
|
|
type: 'http'
|
|
inputs:
|
|
- kong_logs
|
|
- kong_err
|
|
encoding:
|
|
codec: 'json'
|
|
method: 'post'
|
|
request:
|
|
retry_max_duration_secs: 30
|
|
retry_initial_backoff_secs: 1
|
|
headers:
|
|
x-api-key: ${LOGFLARE_PUBLIC_ACCESS_TOKEN?LOGFLARE_PUBLIC_ACCESS_TOKEN is required}
|
|
uri: 'http://analytics:4000/api/logs?source_name=cloudflare.logs.prod'
|
|
"""
|
|
|
|
[[config.mounts]]
|
|
filePath = "/volumes/pooler/pooler.exs"
|
|
content = """{:ok, _} = Application.ensure_all_started(:supavisor)
|
|
|
|
{:ok, version} =
|
|
case Supavisor.Repo.query!("select version()") do
|
|
%{rows: [[ver]]} -> Supavisor.Helpers.parse_pg_version(ver)
|
|
_ -> nil
|
|
end
|
|
|
|
params = %{
|
|
"external_id" => System.get_env("POOLER_TENANT_ID"),
|
|
"db_host" => System.get_env("POSTGRES_HOST") || "db",
|
|
"db_port" => System.get_env("POSTGRES_PORT"),
|
|
"db_database" => System.get_env("POSTGRES_DB"),
|
|
"require_user" => false,
|
|
"auth_query" => "SELECT * FROM pgbouncer.get_auth($1)",
|
|
"default_max_clients" => System.get_env("POOLER_MAX_CLIENT_CONN"),
|
|
"default_pool_size" => System.get_env("POOLER_DEFAULT_POOL_SIZE"),
|
|
"default_parameter_status" => %{"server_version" => version},
|
|
"users" => [%{
|
|
"db_user" => "pgbouncer",
|
|
"db_password" => System.get_env("POSTGRES_PASSWORD"),
|
|
"mode_type" => System.get_env("POOLER_POOL_MODE"),
|
|
"pool_size" => System.get_env("POOLER_DEFAULT_POOL_SIZE"),
|
|
"is_manager" => true
|
|
}]
|
|
}
|
|
|
|
if !Supavisor.Tenants.get_tenant_by_external_id(params["external_id"]) do
|
|
{:ok, _} = Supavisor.Tenants.create_tenant(params)
|
|
end
|
|
"""
|