2024-02-28 12:48:39 +00:00
# documentation: https://supabase.io
# slogan: The open source Firebase alternative.
# tags: firebase, alternative, open-source
# minversion: 4.0.0-beta.228
2024-02-29 08:15:06 +00:00
# logo: svgs/supabase.svg
2024-02-27 14:44:19 +00:00
services :
2024-02-28 12:48:39 +00:00
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'
depends_on :
supabase-analytics :
condition : service_healthy
environment :
- SERVICE_FQDN_SUPABASE
2024-02-29 08:15:06 +00:00
- JWT_SERCET=${SERVICE_PASSWORD_JWT}
2024-02-28 12:48:39 +00:00
- KONG_DATABASE=off
- 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_NGINX_PROXY_PROXY_BUFFER_SIZE=160k
- KONG_NGINX_PROXY_PROXY_BUFFERS=64 160k
- SUPABASE_ANON_KEY=${SERVICE_SUPABASEANON_KEY}
- SUPABASE_SERVICE_KEY=${SERVICE_SUPABASESERVICE_KEY}
- DASHBOARD_USERNAME=${SERVICE_USER_ADMIN}
- DASHBOARD_PASSWORD=${SERVICE_PASSWORD_ADMIN}
volumes :
# https://github.com/supabase/supabase/issues/12661
- type : bind
source : ./volumes/api/kong.yml
target : /home/kong/temp.yml
content : |
_format_version : '2.1'
_transform : true
###
### Consumers / Users
###
consumers :
- username : DASHBOARD
- username : anon
keyauth_credentials :
- key : $SUPABASE_ANON_KEY
- username : service_role
keyauth_credentials :
- key : $SUPABASE_SERVICE_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://supabase-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://supabase-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://supabase-auth:9999/authorize
routes :
- name : auth-v1-open-authorize
strip_path : true
paths :
- /auth/v1/authorize
plugins :
- name : cors
## Secure Auth routes
- name : auth-v1
_comment: 'GoTrue : /auth/v1/* -> http://supabase-auth:9999/*'
url : http://supabase-auth:9999/
routes :
- name : auth-v1-all
strip_path : true
paths :
- /auth/v1/
plugins :
- name : cors
- name : key-auth
config :
hide_credentials : false
- name : acl
config :
hide_groups_header : true
allow :
- admin
- anon
## Secure REST routes
- name : rest-v1
_comment: 'PostgREST : /rest/v1/* -> http://supabase-rest:3000/*'
url : http://supabase-rest:3000/
routes :
- name : rest-v1-all
strip_path : true
paths :
- /rest/v1/
plugins :
- name : cors
- name : key-auth
config :
hide_credentials : true
- name : acl
config :
hide_groups_header : true
allow :
- admin
- anon
## Secure GraphQL routes
- name : graphql-v1
_comment: 'PostgREST : /graphql/v1/* -> http://supabase-rest:3000/rpc/graphql'
url : http://supabase-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
- name : acl
config :
hide_groups_header : true
allow :
- admin
- anon
## Secure Realtime routes
- name : realtime-v1
_comment: 'Realtime : /realtime/v1/* -> ws://realtime:4000/socket/*'
url : http://realtime-dev.supabase-realtime:4000/socket/
routes :
- name : realtime-v1-all
strip_path : true
paths :
- /realtime/v1/
plugins :
- name : cors
- name : key-auth
config :
hide_credentials : false
- name : acl
config :
hide_groups_header : true
allow :
- admin
- anon
## Storage routes: the storage server manages its own auth
- name : storage-v1
_comment: 'Storage : /storage/v1/* -> http://supabase-storage:5000/*'
url : http://supabase-storage:5000/
routes :
- name : storage-v1-all
strip_path : true
paths :
- /storage/v1/
plugins :
- name : cors
## Edge Functions routes
- name : functions-v1
_comment: 'Edge Functions : /functions/v1/* -> http://functions:9000/*'
url : http://functions:9000/
routes :
- name : functions-v1-all
strip_path : true
paths :
- /functions/v1/
plugins :
- name : cors
## Analytics routes
- 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
_comment: 'pg-meta : /pg/* -> http://supabase-meta:8080/*'
url : http://supabase-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
## Protected Dashboard - catch all remaining routes
- name : dashboard
_comment: 'Studio : /* -> http://studio:3000/*'
url : http://supabase-studio:3000/
routes :
- name : dashboard-all
strip_path : true
paths :
- /
plugins :
- name : cors
- name : basic-auth
config :
hide_credentials : true
2024-02-27 14:44:19 +00:00
supabase-studio :
image : supabase/studio:20240205-b145c86
healthcheck :
test :
[
"CMD" ,
"node" ,
"-e" ,
"require('http').get('http://localhost:3000/api/profile', (r) => {if (r.statusCode !== 200) throw new Error(r.statusCode)})" ,
]
timeout : 5s
interval : 5s
retries : 3
depends_on :
supabase-analytics :
condition : service_healthy
environment :
- HOSTNAME=0.0.0.0
2024-02-28 12:48:39 +00:00
- STUDIO_PG_META_URL=http://supabase-meta:8080
2024-02-27 14:44:19 +00:00
- POSTGRES_PASSWORD=${SERVICE_PASSWORD_POSTGRES}
- DEFAULT_ORGANIZATION_NAME=${STUDIO_DEFAULT_ORGANIZATION:-Default Organization}
- DEFAULT_PROJECT_NAME=${STUDIO_DEFAULT_PROJECT:-Default Project}
2024-02-28 12:48:39 +00:00
- SUPABASE_URL=http://supabase-kong:8000
2024-02-27 14:44:19 +00:00
- SUPABASE_PUBLIC_URL=${SERVICE_FQDN_SUPABASE}
2024-02-28 12:48:39 +00:00
- SUPABASE_ANON_KEY=${SERVICE_SUPABASEANON_KEY}
- SUPABASE_SERVICE_KEY=${SERVICE_SUPABASESERVICE_KEY}
2024-02-27 14:44:19 +00:00
- LOGFLARE_API_KEY=${SERVICE_PASSWORD_LOGFLARE}
- LOGFLARE_URL=http://supabase-analytics:4000
- NEXT_PUBLIC_ENABLE_LOGS=true
# Comment to use Big Query backend for analytics
- NEXT_ANALYTICS_BACKEND_PROVIDER=postgres
# Uncomment to use Big Query backend for analytics
# NEXT_ANALYTICS_BACKEND_PROVIDER=bigquery
supabase-db :
image : supabase/postgres:15.1.0.147
healthcheck :
test : pg_isready -U postgres -h localhost
interval : 5s
timeout : 5s
retries : 10
depends_on :
supabase-vector :
condition : service_healthy
command :
- postgres
- -c
- config_file=/etc/postgresql/postgresql.conf
- -c
- log_min_messages=fatal
restart : unless-stopped
environment :
- POSTGRES_HOST=/var/run/postgresql
- PGPORT=${POSTGRES_PORT:-5432}
- POSTGRES_PORT=${POSTGRES_PORT:-5432}
- PGPASSWORD=${SERVICE_PASSWORD_POSTGRES}
- POSTGRES_PASSWORD=${SERVICE_PASSWORD_POSTGRES}
- PGDATABASE=${POSTGRES_DB:-supabase}
- POSTGRES_DB=${POSTGRES_DB:-supabase}
2024-02-28 12:48:39 +00:00
- JWT_SECRET=${SERVICE_PASSWORD_JWT}
2024-02-27 14:44:19 +00:00
- JWT_EXP=${JWT_EXPIRY:-3600}
volumes :
- supabase-db-data:/var/lib/postgresql/data
- type : bind
source : ./volumes/db/realtime.sql
target : /docker-entrypoint-initdb.d/migrations/99-realtime.sql
content : |
2024-02-28 12:48:39 +00:00
\set pguser `echo "supabase_admin"`
2024-02-27 14:44:19 +00:00
create schema if not exists _realtime;
alter schema _realtime owner to :pguser;
- type : bind
source : ./volumes/db/webhooks.sql
target : /docker-entrypoint-initdb.d/init-scripts/98-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;
- type : bind
source : ./volumes/db/roles.sql
target : /docker-entrypoint-initdb.d/init-scripts/99-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';
- type : bind
source : ./volumes/db/jwt.sql
target : /docker-entrypoint-initdb.d/init-scripts/99-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';
- type : bind
source : ./volumes/db/logs.sql
target : /docker-entrypoint-initdb.d/migrations/99-logs.sql
content : |
2024-02-28 12:48:39 +00:00
\set pguser `echo "supabase_admin"`
2024-02-27 14:44:19 +00:00
create schema if not exists _analytics;
alter schema _analytics owner to :pguser;
supabase-analytics :
image : supabase/logflare:1.4.0
healthcheck :
test : [ "CMD" , "curl" , "http://localhost:4000/health" ]
timeout : 5s
interval : 5s
retries : 10
restart : unless-stopped
depends_on :
supabase-db :
condition : service_healthy
# volumes:
# - type: bind
2024-02-28 12:48:39 +00:00
# source: ./volumes/gcloud.json
2024-02-27 14:44:19 +00:00
# target: /opt/app/rel/logflare/bin/gcloud.json
# read_only: true
environment :
- LOGFLARE_NODE_HOST=127.0.0.1
- DB_USERNAME=supabase_admin
- DB_DATABASE=${POSTGRES_DB:-supabase}
- DB_HOSTNAME=${POSTGRES_HOST:-supabase-db}
- DB_PORT=${POSTGRES_PORT:-5432}
- DB_PASSWORD=${SERVICE_PASSWORD_POSTGRES}
- DB_SCHEMA=_analytics
- LOGFLARE_API_KEY=${SERVICE_PASSWORD_LOGFLARE}
- LOGFLARE_SINGLE_TENANT=true
2024-02-28 12:48:39 +00:00
- LOGFLARE_SINGLE_TENANT_MODE=true
2024-02-27 14:44:19 +00:00
- LOGFLARE_SUPABASE_MODE=true
# Comment variables to use Big Query backend for analytics
- POSTGRES_BACKEND_URL=postgresql://supabase_admin:${SERVICE_PASSWORD_POSTGRES}@${POSTGRES_HOST:-supabase-db}:${POSTGRES_PORT:-5432}/${POSTGRES_DB:-supabase}
- POSTGRES_BACKEND_SCHEMA=_analytics
- LOGFLARE_FEATURE_FLAG_OVERRIDE=multibackend=true
# Uncomment to use Big Query backend for analytics
# GOOGLE_PROJECT_ID=${GOOGLE_PROJECT_ID}
# GOOGLE_PROJECT_NUMBER=${GOOGLE_PROJECT_NUMBER}
supabase-vector :
image : timberio/vector:0.28.1-alpine
healthcheck :
test :
[
"CMD" ,
"wget" ,
"--no-verbose" ,
"--tries=1" ,
"--spider" ,
"http://supabase-vector:9001/health" ,
]
timeout : 5s
interval : 5s
retries : 3
volumes :
- type : bind
source : ./volumes/logs/vector.yml
target : /etc/vector/vector.yml
read_only : true
content : |
api :
enabled : true
address : 0.0 .0 .0 : 9001
sources :
docker_host :
type : docker_logs
exclude_containers :
- supabase-vector
transforms :
project_logs :
type : remap
inputs :
- docker_host
source : |-
.project = "default"
.event_message = del(.message)
.appname = del(.container_name)
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 :
2024-02-28 12:48:39 +00:00
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), "supabase-realtime")'
storage : 'starts_with(string!(.appname), "supabase-storage")'
functions : 'starts_with(string!(.appname), "supabase-functions")'
db : 'starts_with(string!(.appname), "supabase-db")'
2024-02-27 14:44:19 +00:00
# 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.request.method = req.method
.metadata.request.path = req.path
.metadata.request.protocol = req.protocol
.metadata.response.status_code = req.status
}
if err != null {
abort
}
# Ignores non nginx errors since they are related with kong booting up
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 = to_timestamp!(parsed.time)
.metadata.host = .project
}
# Realtime logs are structured so we parse the severity level using regex (ignore time because it has no date)
realtime_logs :
type : remap
inputs :
- router.realtime
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
}
# 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 != 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 : 10
uri : 'http://supabase-analytics:4000/api/logs?source_name=gotrue.logs.prod&api_key=${LOGFLARE_API_KEY}'
logflare_realtime :
type : 'http'
inputs :
- realtime_logs
encoding :
codec : 'json'
method : 'post'
request :
retry_max_duration_secs : 10
uri : 'http://supabase-analytics:4000/api/logs?source_name=realtime.logs.prod&api_key=${LOGFLARE_API_KEY}'
logflare_rest :
type : 'http'
inputs :
- rest_logs
encoding :
codec : 'json'
method : 'post'
request :
retry_max_duration_secs : 10
uri : 'http://supabase-analytics:4000/api/logs?source_name=postgREST.logs.prod&api_key=${LOGFLARE_API_KEY}'
logflare_db :
type : 'http'
inputs :
- db_logs
encoding :
codec : 'json'
method : 'post'
request :
retry_max_duration_secs : 10
# We must route the sink through kong because ingesting logs before logflare is fully initialised will
# lead to broken queries from studio. This works by the assumption that containers are started in the
# following order: vector > db > logflare > kong
2024-02-28 12:48:39 +00:00
uri : 'http://supabase-kong:8000/analytics/v1/api/logs?source_name=postgres.logs&api_key=${LOGFLARE_API_KEY}'
2024-02-27 14:44:19 +00:00
logflare_functions :
type : 'http'
inputs :
- router.functions
encoding :
codec : 'json'
method : 'post'
request :
retry_max_duration_secs : 10
uri : 'http://supabase-analytics:4000/api/logs?source_name=deno-relay-logs&api_key=${LOGFLARE_API_KEY}'
logflare_storage :
type : 'http'
inputs :
- storage_logs
encoding :
codec : 'json'
method : 'post'
request :
retry_max_duration_secs : 10
uri : 'http://supabase-analytics:4000/api/logs?source_name=storage.logs.prod.2&api_key=${LOGFLARE_API_KEY}'
logflare_kong :
type : 'http'
inputs :
- kong_logs
- kong_err
encoding :
codec : 'json'
method : 'post'
request :
retry_max_duration_secs : 10
uri : 'http://supabase-analytics:4000/api/logs?source_name=cloudflare.logs.prod&api_key=${LOGFLARE_API_KEY}'
- /var/run/docker.sock:/var/run/docker.sock:ro
environment :
- LOGFLARE_API_KEY=${SERVICE_PASSWORD_LOGFLARE}
command : [ "--config" , "etc/vector/vector.yml" ]
2024-02-28 12:48:39 +00:00
supabase-rest :
image : postgrest/postgrest:v12.0.1
2024-02-27 14:44:19 +00:00
depends_on :
2024-02-28 12:48:39 +00:00
supabase-db :
# Disable this if you are using an external Postgres database
condition : service_healthy
2024-02-27 14:44:19 +00:00
supabase-analytics :
condition : service_healthy
2024-02-28 12:48:39 +00:00
restart : unless-stopped
2024-02-27 14:44:19 +00:00
environment :
2024-02-28 12:48:39 +00:00
- PGRST_DB_URI=postgres://authenticator:${SERVICE_PASSWORD_POSTGRES}@${POSTGRES_HOST:-supabase-db}:${POSTGRES_PORT:-5432}/${POSTGRES_DB:-supabase}
- PGRST_DB_SCHEMAS=${PGRST_DB_SCHEMAS:-public}
- PGRST_DB_ANON_ROLE=anon
- PGRST_JWT_SECRET=${SERVICE_PASSWORD_JWT}
- PGRST_DB_USE_LEGACY_GUCS=false
- PGRST_APP_SETTINGS_JWT_SECRET=${SERVICE_PASSWORD_JWT}
- PGRST_APP_SETTINGS_JWT_EXP=${JWT_EXPIRY:-3600}
command : "postgrest"
supabase-auth :
image : supabase/gotrue:v2.132.3
depends_on :
supabase-db :
# Disable this if you are using an external Postgres database
condition : service_healthy
supabase-analytics :
condition : service_healthy
healthcheck :
test :
[
"CMD" ,
"wget" ,
"--no-verbose" ,
"--tries=1" ,
"--spider" ,
"http://localhost:9999/health" ,
]
timeout : 5s
interval : 5s
retries : 3
environment :
- GOTRUE_API_HOST=0.0.0.0
- GOTRUE_API_PORT=9999
- API_EXTERNAL_URL=${API_EXTERNAL_URL:-http://supabase-kong:8000}
2024-02-27 14:44:19 +00:00
2024-02-28 12:48:39 +00:00
- GOTRUE_DB_DRIVER=postgres
- GOTRUE_DB_DATABASE_URL=postgres://supabase_auth_admin:${SERVICE_PASSWORD_POSTGRES}@${POSTGRES_HOST:-supabase-db}:${POSTGRES_PORT:-5432}/${POSTGRES_DB:-supabase}
2024-02-27 14:44:19 +00:00
2024-02-28 12:48:39 +00:00
- GOTRUE_SITE_URL=${SERVICE_FQDN_SUPABASE}
- GOTRUE_URI_ALLOW_LIST=${ADDITIONAL_REDIRECT_URLS}
- GOTRUE_DISABLE_SIGNUP=${DISABLE_SIGNUP:-false}
2024-02-27 14:44:19 +00:00
2024-02-28 12:48:39 +00:00
- GOTRUE_JWT_ADMIN_ROLES=service_role
- GOTRUE_JWT_AUD=authenticated
- GOTRUE_JWT_DEFAULT_GROUP_NAME=authenticated
- GOTRUE_JWT_EXP=${JWT_EXPIRY:-3600}
- GOTRUE_JWT_SECRET=${SERVICE_PASSWORD_JWT}
2024-02-27 14:44:19 +00:00
2024-02-28 12:48:39 +00:00
- GOTRUE_EXTERNAL_EMAIL_ENABLED=${ENABLE_EMAIL_SIGNUP:-true}
- GOTRUE_MAILER_AUTOCONFIRM=${ENABLE_EMAIL_AUTOCONFIRM:-false}
# GOTRUE_MAILER_SECURE_EMAIL_CHANGE_ENABLED=true
# GOTRUE_SMTP_MAX_FREQUENCY=1s
- GOTRUE_SMTP_ADMIN_EMAIL=${SMTP_ADMIN_EMAIL}
- GOTRUE_SMTP_HOST=${SMTP_HOST}
- GOTRUE_SMTP_PORT=${SMTP_PORT:-587}
- GOTRUE_SMTP_USER=${SMTP_USER}
- GOTRUE_SMTP_PASS=${SMTP_PASS}
- GOTRUE_SMTP_SENDER_NAME=${SMTP_SENDER_NAME}
- GOTRUE_MAILER_URLPATHS_INVITE=${MAILER_URLPATHS_INVITE:-/auth/v1/verify}
- GOTRUE_MAILER_URLPATHS_CONFIRMATION=${MAILER_URLPATHS_CONFIRMATION:-/auth/v1/verify}
- GOTRUE_MAILER_URLPATHS_RECOVERY=${MAILER_URLPATHS_RECOVERY:-/auth/v1/verify}
- GOTRUE_MAILER_URLPATHS_EMAIL_CHANGE=${MAILER_URLPATHS_EMAIL_CHANGE:-/auth/v1/verify}
2024-02-27 14:44:19 +00:00
2024-02-28 12:48:39 +00:00
- GOTRUE_EXTERNAL_PHONE_ENABLED=${ENABLE_PHONE_SIGNUP:-true}
- GOTRUE_SMS_AUTOCONFIRM=${ENABLE_PHONE_AUTOCONFIRM:-true}
supabase-realtime :
# This container name looks inconsistent but is correct because realtime constructs tenant id by parsing the subdomain
2024-03-15 06:11:05 +00:00
container_name : realtime-dev.supabase-realtime
2024-02-28 12:48:39 +00:00
image : supabase/realtime:v2.25.50
depends_on :
supabase-db :
# Disable this if you are using an external Postgres database
condition : service_healthy
supabase-analytics :
condition : service_healthy
healthcheck :
test : [ "CMD" , "bash" , "-c" , "printf \\0 > /dev/tcp/localhost/4000" ]
timeout : 5s
interval : 5s
retries : 3
environment :
- PORT=4000
- DB_HOST=${POSTGRES_HOST:-supabase-db}
- DB_PORT=${POSTGRES_PORT:-5432}
- DB_USER=supabase_admin
- DB_PASSWORD=${SERVICE_PASSWORD_POSTGRES}
- DB_NAME=${POSTGRES_DB:-supabase}
2024-03-15 06:11:05 +00:00
- DB_AFTER_CONNECT_QUERY=SET search_path TO _realtime
2024-02-28 12:48:39 +00:00
- DB_ENC_KEY=supabaserealtime
- API_JWT_SECRET=${SERVICE_PASSWORD_JWT}
- FLY_ALLOC_ID=fly123
- FLY_APP_NAME=realtime
- SECRET_KEY_BASE=${SECRET_PASSWORD_REALTIME}
- ERL_AFLAGS=-proto_dist inet_tcp
- ENABLE_TAILSCALE=false
- DNS_NODES=''
command : >
sh -c "/app/bin/migrate && /app/bin/realtime eval 'Realtime.Release.seeds(Realtime.Repo)' && /app/bin/server"
supabase-minio :
image : minio/minio
environment :
- MINIO_ROOT_USER=${SERVICE_USER_MINIO}
- MINIO_ROOT_PASSWORD=${SERVICE_PASSWORD_MINIO}
command : server --console-address ":9001" /data
healthcheck :
test : sleep 5 && exit 0
interval : 2s
timeout : 10s
retries : 5
volumes :
- ./volumes/storage:/data
2024-02-27 14:44:19 +00:00
2024-02-28 12:48:39 +00:00
minio-createbucket :
image : minio/mc
restart : "no"
environment :
- MINIO_ROOT_USER=${SERVICE_USER_MINIO}
- MINIO_ROOT_PASSWORD=${SERVICE_PASSWORD_MINIO}
depends_on :
supabase-minio :
condition : service_healthy
entrypoint : [ "/entrypoint.sh" ]
volumes :
- type : bind
source : ./entrypoint.sh
target : /entrypoint.sh
content : |
#!/bin/sh
/usr/bin/mc alias set supabase-minio http://supabase-minio:9000 ${MINIO_ROOT_USER} ${MINIO_ROOT_PASSWORD};
/usr/bin/mc mb supabase-minio/stub;
exit 0
2024-02-27 14:44:19 +00:00
2024-02-28 12:48:39 +00:00
supabase-storage :
image : supabase/storage-api:v0.46.4
depends_on :
supabase-db :
# Disable this if you are using an external Postgres database
condition : service_healthy
supabase-rest :
condition : service_started
imgproxy :
condition : service_started
healthcheck :
test :
[
"CMD" ,
"wget" ,
"--no-verbose" ,
"--tries=1" ,
"--spider" ,
"http://localhost:5000/status" ,
]
timeout : 5s
interval : 5s
retries : 3
environment :
- SERVER_PORT=5000
- SERVER_REGION=local
- MULTI_TENANT=false
- AUTH_JWT_SECRET=${SERVICE_PASSWORD_JWT}
- DATABASE_URL=postgres://supabase_storage_admin:${SERVICE_PASSWORD_POSTGRES}@${POSTGRES_HOST:-supabase-db}:${POSTGRES_PORT:-5432}/${POSTGRES_DB:-supabase}
- DB_INSTALL_ROLES=false
- STORAGE_BACKEND=s3
- STORAGE_S3_BUCKET=stub
- STORAGE_S3_ENDPOINT=http://supabase-minio:9000
- STORAGE_S3_FORCE_PATH_STYLE=true
- STORAGE_S3_REGION=us-east-1
- AWS_ACCESS_KEY_ID=${SERVICE_USER_MINIO}
- AWS_SECRET_ACCESS_KEY=${SERVICE_PASSWORD_MINIO}
- UPLOAD_FILE_SIZE_LIMIT=524288000
- UPLOAD_FILE_SIZE_LIMIT_STANDARD=524288000
- UPLOAD_SIGNED_URL_EXPIRATION_TIME=120
- TUS_URL_PATH=/upload/resumable
- TUS_MAX_SIZE=3600000
- IMAGE_TRANSFORMATION_ENABLED=true
- IMGPROXY_URL=http://imgproxy:8080
- IMGPROXY_REQUEST_TIMEOUT=15
- DATABASE_SEARCH_PATH=storage
2024-02-27 14:44:19 +00:00
2024-02-28 12:48:39 +00:00
# - ANON_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.ewogICJyb2xlIjogImFub24iLAogICJpc3MiOiAic3VwYWJhc2UiLAogICJpYXQiOiAxNzA4OTg4NDAwLAogICJleHAiOiAxODY2ODQxMjAwCn0.jCDqsoXGT58JnAjf27KOowNQsokkk0aR7rdbGG18P-8
# - SERVICE_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.ewogICJyb2xlIjogInNlcnZpY2Vfcm9sZSIsCiAgImlzcyI6ICJzdXBhYmFzZSIsCiAgImlhdCI6IDE3MDg5ODg0MDAsCiAgImV4cCI6IDE4NjY4NDEyMDAKfQ.GA7yF2BmqTzqGkP_oqDdJAQVt0djjIxGYuhE0zFDJV4
# - POSTGREST_URL=http://supabase-rest:3000
# - PGRST_JWT_SECRET=${SERVICE_PASSWORD_JWT}
# - DATABASE_URL=postgres://supabase_storage_admin:${SERVICE_PASSWORD_POSTGRES}@${POSTGRES_HOST:-supabase-db}:${POSTGRES_PORT:-5432}/${POSTGRES_DB:-supabase}
# - FILE_SIZE_LIMIT=52428800
# - STORAGE_BACKEND=s3
# - STORAGE_S3_BUCKET=stub
# - STORAGE_S3_ENDPOINT=http://supabase-minio:9000
# - STORAGE_S3_PROTOCOL=http
# - STORAGE_S3_REGION=stub
# - STORAGE_S3_FORCE_PATH_STYLE=true
# - AWS_ACCESS_KEY_ID=${SERVICE_USER_MINIO}
# - AWS_SECRET_ACCESS_KEY=${SERVICE_PASSWORD_MINIO}
# - AWS_DEFAULT_REGION=stub
# - FILE_STORAGE_BACKEND_PATH=/var/lib/storage
# - TENANT_ID=stub
# # TODO: https://github.com/supabase/storage-api/issues/55
# - REGION=stub
# - ENABLE_IMAGE_TRANSFORMATION=true
# - IMGPROXY_URL=http://imgproxy:8080
volumes :
- ./volumes/storage:/var/lib/storage
imgproxy :
image : darthsim/imgproxy:v3.8.0
healthcheck :
test : [ "CMD" , "imgproxy" , "health" ]
timeout : 5s
interval : 5s
retries : 3
environment :
- IMGPROXY_LOCAL_FILESYSTEM_ROOT=/
- IMGPROXY_USE_ETAG=true
- IMGPROXY_ENABLE_WEBP_DETECTION=${IMGPROXY_ENABLE_WEBP_DETECTION:-true}
volumes :
- ./volumes/storage:/var/lib/storage
2024-02-27 14:44:19 +00:00
2024-02-28 12:48:39 +00:00
supabase-meta :
image : supabase/postgres-meta:v0.77.2
2024-02-27 14:44:19 +00:00
depends_on :
supabase-db :
# Disable this if you are using an external Postgres database
condition : service_healthy
supabase-analytics :
condition : service_healthy
environment :
2024-02-28 12:48:39 +00:00
- PG_META_PORT=8080
- PG_META_DB_HOST=${POSTGRES_HOST:-supabase-db}
- PG_META_DB_PORT=${POSTGRES_PORT:-5432}
- PG_META_DB_NAME=${POSTGRES_DB:-supabase}
- PG_META_DB_USER=supabase_admin
- PG_META_DB_PASSWORD=${SERVICE_PASSWORD_POSTGRES}
supabase-edge-functions :
image : supabase/edge-runtime:v1.36.1
depends_on :
supabase-analytics :
condition : service_healthy
environment :
- JWT_SECRET=${SERVICE_PASSWORD_JWT}
- SUPABASE_URL=http://supabase-kong:8000
- SUPABASE_ANON_KEY=${SERVICE_SUPABASEANON_KEY}
- SUPABASE_SERVICE_ROLE_KEY=${SERVICE_SUPABASESERVICE_KEY}
- SUPABASE_DB_URL=postgresql://postgres:${SERVICE_PASSWORD_POSTGRES}@${POSTGRES_HOST:-supabase-db}:${POSTGRES_PORT:-5432}/${POSTGRES_DB:-supabase}
# TODO: Allow configuring VERIFY_JWT per function. This PR might help: https://github.com/supabase/cli/pull/786
- VERIFY_JWT=${FUNCTIONS_VERIFY_JWT:-false}
volumes :
- ./volumes/functions:/home/deno/functions
- type : bind
source : ./volumes/functions/main/index.ts
target : /home/deno/functions/main/index.ts
content : |
import { serve } from 'https://deno.land/std@0.131.0/http/server.ts'
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 VERIFY_JWT = Deno.env.get('VERIFY_JWT') === 'true'
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 verifyJWT(jwt: string) : Promise<boolean> {
const encoder = new TextEncoder()
const secretKey = encoder.encode(JWT_SECRET)
try {
await jose.jwtVerify(jwt, secretKey)
} catch (err) {
console.error(err)
return false
}
return true
}
serve(async (req : Request) => {
if (req.method !== 'OPTIONS' && VERIFY_JWT) {
try {
const token = getAuthToken(req)
const isValidJWT = await verifyJWT(token)
if (!isValidJWT) {
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' },
})
}
})
- type : bind
source : ./volumes/functions/hello/index.ts
target : /home/deno/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>'
command :
- start
- --main-service
- /home/deno/functions/main