Files
mesh-catalog/modules/supabase/module.json
T
jschoubben 62cecc8a2b supabase: the self-hosted stack as one module, its secrets rendered into files
ace runs Supabase under HAL as upstream's 13-container compose: a 2.2 GB
database (1.8 GB of it the dormant `novox` schema, 5.8 M rows in its largest
table), Kong at supabase.zurag.be, the pooler on 5433/6543. This is that stack
as a catalogue module, same images (the digests ace runs), same container
names so an assignment holds the running ones and a take replaces them.

The database stays inside the module. Supabase is a Postgres distribution: its
own image with pgsodium, pg_graphql, pg_net, vault and timescale preloaded, a
superuser (supabase_admin), a dozen reserved roles and a second database
(_supabase). A postgres-database grant - one database, one unprivileged role -
cannot hold it, so ace's data directory moves as a copy, not a dump/restore.

What HAL did by shell and environment the mesh now renders as files:
- kong.yml carries the anon/service keys and the dashboard login (owned by
  kong's uid 100), instead of an entrypoint that eval'd the environment;
- GoTrue reads a dotenv file (auth -c), PostgREST a config file, Vector its yml
  with the Logflare key in it, the database POSTGRES_PASSWORD_FILE and a
  jwt.sql rendered with the secret (owned by postgres, uid 105). None of these
  five containers has a secret in its environment.
- realtime, storage, meta, functions, analytics, studio and supavisor read
  their credentials from the environment only; each declares
  secrets-in-environment with the reason (ADR 0086).
- Upstreams are container names (supabase-db, supabase-kong, ...) instead of
  compose service names, which the mesh does not have.
- SITE_URL / API_EXTERNAL_URL / SUPABASE_PUBLIC_URL are
  https://${bound:route:name} (mesh-controller #149); on ace HAL rendered them
  as "https://supabase." - broken today.
- Vector reads the docker socket, as upstream does, but now includes only this
  module's containers instead of every container's logs on the machine.

Three things compose did that a declaration cannot, done as steps: a run-once
seed copies the image's /etc/postgresql-custom into the placed config
directory with cp -n (a named volume did that implicitly; never overwrites the
pgsodium root key), and two run-once gates wait for the database and for
Logflare, which compose expressed as depends_on: service_healthy.

The pooler bootstrap (pooler.exs) takes the tenant id and pool sizes from
settings.json, the module's one merge:json file, so ace keeps its tenant
"zurag"; and it repoints an existing tenant whose database host is not
supabase-db - HAL created ace's with host "db", which no longer resolves.

Secrets (vault, requires "secret"): postgres, jwt, anon-key,
service-role-key, dashboard-user, dashboard, logflare, pooler-vault,
key-base-a + key-base-b (concatenated: Phoenix wants 64+ bytes, a minted
secret is 40), openai. Three cannot be minted on any machine: anon-key and
service-role-key are JWTs signed with jwt, and pooler-vault must be exactly
32 bytes (AES-256-GCM, found in the bed). They are accepted. On ace every
secret the data already knows is accepted (all but key-base-a/b).

Not carried: Kong's 8443 and Logflare's 4000 on all interfaces (nothing
outside the module uses them); realtime's DB_ENC_KEY stays upstream's constant
(realtime deletes and re-seeds that tenant from its environment every start,
and the key must be exactly 16 bytes).

Verified: catalogue tests with MESH_CATALOGUE pointed here on mesh-controller
main and #149 (on main the render is refused for "name", never written
empty). The #149 resolution with stub providers, turned into a throwaway
stack of all 13 pinned digests with dummy secrets and the rendered files at
their owners and modes: the database initialised through the rendered scripts
(jwt setting applied, _analytics/_supavisor created, roles' password from
POSTGRES_PASSWORD_FILE); through Kong: REST 200 with the anon key and 401
without, auth health and settings 200, storage buckets 200, GraphQL 200, pg-meta
200, an edge function 200, Studio 401 without and 200 with the dashboard login,
realtime tenant health 200; the pooler in session and transaction mode as
postgres.zurag; a tenant set to host "db" was repointed to supabase-db by the
bootstrap and connections worked.
2026-09-30 12:12:27 +02:00

539 lines
47 KiB
JSON

