diff --git a/modules/supabase/module.json b/modules/supabase/module.json new file mode 100644 index 0000000..95358cd --- /dev/null +++ b/modules/supabase/module.json @@ -0,0 +1,538 @@ +{ + "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 ." + }, + { + "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