{
"module": "supabase",
"version": "1",
"capabilities": [
"container-runtime"
],
"listens": [
{
"name": "api",
"port": 8000,
"protocol": "tcp",
"from": "mesh",
"why": "the Kong gateway: the REST, auth, storage, realtime, functions and GraphQL APIs under their /\u2026/v1 paths, and Studio at / behind the dashboard password. The one surface people and apps use, reached through the route"
},
{
"name": "pooler-session",
"port": 5432,
"protocol": "tcp",
"from": "mesh",
"why": "Supavisor's session-mode Postgres port, for clients that connect to the database directly as <user>.<tenant>"
},
{
"name": "pooler-transaction",
"port": 6543,
"protocol": "tcp",
"from": "mesh",
"why": "Supavisor's transaction-mode Postgres port"
}
],
"requires": [
"route",
"secret"
],
"contributes": {
"route": {
"label": "supabase",
"endpoint": "api"
}
},
"binds": {
"route": "${dir:state}/route.json"
},
"secrets": {
"secret": {
"postgres": "${dir:state}/postgres.secret",
"jwt": "${dir:state}/jwt.secret",
"anon-key": "${dir:state}/anon-key.secret",
"service-role-key": "${dir:state}/service-role-key.secret",
"dashboard-user": "${dir:state}/dashboard-user.secret",
"dashboard": "${dir:state}/dashboard.secret",
"logflare": "${dir:state}/logflare.secret",
"pooler-vault": "${dir:state}/pooler-vault.secret",
"key-base-a": "${dir:state}/key-base-a.secret",
"key-base-b": "${dir:state}/key-base-b.secret",
"openai": "${dir:state}/openai.secret"
}
},
"resources": [
{
"id": "state",
"type": "directory",
"mode": "0700",
"place": "."
},
{
"id": "db-init",
"type": "directory",
"path": "${dir:state}/db-init",
"mode": "0755"
},
{
"id": "functions-main-dir",
"type": "directory",
"path": "${dir:state}/functions-main",
"mode": "0755"
},
{
"id": "db-data",
"type": "directory",
"mode": "0700",
"owner": "105:106"
},
{
"id": "db-config",
"type": "directory",
"mode": "0755",
"owner": "105:106"
},
{
"id": "storage",
"type": "directory",
"mode": "0755"
},
{
"id": "functions",
"type": "directory",
"mode": "0755"
},
{
"id": "settings",
"type": "file",
"path": "${dir:state}/settings.json",
"mode": "0644",
"merge": "json",
"content": "{\n \"pooler\": {\n \"tenant\": \"default\",\n \"pool-size\": 20,\n \"max-client-connections\": 100\n }\n}\n"
},
{
"id": "kong-conf",
"type": "file",
"path": "${dir:state}/kong.yml",
"mode": "0600",
"owner": "100:65533",
"content": "# Written by the mesh (modules/supabase): the gateway's declarative config, credentials rendered in.\n_format_version: '2.1'\n_transform: true\n\n###\n### Consumers / Users\n###\nconsumers:\n - username: DASHBOARD\n - username: anon\n keyauth_credentials:\n - key: \"${secret:anon-key}\"\n - username: service_role\n keyauth_credentials:\n - key: \"${secret:service-role-key}\"\n\n###\n### Access Control List\n###\nacls:\n - consumer: anon\n group: anon\n - consumer: service_role\n group: admin\n\n###\n### Dashboard credentials\n###\nbasicauth_credentials:\n - consumer: DASHBOARD\n username: \"${secret:dashboard-user}\"\n password: \"${secret:dashboard}\"\n\n###\n### API Routes\n###\nservices:\n ## Open Auth routes\n - name: auth-v1-open\n url: http://supabase-auth:9999/verify\n routes:\n - name: auth-v1-open\n strip_path: true\n paths:\n - /auth/v1/verify\n plugins:\n - name: cors\n - name: auth-v1-open-callback\n url: http://supabase-auth:9999/callback\n routes:\n - name: auth-v1-open-callback\n strip_path: true\n paths:\n - /auth/v1/callback\n plugins:\n - name: cors\n - name: auth-v1-open-authorize\n url: http://supabase-auth:9999/authorize\n routes:\n - name: auth-v1-open-authorize\n strip_path: true\n paths:\n - /auth/v1/authorize\n plugins:\n - name: cors\n\n ## Secure Auth routes\n - name: auth-v1\n _comment: 'GoTrue: /auth/v1/* -> http://auth:9999/*'\n url: http://supabase-auth:9999/\n routes:\n - name: auth-v1-all\n strip_path: true\n paths:\n - /auth/v1/\n plugins:\n - name: cors\n - name: key-auth\n config:\n hide_credentials: false\n - name: acl\n config:\n hide_groups_header: true\n allow:\n - admin\n - anon\n\n ## Secure REST routes\n - name: rest-v1\n _comment: 'PostgREST: /rest/v1/* -> http://rest:3000/*'\n url: http://supabase-rest:3000/\n routes:\n - name: rest-v1-all\n strip_path: true\n paths:\n - /rest/v1/\n plugins:\n - name: cors\n - name: key-auth\n config:\n hide_credentials: true\n - name: acl\n config:\n hide_groups_header: true\n allow:\n - admin\n - anon\n\n ## Secure GraphQL routes\n - name: graphql-v1\n _comment: 'PostgREST: /graphql/v1/* -> http://rest:3000/rpc/graphql'\n url: http://supabase-rest:3000/rpc/graphql\n routes:\n - name: graphql-v1-all\n strip_path: true\n paths:\n - /graphql/v1\n plugins:\n - name: cors\n - name: key-auth\n config:\n hide_credentials: true\n - name: request-transformer\n config:\n add:\n headers:\n - Content-Profile:graphql_public\n - name: acl\n config:\n hide_groups_header: true\n allow:\n - admin\n - anon\n\n ## Secure Realtime routes\n - name: realtime-v1-ws\n _comment: 'Realtime: /realtime/v1/* -> ws://realtime:4000/socket/*'\n url: http://realtime-dev.supabase-realtime:4000/socket\n protocol: ws\n routes:\n - name: realtime-v1-ws\n strip_path: true\n paths:\n - /realtime/v1/\n plugins:\n - name: cors\n - name: key-auth\n config:\n hide_credentials: false\n - name: acl\n config:\n hide_groups_header: true\n allow:\n - admin\n - anon\n - name: realtime-v1-rest\n _comment: 'Realtime: /realtime/v1/* -> ws://realtime:4000/socket/*'\n url: http://realtime-dev.supabase-realtime:4000/api\n protocol: http\n routes:\n - name: realtime-v1-rest\n strip_path: true\n paths:\n - /realtime/v1/api\n plugins:\n - name: cors\n - name: key-auth\n config:\n hide_credentials: false\n - name: acl\n config:\n hide_groups_header: true\n allow:\n - admin\n - anon\n ## Storage routes: the storage server manages its own auth\n - name: storage-v1\n _comment: 'Storage: /storage/v1/* -> http://storage:5000/*'\n url: http://supabase-storage:5000/\n routes:\n - name: storage-v1-all\n strip_path: true\n paths:\n - /storage/v1/\n plugins:\n - name: cors\n\n ## Edge Functions routes\n - name: functions-v1\n _comment: 'Edge Functions: /functions/v1/* -> http://functions:9000/*'\n url: http://supabase-edge-functions:9000/\n routes:\n - name: functions-v1-all\n strip_path: true\n paths:\n - /functions/v1/\n plugins:\n - name: cors\n\n ## Analytics routes\n - name: analytics-v1\n _comment: 'Analytics: /analytics/v1/* -> http://logflare:4000/*'\n url: http://supabase-analytics:4000/\n routes:\n - name: analytics-v1-all\n strip_path: true\n paths:\n - /analytics/v1/\n\n ## Secure Database routes\n - name: meta\n _comment: 'pg-meta: /pg/* -> http://pg-meta:8080/*'\n url: http://supabase-meta:8080/\n routes:\n - name: meta-all\n strip_path: true\n paths:\n - /pg/\n plugins:\n - name: key-auth\n config:\n hide_credentials: false\n - name: acl\n config:\n hide_groups_header: true\n allow:\n - admin\n\n ## Protected Dashboard - catch all remaining routes\n - name: dashboard\n _comment: 'Studio: /* -> http://studio:3000/*'\n url: http://supabase-studio:3000/\n routes:\n - name: dashboard-all\n strip_path: true\n paths:\n - /\n plugins:\n - name: cors\n - name: basic-auth\n config:\n hide_credentials: true\n"
},
{
"id": "auth-conf",
"type": "file",
"path": "${dir:state}/gotrue.env",
"mode": "0600",
"owner": "1000:1000",
"content": "GOTRUE_API_HOST=0.0.0.0\nGOTRUE_API_PORT=9999\nAPI_EXTERNAL_URL=https://${bound:route:name}\nGOTRUE_DB_DRIVER=postgres\nGOTRUE_DB_DATABASE_URL=postgres://supabase_auth_admin:${secret:postgres}@supabase-db:5432/postgres\nGOTRUE_SITE_URL=https://${bound:route:name}\nGOTRUE_URI_ALLOW_LIST=\nGOTRUE_DISABLE_SIGNUP=true\nGOTRUE_JWT_ADMIN_ROLES=service_role\nGOTRUE_JWT_AUD=authenticated\nGOTRUE_JWT_DEFAULT_GROUP_NAME=authenticated\nGOTRUE_JWT_EXP=3600\nGOTRUE_JWT_SECRET=${secret:jwt}\nGOTRUE_EXTERNAL_EMAIL_ENABLED=false\nGOTRUE_EXTERNAL_ANONYMOUS_USERS_ENABLED=false\nGOTRUE_MAILER_AUTOCONFIRM=false\nGOTRUE_SMTP_ADMIN_EMAIL=admin@example.com\nGOTRUE_SMTP_HOST=supabase-mail\nGOTRUE_SMTP_PORT=2500\nGOTRUE_SMTP_USER=fake_mail_user\nGOTRUE_SMTP_PASS=fake_mail_password\nGOTRUE_SMTP_SENDER_NAME=fake_sender\nGOTRUE_MAILER_URLPATHS_INVITE=/auth/v1/verify\nGOTRUE_MAILER_URLPATHS_CONFIRMATION=/auth/v1/verify\nGOTRUE_MAILER_URLPATHS_RECOVERY=/auth/v1/verify\nGOTRUE_MAILER_URLPATHS_EMAIL_CHANGE=/auth/v1/verify\nGOTRUE_EXTERNAL_PHONE_ENABLED=false\nGOTRUE_SMS_AUTOCONFIRM=true\n"
},
{
"id": "rest-conf",
"type": "file",
"path": "${dir:state}/postgrest.conf",
"mode": "0600",
"owner": "1000:1000",
"content": "db-uri = \"postgres://authenticator:${secret:postgres}@supabase-db:5432/postgres\"\ndb-schemas = \"public,storage,graphql_public\"\ndb-anon-role = \"anon\"\njwt-secret = \"${secret:jwt}\"\ndb-use-legacy-gucs = false\napp.settings.jwt_secret = \"${secret:jwt}\"\napp.settings.jwt_exp = \"3600\"\n"
},
{
"id": "vector-conf",
"type": "file",
"path": "${dir:state}/vector.yml",
"mode": "0600",
"content": "# Written by the mesh (modules/supabase).\napi:\n enabled: true\n address: 0.0.0.0:9001\n\nsources:\n docker_host:\n type: docker_logs\n include_containers:\n - supabase-kong\n - supabase-auth\n - supabase-rest\n - realtime-dev.supabase-realtime\n - supabase-storage\n - supabase-edge-functions\n - supabase-db\n\ntransforms:\n project_logs:\n type: remap\n inputs:\n - docker_host\n source: |-\n .project = \"default\"\n .event_message = del(.message)\n .appname = del(.container_name)\n del(.container_created_at)\n del(.container_id)\n del(.source_type)\n del(.stream)\n del(.label)\n del(.image)\n del(.host)\n del(.stream)\n router:\n type: route\n inputs:\n - project_logs\n route:\n kong: '.appname == \"supabase-kong\"'\n auth: '.appname == \"supabase-auth\"'\n rest: '.appname == \"supabase-rest\"'\n realtime: '.appname == \"realtime-dev.supabase-realtime\"'\n storage: '.appname == \"supabase-storage\"'\n functions: '.appname == \"supabase-edge-functions\"'\n db: '.appname == \"supabase-db\"'\n # Ignores non nginx errors since they are related with kong booting up\n kong_logs:\n type: remap\n inputs:\n - router.kong\n source: |-\n req, err = parse_nginx_log(.event_message, \"combined\")\n if err == null {\n .timestamp = req.timestamp\n .metadata.request.headers.referer = req.referer\n .metadata.request.headers.user_agent = req.agent\n .metadata.request.headers.cf_connecting_ip = req.client\n .metadata.request.method = req.method\n .metadata.request.path = req.path\n .metadata.request.protocol = req.protocol\n .metadata.response.status_code = req.status\n }\n if err != null {\n abort\n }\n # Ignores non nginx errors since they are related with kong booting up\n kong_err:\n type: remap\n inputs:\n - router.kong\n source: |-\n .metadata.request.method = \"GET\"\n .metadata.response.status_code = 200\n parsed, err = parse_nginx_log(.event_message, \"error\")\n if err == null {\n .timestamp = parsed.timestamp\n .severity = parsed.severity\n .metadata.request.host = parsed.host\n .metadata.request.headers.cf_connecting_ip = parsed.client\n url, err = split(parsed.request, \" \")\n if err == null {\n .metadata.request.method = url[0]\n .metadata.request.path = url[1]\n .metadata.request.protocol = url[2]\n }\n }\n if err != null {\n abort\n }\n # Gotrue logs are structured json strings which frontend parses directly. But we keep metadata for consistency.\n auth_logs:\n type: remap\n inputs:\n - router.auth\n source: |-\n parsed, err = parse_json(.event_message)\n if err == null {\n .metadata.timestamp = parsed.time\n .metadata = merge!(.metadata, parsed)\n }\n # PostgREST logs are structured so we separate timestamp from message using regex\n rest_logs:\n type: remap\n inputs:\n - router.rest\n source: |-\n parsed, err = parse_regex(.event_message, r'^(?P<time>.*): (?P<msg>.*)$')\n if err == null {\n .event_message = parsed.msg\n .timestamp = to_timestamp!(parsed.time)\n .metadata.host = .project\n }\n # Realtime logs are structured so we parse the severity level using regex (ignore time because it has no date)\n realtime_logs:\n type: remap\n inputs:\n - router.realtime\n source: |-\n .metadata.project = del(.project)\n .metadata.external_id = .metadata.project\n parsed, err = parse_regex(.event_message, r'^(?P<time>\\d+:\\d+:\\d+\\.\\d+) \\[(?P<level>\\w+)\\] (?P<msg>.*)$')\n if err == null {\n .event_message = parsed.msg\n .metadata.level = parsed.level\n }\n # Storage logs may contain json objects so we parse them for completeness\n storage_logs:\n type: remap\n inputs:\n - router.storage\n source: |-\n .metadata.project = del(.project)\n .metadata.tenantId = .metadata.project\n parsed, err = parse_json(.event_message)\n if err == null {\n .event_message = parsed.msg\n .metadata.level = parsed.level\n .metadata.timestamp = parsed.time\n .metadata.context[0].host = parsed.hostname\n .metadata.context[0].pid = parsed.pid\n }\n # Postgres logs some messages to stderr which we map to warning severity level\n db_logs:\n type: remap\n inputs:\n - router.db\n source: |-\n .metadata.host = \"db-default\"\n .metadata.parsed.timestamp = .timestamp\n\n parsed, err = parse_regex(.event_message, r'.*(?P<level>INFO|NOTICE|WARNING|ERROR|LOG|FATAL|PANIC?):.*', numeric_groups: true)\n\n if err != null || parsed == null {\n .metadata.parsed.error_severity = \"info\"\n }\n if parsed != null {\n .metadata.parsed.error_severity = parsed.level\n }\n if .metadata.parsed.error_severity == \"info\" {\n .metadata.parsed.error_severity = \"log\"\n }\n .metadata.parsed.error_severity = upcase!(.metadata.parsed.error_severity)\n\nsinks:\n logflare_auth:\n type: 'http'\n inputs:\n - auth_logs\n encoding:\n codec: 'json'\n method: 'post'\n request:\n retry_max_duration_secs: 10\n uri: 'http://supabase-analytics:4000/api/logs?source_name=gotrue.logs.prod&api_key=${secret:logflare}'\n logflare_realtime:\n type: 'http'\n inputs:\n - realtime_logs\n encoding:\n codec: 'json'\n method: 'post'\n request:\n retry_max_duration_secs: 10\n uri: 'http://supabase-analytics:4000/api/logs?source_name=realtime.logs.prod&api_key=${secret:logflare}'\n logflare_rest:\n type: 'http'\n inputs:\n - rest_logs\n encoding:\n codec: 'json'\n method: 'post'\n request:\n retry_max_duration_secs: 10\n uri: 'http://supabase-analytics:4000/api/logs?source_name=postgREST.logs.prod&api_key=${secret:logflare}'\n logflare_db:\n type: 'http'\n inputs:\n - db_logs\n encoding:\n codec: 'json'\n method: 'post'\n request:\n retry_max_duration_secs: 10\n # We must route the sink through kong because ingesting logs before logflare is fully initialised will\n # lead to broken queries from studio. This works by the assumption that containers are started in the\n # following order: vector > db > logflare > kong\n uri: 'http://supabase-kong:8000/analytics/v1/api/logs?source_name=postgres.logs&api_key=${secret:logflare}'\n logflare_functions:\n type: 'http'\n inputs:\n - router.functions\n encoding:\n codec: 'json'\n method: 'post'\n request:\n retry_max_duration_secs: 10\n uri: 'http://supabase-analytics:4000/api/logs?source_name=deno-relay-logs&api_key=${secret:logflare}'\n logflare_storage:\n type: 'http'\n inputs:\n - storage_logs\n encoding:\n codec: 'json'\n method: 'post'\n request:\n retry_max_duration_secs: 10\n uri: 'http://supabase-analytics:4000/api/logs?source_name=storage.logs.prod.2&api_key=${secret:logflare}'\n logflare_kong:\n type: 'http'\n inputs:\n - kong_logs\n - kong_err\n encoding:\n codec: 'json'\n method: 'post'\n request:\n retry_max_duration_secs: 10\n uri: 'http://supabase-analytics:4000/api/logs?source_name=cloudflare.logs.prod&api_key=${secret:logflare}'\n"
},
{
"id": "pooler-conf",
"type": "file",
"path": "${dir:state}/pooler.exs",
"mode": "0644",
"content": "{:ok, _} = Application.ensure_all_started(:supavisor)\n\n# Written by the mesh (modules/supabase). Per-machine values come from settings.json (the module's\n# one merge:json file); secrets come from the environment the image reads.\nsettings = \"/etc/pooler/settings.json\" |> File.read!() |> Jason.decode!() |> Map.get(\"pooler\", %{})\n\n{:ok, version} =\n case Supavisor.Repo.query!(\"select version()\") do\n %{rows: [[ver]]} -> Supavisor.Helpers.parse_pg_version(ver)\n _ -> nil\n end\n\nparams = %{\n \"external_id\" => to_string(settings[\"tenant\"] || \"default\"),\n \"db_host\" => \"supabase-db\",\n \"db_port\" => System.get_env(\"POSTGRES_PORT\"),\n \"db_database\" => System.get_env(\"POSTGRES_DB\"),\n \"require_user\" => false,\n \"auth_query\" => \"SELECT * FROM pgbouncer.get_auth($1)\",\n \"default_max_clients\" => settings[\"max-client-connections\"] || 100,\n \"default_pool_size\" => settings[\"pool-size\"] || 20,\n \"default_parameter_status\" => %{\"server_version\" => version},\n \"users\" => [%{\n \"db_user\" => \"pgbouncer\",\n \"db_password\" => System.get_env(\"POSTGRES_PASSWORD\"),\n \"mode_type\" => System.get_env(\"POOLER_POOL_MODE\"),\n \"pool_size\" => settings[\"pool-size\"] || 20,\n \"is_manager\" => true\n }]\n}\n\ncase Supavisor.Tenants.get_tenant_by_external_id(params[\"external_id\"]) do\n nil ->\n {:ok, _} = Supavisor.Tenants.create_tenant(params)\n\n # A tenant made elsewhere (HAL named the database host \"db\") is pointed at this module's\n # database container; nothing else it holds is changed.\n %{db_host: host} = tenant when host != \"supabase-db\" ->\n {:ok, _} = Supavisor.Tenants.update_tenant(tenant, %{\"db_host\" => \"supabase-db\"})\n\n _ ->\n :ok\nend\n"
},
{
"id": "db-init-roles",
"type": "file",
"path": "${dir:state}/db-init/roles.sql",
"mode": "0600",
"owner": "105:106",
"content": "\\set pgpass `echo \"$POSTGRES_PASSWORD\"`\n\nALTER USER authenticator WITH PASSWORD :'pgpass';\nALTER USER pgbouncer WITH PASSWORD :'pgpass';\nALTER USER supabase_auth_admin WITH PASSWORD :'pgpass';\nALTER USER supabase_functions_admin WITH PASSWORD :'pgpass';\nALTER USER supabase_storage_admin WITH PASSWORD :'pgpass';\n"
},
{
"id": "db-init-jwt",
"type": "file",
"path": "${dir:state}/db-init/jwt.sql",
"mode": "0600",
"owner": "105:106",
"content": "ALTER DATABASE postgres SET \"app.settings.jwt_secret\" TO '${secret:jwt}';\nALTER DATABASE postgres SET \"app.settings.jwt_exp\" TO '3600';\n"
},
{
"id": "db-init-realtime",
"type": "file",
"path": "${dir:state}/db-init/realtime.sql",
"mode": "0600",
"owner": "105:106",
"content": "\\set pguser `echo \"$POSTGRES_USER\"`\n\ncreate schema if not exists _realtime;\nalter schema _realtime owner to :pguser;\n"
},
{
"id": "db-init-supabase",
"type": "file",
"path": "${dir:state}/db-init/supabase.sql",
"mode": "0600",
"owner": "105:106",
"content": "\\set pguser `echo \"$POSTGRES_USER\"`\n\nCREATE DATABASE _supabase WITH OWNER :pguser;\n"
},
{
"id": "db-init-logs",
"type": "file",
"path": "${dir:state}/db-init/logs.sql",
"mode": "0600",
"owner": "105:106",
"content": "\\set pguser `echo \"$POSTGRES_USER\"`\n\n\\c _supabase\ncreate schema if not exists _analytics;\nalter schema _analytics owner to :pguser;\n\\c postgres\n"
},
{
"id": "db-init-pooler",
"type": "file",
"path": "${dir:state}/db-init/pooler.sql",
"mode": "0600",
"owner": "105:106",
"content": "\\set pguser `echo \"$POSTGRES_USER\"`\n\n\\c _supabase\ncreate schema if not exists _supavisor;\nalter schema _supavisor owner to :pguser;\n\\c postgres\n"
},
{
"id": "db-init-webhooks",
"type": "file",
"path": "${dir:state}/db-init/webhooks.sql",
"mode": "0600",
"owner": "105:106",
"content": "BEGIN;\n -- Create pg_net extension\n CREATE EXTENSION IF NOT EXISTS pg_net SCHEMA extensions;\n -- Create supabase_functions schema\n CREATE SCHEMA supabase_functions AUTHORIZATION supabase_admin;\n GRANT USAGE ON SCHEMA supabase_functions TO postgres, anon, authenticated, service_role;\n ALTER DEFAULT PRIVILEGES IN SCHEMA supabase_functions GRANT ALL ON TABLES TO postgres, anon, authenticated, service_role;\n ALTER DEFAULT PRIVILEGES IN SCHEMA supabase_functions GRANT ALL ON FUNCTIONS TO postgres, anon, authenticated, service_role;\n ALTER DEFAULT PRIVILEGES IN SCHEMA supabase_functions GRANT ALL ON SEQUENCES TO postgres, anon, authenticated, service_role;\n -- supabase_functions.migrations definition\n CREATE TABLE supabase_functions.migrations (\n version text PRIMARY KEY,\n inserted_at timestamptz NOT NULL DEFAULT NOW()\n );\n -- Initial supabase_functions migration\n INSERT INTO supabase_functions.migrations (version) VALUES ('initial');\n -- supabase_functions.hooks definition\n CREATE TABLE supabase_functions.hooks (\n id bigserial PRIMARY KEY,\n hook_table_id integer NOT NULL,\n hook_name text NOT NULL,\n created_at timestamptz NOT NULL DEFAULT NOW(),\n request_id bigint\n );\n CREATE INDEX supabase_functions_hooks_request_id_idx ON supabase_functions.hooks USING btree (request_id);\n CREATE INDEX supabase_functions_hooks_h_table_id_h_name_idx ON supabase_functions.hooks USING btree (hook_table_id, hook_name);\n COMMENT ON TABLE supabase_functions.hooks IS 'Supabase Functions Hooks: Audit trail for triggered hooks.';\n CREATE FUNCTION supabase_functions.http_request()\n RETURNS trigger\n LANGUAGE plpgsql\n AS $function$\n DECLARE\n request_id bigint;\n payload jsonb;\n url text := TG_ARGV[0]::text;\n method text := TG_ARGV[1]::text;\n headers jsonb DEFAULT '{}'::jsonb;\n params jsonb DEFAULT '{}'::jsonb;\n timeout_ms integer DEFAULT 1000;\n BEGIN\n IF url IS NULL OR url = 'null' THEN\n RAISE EXCEPTION 'url argument is missing';\n END IF;\n\n IF method IS NULL OR method = 'null' THEN\n RAISE EXCEPTION 'method argument is missing';\n END IF;\n\n IF TG_ARGV[2] IS NULL OR TG_ARGV[2] = 'null' THEN\n headers = '{\"Content-Type\": \"application/json\"}'::jsonb;\n ELSE\n headers = TG_ARGV[2]::jsonb;\n END IF;\n\n IF TG_ARGV[3] IS NULL OR TG_ARGV[3] = 'null' THEN\n params = '{}'::jsonb;\n ELSE\n params = TG_ARGV[3]::jsonb;\n END IF;\n\n IF TG_ARGV[4] IS NULL OR TG_ARGV[4] = 'null' THEN\n timeout_ms = 1000;\n ELSE\n timeout_ms = TG_ARGV[4]::integer;\n END IF;\n\n CASE\n WHEN method = 'GET' THEN\n SELECT http_get INTO request_id FROM net.http_get(\n url,\n params,\n headers,\n timeout_ms\n );\n WHEN method = 'POST' THEN\n payload = jsonb_build_object(\n 'old_record', OLD,\n 'record', NEW,\n 'type', TG_OP,\n 'table', TG_TABLE_NAME,\n 'schema', TG_TABLE_SCHEMA\n );\n\n SELECT http_post INTO request_id FROM net.http_post(\n url,\n payload,\n params,\n headers,\n timeout_ms\n );\n ELSE\n RAISE EXCEPTION 'method argument % is invalid', method;\n END CASE;\n\n INSERT INTO supabase_functions.hooks\n (hook_table_id, hook_name, request_id)\n VALUES\n (TG_RELID, TG_NAME, request_id);\n\n RETURN NEW;\n END\n $function$;\n -- Supabase super admin\n DO\n $$\n BEGIN\n IF NOT EXISTS (\n SELECT 1\n FROM pg_roles\n WHERE rolname = 'supabase_functions_admin'\n )\n THEN\n CREATE USER supabase_functions_admin NOINHERIT CREATEROLE LOGIN NOREPLICATION;\n END IF;\n END\n $$;\n GRANT ALL PRIVILEGES ON SCHEMA supabase_functions TO supabase_functions_admin;\n GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA supabase_functions TO supabase_functions_admin;\n GRANT ALL PRIVILEGES ON ALL SEQUENCES IN SCHEMA supabase_functions TO supabase_functions_admin;\n ALTER USER supabase_functions_admin SET search_path = \"supabase_functions\";\n ALTER table \"supabase_functions\".migrations OWNER TO supabase_functions_admin;\n ALTER table \"supabase_functions\".hooks OWNER TO supabase_functions_admin;\n ALTER function \"supabase_functions\".http_request() OWNER TO supabase_functions_admin;\n GRANT supabase_functions_admin TO postgres;\n -- Remove unused supabase_pg_net_admin role\n DO\n $$\n BEGIN\n IF EXISTS (\n SELECT 1\n FROM pg_roles\n WHERE rolname = 'supabase_pg_net_admin'\n )\n THEN\n REASSIGN OWNED BY supabase_pg_net_admin TO supabase_admin;\n DROP OWNED BY supabase_pg_net_admin;\n DROP ROLE supabase_pg_net_admin;\n END IF;\n END\n $$;\n -- pg_net grants when extension is already enabled\n DO\n $$\n BEGIN\n IF EXISTS (\n SELECT 1\n FROM pg_extension\n WHERE extname = 'pg_net'\n )\n THEN\n GRANT USAGE ON SCHEMA net TO supabase_functions_admin, postgres, anon, authenticated, service_role;\n ALTER function net.http_get(url text, params jsonb, headers jsonb, timeout_milliseconds integer) SECURITY DEFINER;\n ALTER function net.http_post(url text, body jsonb, params jsonb, headers jsonb, timeout_milliseconds integer) SECURITY DEFINER;\n ALTER function net.http_get(url text, params jsonb, headers jsonb, timeout_milliseconds integer) SET search_path = net;\n ALTER function net.http_post(url text, body jsonb, params jsonb, headers jsonb, timeout_milliseconds integer) SET search_path = net;\n REVOKE ALL ON FUNCTION net.http_get(url text, params jsonb, headers jsonb, timeout_milliseconds integer) FROM PUBLIC;\n REVOKE ALL ON FUNCTION net.http_post(url text, body jsonb, params jsonb, headers jsonb, timeout_milliseconds integer) FROM PUBLIC;\n 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;\n 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;\n END IF;\n END\n $$;\n -- Event trigger for pg_net\n CREATE OR REPLACE FUNCTION extensions.grant_pg_net_access()\n RETURNS event_trigger\n LANGUAGE plpgsql\n AS $$\n BEGIN\n IF EXISTS (\n SELECT 1\n FROM pg_event_trigger_ddl_commands() AS ev\n JOIN pg_extension AS ext\n ON ev.objid = ext.oid\n WHERE ext.extname = 'pg_net'\n )\n THEN\n GRANT USAGE ON SCHEMA net TO supabase_functions_admin, postgres, anon, authenticated, service_role;\n ALTER function net.http_get(url text, params jsonb, headers jsonb, timeout_milliseconds integer) SECURITY DEFINER;\n ALTER function net.http_post(url text, body jsonb, params jsonb, headers jsonb, timeout_milliseconds integer) SECURITY DEFINER;\n ALTER function net.http_get(url text, params jsonb, headers jsonb, timeout_milliseconds integer) SET search_path = net;\n ALTER function net.http_post(url text, body jsonb, params jsonb, headers jsonb, timeout_milliseconds integer) SET search_path = net;\n REVOKE ALL ON FUNCTION net.http_get(url text, params jsonb, headers jsonb, timeout_milliseconds integer) FROM PUBLIC;\n REVOKE ALL ON FUNCTION net.http_post(url text, body jsonb, params jsonb, headers jsonb, timeout_milliseconds integer) FROM PUBLIC;\n 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;\n 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;\n END IF;\n END;\n $$;\n COMMENT ON FUNCTION extensions.grant_pg_net_access IS 'Grants access to pg_net';\n DO\n $$\n BEGIN\n IF NOT EXISTS (\n SELECT 1\n FROM pg_event_trigger\n WHERE evtname = 'issue_pg_net_access'\n ) THEN\n CREATE EVENT TRIGGER issue_pg_net_access ON ddl_command_end WHEN TAG IN ('CREATE EXTENSION')\n EXECUTE PROCEDURE extensions.grant_pg_net_access();\n END IF;\n END\n $$;\n INSERT INTO supabase_functions.migrations (version) VALUES ('20210809183423_update_grants');\n ALTER function supabase_functions.http_request() SECURITY DEFINER;\n ALTER function supabase_functions.http_request() SET search_path = supabase_functions;\n REVOKE ALL ON FUNCTION supabase_functions.http_request() FROM PUBLIC;\n GRANT EXECUTE ON FUNCTION supabase_functions.http_request() TO postgres, anon, authenticated, service_role;\nCOMMIT;\n"
},
{
"id": "functions-main",
"type": "file",
"path": "${dir:state}/functions-main/index.ts",
"mode": "0644",
"content": "import { serve } from 'https://deno.land/std@0.131.0/http/server.ts'\nimport * as jose from 'https://deno.land/x/jose@v4.14.4/index.ts'\n\nconsole.log('main function started')\n\nconst JWT_SECRET = Deno.env.get('JWT_SECRET')\nconst VERIFY_JWT = Deno.env.get('VERIFY_JWT') === 'true'\n\nfunction getAuthToken(req: Request) {\n const authHeader = req.headers.get('authorization')\n if (!authHeader) {\n throw new Error('Missing authorization header')\n }\n const [bearer, token] = authHeader.split(' ')\n if (bearer !== 'Bearer') {\n throw new Error(`Auth header is not 'Bearer {token}'`)\n }\n return token\n}\n\nasync function verifyJWT(jwt: string): Promise<boolean> {\n const encoder = new TextEncoder()\n const secretKey = encoder.encode(JWT_SECRET)\n try {\n await jose.jwtVerify(jwt, secretKey)\n } catch (err) {\n console.error(err)\n return false\n }\n return true\n}\n\nserve(async (req: Request) => {\n if (req.method !== 'OPTIONS' && VERIFY_JWT) {\n try {\n const token = getAuthToken(req)\n const isValidJWT = await verifyJWT(token)\n\n if (!isValidJWT) {\n return new Response(JSON.stringify({ msg: 'Invalid JWT' }), {\n status: 401,\n headers: { 'Content-Type': 'application/json' },\n })\n }\n } catch (e) {\n console.error(e)\n return new Response(JSON.stringify({ msg: e.toString() }), {\n status: 401,\n headers: { 'Content-Type': 'application/json' },\n })\n }\n }\n\n const url = new URL(req.url)\n const { pathname } = url\n const path_parts = pathname.split('/')\n const service_name = path_parts[1]\n\n if (!service_name || service_name === '') {\n const error = { msg: 'missing function name in request' }\n return new Response(JSON.stringify(error), {\n status: 400,\n headers: { 'Content-Type': 'application/json' },\n })\n }\n\n const servicePath = `/home/deno/functions/${service_name}`\n console.error(`serving the request with ${servicePath}`)\n\n const memoryLimitMb = 150\n const workerTimeoutMs = 1 * 60 * 1000\n const noModuleCache = false\n const importMapPath = null\n const envVarsObj = Deno.env.toObject()\n const envVars = Object.keys(envVarsObj).map((k) => [k, envVarsObj[k]])\n\n try {\n const worker = await EdgeRuntime.userWorkers.create({\n servicePath,\n memoryLimitMb,\n workerTimeoutMs,\n noModuleCache,\n importMapPath,\n envVars,\n })\n return await worker.fetch(req)\n } catch (e) {\n const error = { msg: e.toString() }\n return new Response(JSON.stringify(error), {\n status: 500,\n headers: { 'Content-Type': 'application/json' },\n })\n }\n})\n"
},
{
"id": "realtime-env",
"type": "file",
"path": "${dir:state}/realtime-env.env",
"mode": "0600",
"content": "PORT=4000\nDB_HOST=supabase-db\nDB_PORT=5432\nDB_USER=supabase_admin\nDB_PASSWORD=${secret:postgres}\nDB_NAME=postgres\nDB_AFTER_CONNECT_QUERY=SET search_path TO _realtime\nDB_ENC_KEY=supabaserealtime\nAPI_JWT_SECRET=${secret:jwt}\nSECRET_KEY_BASE=${secret:key-base-a}${secret:key-base-b}\nERL_AFLAGS=-proto_dist inet_tcp\nDNS_NODES=''\nRLIMIT_NOFILE=10000\nAPP_NAME=realtime\nSEED_SELF_HOST=true\nRUN_JANITOR=true\n"
},
{
"id": "storage-env",
"type": "file",
"path": "${dir:state}/storage-env.env",
"mode": "0600",
"content": "ANON_KEY=${secret:anon-key}\nSERVICE_KEY=${secret:service-role-key}\nPOSTGREST_URL=http://supabase-rest:3000\nPGRST_JWT_SECRET=${secret:jwt}\nDATABASE_URL=postgres://supabase_storage_admin:${secret:postgres}@supabase-db:5432/postgres\nFILE_SIZE_LIMIT=52428800\nSTORAGE_BACKEND=file\nFILE_STORAGE_BACKEND_PATH=/var/lib/storage\nTENANT_ID=stub\nREGION=stub\nGLOBAL_S3_BUCKET=stub\nENABLE_IMAGE_TRANSFORMATION=true\nIMGPROXY_URL=http://supabase-imgproxy:5001\n"
},
{
"id": "meta-env",
"type": "file",
"path": "${dir:state}/meta-env.env",
"mode": "0600",
"content": "PG_META_PORT=8080\nPG_META_DB_HOST=supabase-db\nPG_META_DB_PORT=5432\nPG_META_DB_NAME=postgres\nPG_META_DB_USER=supabase_admin\nPG_META_DB_PASSWORD=${secret:postgres}\n"
},
{
"id": "functions-env",
"type": "file",
"path": "${dir:state}/functions-env.env",
"mode": "0600",
"content": "JWT_SECRET=${secret:jwt}\nSUPABASE_URL=http://supabase-kong:8000\nSUPABASE_ANON_KEY=${secret:anon-key}\nSUPABASE_SERVICE_ROLE_KEY=${secret:service-role-key}\nSUPABASE_DB_URL=postgresql://postgres:${secret:postgres}@supabase-db:5432/postgres\nVERIFY_JWT=false\n"
},
{
"id": "analytics-env",
"type": "file",
"path": "${dir:state}/analytics-env.env",
"mode": "0600",
"content": "LOGFLARE_NODE_HOST=127.0.0.1\nDB_USERNAME=supabase_admin\nDB_DATABASE=_supabase\nDB_HOSTNAME=supabase-db\nDB_PORT=5432\nDB_PASSWORD=${secret:postgres}\nDB_SCHEMA=_analytics\nLOGFLARE_API_KEY=${secret:logflare}\nLOGFLARE_SINGLE_TENANT=true\nLOGFLARE_SUPABASE_MODE=true\nLOGFLARE_MIN_CLUSTER_SIZE=1\nPOSTGRES_BACKEND_URL=postgresql://supabase_admin:${secret:postgres}@supabase-db:5432/_supabase\nPOSTGRES_BACKEND_SCHEMA=_analytics\nLOGFLARE_FEATURE_FLAG_OVERRIDE=multibackend=true\n"
},
{
"id": "studio-env",
"type": "file",
"path": "${dir:state}/studio-env.env",
"mode": "0600",
"content": "STUDIO_PG_META_URL=http://supabase-meta:8080\nPOSTGRES_PASSWORD=${secret:postgres}\nDEFAULT_ORGANIZATION_NAME=Default Organization\nDEFAULT_PROJECT_NAME=Default Project\nOPENAI_API_KEY=${secret:openai}\nSUPABASE_URL=http://supabase-kong:8000\nSUPABASE_PUBLIC_URL=https://${bound:route:name}\nSUPABASE_ANON_KEY=${secret:anon-key}\nSUPABASE_SERVICE_KEY=${secret:service-role-key}\nAUTH_JWT_SECRET=${secret:jwt}\nLOGFLARE_API_KEY=${secret:logflare}\nLOGFLARE_URL=http://supabase-analytics:4000\nNEXT_PUBLIC_ENABLE_LOGS=true\nNEXT_ANALYTICS_BACKEND_PROVIDER=postgres\n"
},
{
"id": "pooler-env",
"type": "file",
"path": "${dir:state}/pooler-env.env",
"mode": "0600",
"content": "PORT=4000\nPOSTGRES_PORT=5432\nPOSTGRES_DB=postgres\nPOSTGRES_PASSWORD=${secret:postgres}\nDATABASE_URL=ecto://supabase_admin:${secret:postgres}@supabase-db:5432/_supabase\nCLUSTER_POSTGRES=true\nSECRET_KEY_BASE=${secret:key-base-a}${secret:key-base-b}\nVAULT_ENC_KEY=${secret:pooler-vault}\nAPI_JWT_SECRET=${secret:jwt}\nMETRICS_JWT_SECRET=${secret:jwt}\nREGION=local\nERL_AFLAGS=-proto_dist inet_tcp\nRELEASE_NODE=supavisor@127.0.0.1\nPOOLER_POOL_MODE=transaction\n"
},
{
"id": "net",
"type": "network",
"name": "supabase"
},
{
"id": "vector",
"type": "container",
"name": "supabase-vector",
"image": "timberio/vector@sha256:4bc04aca94a44f04b427a490f346e7397ef7ce61fe589d718f744f7d92cb5c80",
"network": "supabase",
"args": [
"--config",
"/etc/vector/vector.yml"
],
"volumes": [
"${dir:state}/vector.yml:/etc/vector/vector.yml:ro",
"/var/run/docker.sock:/var/run/docker.sock:ro"
],
"restart-on": [
"vector-conf"
]
},
{
"id": "db-config-seed",
"type": "container",
"name": "supabase-db-config-seed",
"image": "supabase/postgres@sha256:ee29d0aaff03d0e12c7aa63437cba25f24246872f0ed3f82a9fd13184d780777",
"network": "supabase",
"run-once": true,
"args": [
"sh",
"-c",
"cp -rn /etc/postgresql-custom/. /seed/ && chown -R postgres:postgres /seed"
],
"volumes": [
"${dir:db-config}:/seed"
]
},
{
"id": "db",
"type": "container",
"name": "supabase-db",
"image": "supabase/postgres@sha256:ee29d0aaff03d0e12c7aa63437cba25f24246872f0ed3f82a9fd13184d780777",
"network": "supabase",
"args": [
"postgres",
"-c",
"config_file=/etc/postgresql/postgresql.conf",
"-c",
"log_min_messages=fatal"
],
"env": {
"POSTGRES_HOST": "/var/run/postgresql",
"PGPORT": "5432",
"POSTGRES_PORT": "5432",
"PGDATABASE": "postgres",
"POSTGRES_DB": "postgres",
"POSTGRES_PASSWORD_FILE": "/run/secrets/postgres"
},
"volumes": [
"${dir:state}/postgres.secret:/run/secrets/postgres:ro",
"${dir:state}/db-init/realtime.sql:/docker-entrypoint-initdb.d/migrations/99-realtime.sql:ro",
"${dir:state}/db-init/webhooks.sql:/docker-entrypoint-initdb.d/init-scripts/98-webhooks.sql:ro",
"${dir:state}/db-init/roles.sql:/docker-entrypoint-initdb.d/init-scripts/99-roles.sql:ro",
"${dir:state}/db-init/jwt.sql:/docker-entrypoint-initdb.d/init-scripts/99-jwt.sql:ro",
"${dir:state}/db-init/supabase.sql:/docker-entrypoint-initdb.d/migrations/97-_supabase.sql:ro",
"${dir:state}/db-init/logs.sql:/docker-entrypoint-initdb.d/migrations/99-logs.sql:ro",
"${dir:state}/db-init/pooler.sql:/docker-entrypoint-initdb.d/migrations/99-pooler.sql:ro",
"${dir:db-data}:/var/lib/postgresql/data",
"${dir:db-config}:/etc/postgresql-custom"
]
},
{
"id": "db-ready",
"type": "container",
"name": "supabase-db-ready",
"image": "supabase/postgres@sha256:ee29d0aaff03d0e12c7aa63437cba25f24246872f0ed3f82a9fd13184d780777",
"network": "supabase",
"run-once": true,
"args": [
"sh",
"-c",
"for i in $(seq 150); do pg_isready -h supabase-db -p 5432 -U postgres && exit 0; sleep 2; done; echo 'the database did not come up' >&2; exit 1"
]
},
{
"id": "analytics",
"type": "container",
"name": "supabase-analytics",
"image": "supabase/logflare@sha256:e693c787ffe1ae17b6e4e920a3cdd212416d3e1f97e1bd7cb5b67de0abbb0264",
"network": "supabase",
"env-file": [
"${dir:state}/analytics-env.env"
],
"secrets-in-environment": "Logflare reads its database password and API key from the environment only"
},
{
"id": "analytics-ready",
"type": "container",
"name": "supabase-analytics-ready",
"image": "supabase/logflare@sha256:e693c787ffe1ae17b6e4e920a3cdd212416d3e1f97e1bd7cb5b67de0abbb0264",
"network": "supabase",
"run-once": true,
"args": [
"sh",
"-c",
"for i in $(seq 150); do curl -fsS -o /dev/null http://supabase-analytics:4000/health && exit 0; sleep 2; done; echo 'analytics did not come up' >&2; exit 1"
]
},
{
"id": "kong",
"type": "container",
"name": "supabase-kong",
"image": "kong@sha256:1b53405d8680a09d6f44494b7990bf7da2ea43f84a258c59717d4539abf09f6d",
"network": "supabase",
"ports": [
"8000"
],
"env": {
"KONG_DATABASE": "off",
"KONG_DECLARATIVE_CONFIG": "/home/kong/kong.yml",
"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"
},
"volumes": [
"${dir:state}/kong.yml:/home/kong/kong.yml:ro"
],
"restart-on": [
"kong-conf"
]
},
{
"id": "auth",
"type": "container",
"name": "supabase-auth",
"image": "supabase/gotrue@sha256:f8b871f46f3f9c5306af00d2fb856fb5479db730bb6d4fd2757b2da8abe953f9",
"network": "supabase",
"args": [
"auth",
"-c",
"/etc/gotrue/gotrue.env"
],
"volumes": [
"${dir:state}/gotrue.env:/etc/gotrue/gotrue.env:ro"
],
"restart-on": [
"auth-conf"
]
},
{
"id": "rest",
"type": "container",
"name": "supabase-rest",
"image": "postgrest/postgrest@sha256:2cf1efd2c9c2e7606610c113cc73e936d8ce9ba089271cb9cbf11aa564bc30c7",
"network": "supabase",
"args": [
"postgrest",
"/etc/postgrest/postgrest.conf"
],
"volumes": [
"${dir:state}/postgrest.conf:/etc/postgrest/postgrest.conf:ro"
],
"restart-on": [
"rest-conf"
]
},
{
"id": "realtime",
"type": "container",
"name": "realtime-dev.supabase-realtime",
"image": "supabase/realtime@sha256:104fe2e4e09a7c81eebe0c39e3a9b9dca4684f55470ca01cb3c080d1d56453a1",
"network": "supabase",
"env-file": [
"${dir:state}/realtime-env.env"
],
"secrets-in-environment": "Supabase Realtime (Elixir release) reads DB_PASSWORD, API_JWT_SECRET and SECRET_KEY_BASE from its environment only; no file or _FILE form"
},
{
"id": "imgproxy",
"type": "container",
"name": "supabase-imgproxy",
"image": "darthsim/imgproxy@sha256:0facd355d50f3be665ebe674486f2b2e9cdaebd3f74404acd9b7fece2f661435",
"network": "supabase",
"env": {
"IMGPROXY_BIND": ":5001",
"IMGPROXY_LOCAL_FILESYSTEM_ROOT": "/",
"IMGPROXY_USE_ETAG": "true",
"IMGPROXY_ENABLE_WEBP_DETECTION": "true"
},
"volumes": [
"${dir:storage}:/var/lib/storage"
]
},
{
"id": "storage",
"type": "container",
"name": "supabase-storage",
"image": "supabase/storage-api@sha256:1e85dad48e8b3e85890a555e5114dc7ee48c2e8be4cfd97dd4e3564b4f104fcd",
"network": "supabase",
"volumes": [
"${dir:storage}:/var/lib/storage"
],
"env-file": [
"${dir:state}/storage-env.env"
],
"secrets-in-environment": "Supabase Storage reads its keys and DATABASE_URL from the environment only"
},
{
"id": "meta",
"type": "container",
"name": "supabase-meta",
"image": "supabase/postgres-meta@sha256:d0a96973e9f1b6303f7500421a459af3d7273b3f9f1db38610899f9f573da7c7",
"network": "supabase",
"env-file": [
"${dir:state}/meta-env.env"
],
"secrets-in-environment": "postgres-meta reads PG_META_DB_PASSWORD from the environment only"
},
{
"id": "functions",
"type": "container",
"name": "supabase-edge-functions",
"image": "supabase/edge-runtime@sha256:6f7ac3b363f6b278ff9235672a81504ad07709a07f62c50cd4fffb6708842ff1",
"network": "supabase",
"args": [
"start",
"--main-service",
"/home/deno/main"
],
"volumes": [
"${dir:functions}:/home/deno/functions",
"${dir:state}/functions-main/index.ts:/home/deno/main/index.ts:ro"
],
"env-file": [
"${dir:state}/functions-env.env"
],
"secrets-in-environment": "the edge runtime's main service hands its own environment (JWT secret, service key, database URL) to every function worker by design; there is no other channel"
},
{
"id": "studio",
"type": "container",
"name": "supabase-studio",
"image": "supabase/studio@sha256:ebf492ba82db1fc9497f56e4cdebf5cddb464b61f6227e21c8f5f3693f9dbbc5",
"network": "supabase",
"env-file": [
"${dir:state}/studio-env.env"
],
"secrets-in-environment": "Studio (Next.js) reads its keys, the database password and the OpenAI key from the environment only"
},
{
"id": "pooler",
"type": "container",
"name": "supabase-pooler",
"image": "supabase/supavisor@sha256:2f24ab556380db7aaf402d2ec5db09d1974739e685781a91a77ab5ae0dadb3a5",
"network": "supabase",
"ports": [
"5432",
"6543"
],
"args": [
"/bin/sh",
"-c",
"/app/bin/migrate && /app/bin/supavisor eval \"$(cat /etc/pooler/pooler.exs)\" && /app/bin/server"
],
"volumes": [
"${dir:state}/pooler.exs:/etc/pooler/pooler.exs:ro",
"${dir:state}/settings.json:/etc/pooler/settings.json:ro"
],
"restart-on": [
"pooler-conf",
"settings"
],
"env-file": [
"${dir:state}/pooler-env.env"
],
"secrets-in-environment": "Supavisor reads DATABASE_URL, SECRET_KEY_BASE, VAULT_ENC_KEY and the JWT secret from the environment only"
}
]
}