commit 6af0989fe79d8460c01fb3e481defbe13192ec05 Author: qzl Date: Wed Jan 28 15:21:06 2026 +0800 chore: initial commit - Supabase stack with local Docker configuration diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..89374a7 --- /dev/null +++ b/.gitignore @@ -0,0 +1,12 @@ +# Local environment files +infra/local/env/*.env +configs/env/*.env +infra/cloud/volcano/env/*.env +!infra/local/env/*.env.example +!configs/env/*.env.example +!infra/cloud/volcano/env/*.env.example +.env +.env.local +.env.*.local +.env.cloud +.env.*.cloud diff --git a/.opencode/opencode.json b/.opencode/opencode.json new file mode 100644 index 0000000..2734f01 --- /dev/null +++ b/.opencode/opencode.json @@ -0,0 +1,11 @@ +{ + "$schema": "https://opencode.ai/config.json", + "mcp": { + "chrome-devtools": { + "enabled": false + }, + "zai-mcp-server": { + "enabled": false + } + } +} diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..ad26777 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,6 @@ +## Docker Startup + +Must use environment file when starting services: +```bash +docker compose --env-file infra/env/.env.local -f infra/local/docker-compose.yml up -d +``` diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..4e03c48 --- /dev/null +++ b/Makefile @@ -0,0 +1,180 @@ +# ============================================ +# Social App Monorepo - 统一命令入口 +# ============================================ +# +# 使用方式: +# make +# +# 常用命令: +# make up 启动本地开发依赖 +# make down 停止并移除容器 +# make logs 查看依赖服务日志 +# make ps 查看运行状态 +# make clean 清理本地数据卷(警告:会丢失数据) +# make env 提示如何配置环境变量 +# make api-dev 启动 FastAPI 后端服务 +# make flutter-dev 启动 Flutter 移动应用 +# make milvus-init 初始化 Milvus 向量数据库 + +.PHONY: help up down logs ps clean env api-dev flutter-dev milvus-init + +# 默认目标:显示帮助 +help: + @echo "Social App Monorepo - 可用命令:" + @echo "" + @echo " 依赖服务管理:" + @echo " make up 启动本地开发依赖(Redis、Milvus、Postgres)" + @echo " make down 停止并移除所有容器" + @echo " make logs 查看依赖服务日志(可用 SERVICE=redis 等指定)" + @echo " make ps 查看容器运行状态" + @echo " make clean 清理本地数据卷(警告:会丢失数据)" + @echo "" + @echo " 应用启动:" + @echo " make api-dev 启动 FastAPI 后端服务" + @echo " make flutter-dev 启动 Flutter 移动应用" + @echo "" + @echo " 配置与初始化:" + @echo " make env 显示如何配置环境变量" + @echo " make milvus-init 初始化 Milvus 向量数据库" + @echo "" + @echo " 示例:" + @echo " make logs SERVICE=redis" + @echo " make logs SERVICE=milvus" + +# ============================================ +# 依赖服务管理 +# ============================================ + +# 启动本地开发依赖 +up: + @echo "🚀 启动本地开发依赖..." + docker compose -f infra/local/docker-compose.yml --env-file infra/local/env/.env up -d + @echo "" + @echo "✅ 依赖服务已启动" + @echo "" + @echo "服务端口:" + @echo " - Redis: 6379" + @echo " - Milvus: 19530 (gRPC) / 19111 (HTTP)" + @echo " - Postgres: 54322" + @echo "" + @echo "检查状态:make ps" + @echo "查看日志:make logs" + +# 停止并移除容器 +down: + @echo "🛑 停止并移除容器..." + docker compose -f infra/local/docker-compose.yml --env-file infra/local/env/.env down + @echo "✅ 容器已停止并移除" + +# 查看日志 +logs: +ifndef SERVICE + @echo "📋 查看所有服务日志(Ctrl+C 退出)..." + docker compose -f infra/local/docker-compose.yml --env-file infra/local/env/.env logs -f +else + @echo "📋 查看 $(SERVICE) 服务日志(Ctrl+C 退出)..." + docker compose -f infra/local/docker-compose.yml --env-file infra/local/env/.env logs -f $(SERVICE) +endif + +# 查看运行状态 +ps: + @echo "📊 容器运行状态:" + docker compose -f infra/local/docker-compose.yml --env-file infra/local/env/.env ps + +# 清理数据卷(警告:会丢失数据) +clean: + @echo "⚠️ 警告:此操作将删除所有本地数据(Redis、Milvus、Postgres)" + @read -p "确认继续?[y/N] " confirm; \ + if [ "$$confirm" = "y" ] || [ "$$confirm" = "Y" ]; then \ + docker compose -f infra/local/docker-compose.yml --env-file infra/local/env/.env down -v; \ + echo "✅ 数据卷已清理"; \ + else \ + echo "❌ 操作已取消"; \ + fi + +# ============================================ +# 配置管理 +# ============================================ + +# 显示环境变量配置说明 +env: + @echo "📝 环境变量配置说明:" + @echo "" + @echo "1. 查看配置规范:" + @echo " cat configs/env/.env.example" + @echo "" + @echo "2. 本地开发配置:" + @echo " mkdir -p infra/local/env" + @echo " cp configs/env/.env.example configs/env/.env" + @echo " cp infra/local/env/.env.example infra/local/env/.env" + @echo " # 编辑 configs/env/.env 与 infra/local/env/.env" + @echo "" + @echo "3. 确保以下变量已正确配置:" + @echo " - DATABASE_URL (连接到 localhost:54322)" + @echo " - REDIS_URL (连接到 localhost:6379)" + @echo " - MILVUS_URI (连接到 localhost:19530)" + @echo "" + @echo "4. 严禁将包含真实密钥的配置文件提交到 Git" + +# ============================================ +# 应用启动 +# ============================================ + +# 启动 FastAPI 后端服务 +api-dev: + @echo "🔧 启动 FastAPI 后端服务..." + @echo "" + @echo "前置条件:" + @echo " 1. 确保依赖服务已启动:make up" + @echo " 2. 确保环境变量已配置:make env" + @echo "" + @echo "启动命令(示例):" + @echo " cd apps/api" + @echo " uvicorn src.main:app --host 0.0.0.0 --port 8000 --reload" + @echo "" + @echo "或使用 Python 虚拟环境:" + @echo " python -m venv .venv" + @echo " source .venv/bin/activate" + @echo " pip install -r requirements.txt" + @echo " uvicorn src.main:app --host 0.0.0.0 --port 8000 --reload" + +# 启动 Flutter 移动应用 +flutter-dev: + @echo "📱 启动 Flutter 移动应用..." + @echo "" + @echo "前置条件:" + @echo " 1. 确保后端服务已启动:make api-dev" + @echo " 2. 确保 Flutter SDK 已安装" + @echo "" + @echo "启动命令(示例):" + @echo " cd apps/mobile" + @echo " flutter run --dart-define=PUBLIC_API_BASE_URL=http://localhost:8000" + @echo "" + @echo "或使用调试模式:" + @echo " flutter run --dart-define=PUBLIC_API_BASE_URL=http://localhost:8000 --debug" + @echo "" + @echo "构建版本:" + @echo " flutter build apk --dart-define=PUBLIC_API_BASE_URL=http://localhost:8000" + +# ============================================ +# 初始化脚本 +# ============================================ + +# 初始化 Milvus 向量数据库 +milvus-init: + @echo "🔧 初始化 Milvus 向量数据库..." + @echo "" + @echo "前置条件:" + @echo " 1. 确保依赖服务已启动:make up" + @echo " 2. 确保 Milvus 服务已健康(检查:make ps)" + @echo "" + @echo "初始化脚本:" + @echo " bash tools/scripts/init_milvus.sh" + @echo "" + @echo "如脚本不存在,请创建:" + @echo " mkdir -p tools/scripts" + @echo " cat > tools/scripts/init_milvus.sh << 'EOF'" + @echo "#!/bin/bash" + @echo "# Milvus 初始化脚本" + @echo "# TODO: 创建集合、索引等" + @echo "EOF" diff --git a/README.md b/README.md new file mode 100644 index 0000000..7b061c6 --- /dev/null +++ b/README.md @@ -0,0 +1,17 @@ +# Social App Monorepo + +Flutter + FastAPI + Supabase + Redis + Milvus + +## 说明 + +本仓库仅初始化结构,不包含业务实现 + +## 目录结构 + +- `apps/` —— 可运行应用(Flutter / FastAPI / Worker) +- `infra/` —— 基础设施(本地 docker / 云部署 / 迁移) +- `configs/` —— 配置规范与公共配置模板(不含密钥) +- `tools/` —— 脚本与生成器 +- `docs/` —— 文档与规则 + +详见 `docs/rules/repo-structure.md` diff --git a/apps/api/pyproject.toml b/apps/api/pyproject.toml new file mode 100644 index 0000000..29a8bc7 --- /dev/null +++ b/apps/api/pyproject.toml @@ -0,0 +1,2 @@ +# FastAPI 服务占位文件 +# 后续添加依赖和配置 diff --git a/apps/mobile/pubspec.yaml b/apps/mobile/pubspec.yaml new file mode 100644 index 0000000..d4f369f --- /dev/null +++ b/apps/mobile/pubspec.yaml @@ -0,0 +1,2 @@ +# Flutter 应用占位文件 +# 后续添加依赖和配置 diff --git a/apps/worker/pyproject.toml b/apps/worker/pyproject.toml new file mode 100644 index 0000000..7ed8598 --- /dev/null +++ b/apps/worker/pyproject.toml @@ -0,0 +1,2 @@ +# 异步任务/队列服务占位文件 +# 预留:后续可能添加 diff --git a/configs/env/.env.example b/configs/env/.env.example new file mode 100644 index 0000000..2baac69 --- /dev/null +++ b/configs/env/.env.example @@ -0,0 +1 @@ +# 放后端开发项目需要的环境变量 diff --git a/configs/flutter/dev.json b/configs/flutter/dev.json new file mode 100644 index 0000000..f081a4a --- /dev/null +++ b/configs/flutter/dev.json @@ -0,0 +1,4 @@ +{ + "apiBaseUrl": "http://localhost:8000", + "environment": "development" +} diff --git a/configs/flutter/prod.json b/configs/flutter/prod.json new file mode 100644 index 0000000..6cf19bc --- /dev/null +++ b/configs/flutter/prod.json @@ -0,0 +1,4 @@ +{ + "apiBaseUrl": "https://api.yourdomain.com", + "environment": "production" +} diff --git a/configs/openapi/openapi.yaml b/configs/openapi/openapi.yaml new file mode 100644 index 0000000..e966ffa --- /dev/null +++ b/configs/openapi/openapi.yaml @@ -0,0 +1,2 @@ +# OpenAPI 规范占位文件 +# 后续通过 FastAPI 自动生成 diff --git a/docs/adr/0001-tech-stack.md b/docs/adr/0001-tech-stack.md new file mode 100644 index 0000000..8cd55f7 --- /dev/null +++ b/docs/adr/0001-tech-stack.md @@ -0,0 +1,38 @@ +# 技术栈选择 + +## 背景 + +本项目需要构建一个跨平台社交应用,支持本地开发和云端部署。 + +## 决策 + +1. **前端框架:Flutter** + - 跨平台支持(iOS / Android / Web) + - 高性能原生渲染 + - 丰富的 UI 组件 + +2. **后端框架:FastAPI** + - 高性能异步框架 + - 自动生成 OpenAPI 文档 + - 类型安全 + +3. **数据库:Supabase(PostgreSQL)** + - 开箱即用的 PostgreSQL + - 内置认证和权限管理 + - 实时订阅功能 + +4. **缓存:Redis** + - 高性能键值存储 + - 支持多种数据结构 + +5. **向量数据库:Milvus** + - 高性能向量检索 + - 支持大规模向量存储 + - 适合 RAG 和推荐场景 + +## 后续考虑 + +根据业务发展,可能需要评估: +- CDN 方案 +- 消息队列 +- 监控和日志系统 diff --git a/docs/architecture/overview.md b/docs/architecture/overview.md new file mode 100644 index 0000000..7fb5f73 --- /dev/null +++ b/docs/architecture/overview.md @@ -0,0 +1,17 @@ +# 系统架构概述 + +## 技术栈 + +- **前端**:Flutter +- **后端**:FastAPI +- **数据库**:Supabase(PostgreSQL) +- **缓存**:Redis +- **向量数据库**:Milvus +- **部署**:Docker + 火山云(未来) + +## 架构特点 + +- Monorepo 结构 +- 微服务架构(API + Worker) +- 云原生设计 +- 支持本地 Docker 开发和云端部署 diff --git a/docs/rules/config-rules.md b/docs/rules/config-rules.md new file mode 100644 index 0000000..4a8f285 --- /dev/null +++ b/docs/rules/config-rules.md @@ -0,0 +1,203 @@ +# 配置规则与使用原则 + +## 配置文件组织 + +### 1. 环境变量规范(真相源) + +**文件位置**:`configs/env/.env.example` + +**作用**: +- 定义应用层环境变量的名称、类型、用途和敏感级别 +- 作为应用配置的"真相源"(source of truth) +- 不可直接用于运行,仅作为参考和规范 +- Docker/Supabase 栈配置不在本文件,使用 `infra/local/env/.env.example` +- 本文件仅包含应用层连接配置(如 `SUPABASE_URL`、`DATABASE_URL`),不包含 Supabase 栈运行变量 +- 不得在 `infra/local/env/.env.example` 中添加应用层变量(如 `PUBLIC_`、`API_`) + +**变量分类**: +- `public`:可公开的信息,如服务地址、端口号 +- `secret`:敏感信息,如密钥、密码、连接串 + +**变量块说明**: +- A. 通用环境(APP_ENV、LOG_LEVEL、TZ) +- B. Flutter 配置(仅 PUBLIC_ 变量) +- C. FastAPI 服务配置 +- D. Supabase / Postgres 连接配置 +- E. Redis 配置 +- F. Milvus 配置 +- G. 对象存储配置(可选) +- H. 其他配置 + +### 2. 本地开发配置 + +**文件位置**:`configs/env/.env` + +**作用**: +- 本地应用层的实际配置文件 +- 从 `configs/env/.env.example` 复制后填入真实值 +- **禁止提交到 Git 仓库** + +**创建方式**: +```bash +cp configs/env/.env.example configs/env/.env +# 编辑 configs/env/.env,填入实际值 +``` + +### 3. 本地 Supabase 栈配置 + +**文件位置**:`infra/local/env/.env` + +**作用**: +- 本地 Supabase + 周边依赖的实际配置文件 +- 从 `infra/local/env/.env.example` 复制后填入真实值 +- **禁止提交到 Git 仓库** + +**创建方式**: +```bash +cp infra/local/env/.env.example infra/local/env/.env +``` + +### 4. 云端部署配置 + +**方式一:环境文件** +- 文件位置:`infra/cloud/volcano/env/.env`(不提交) +- 从 `infra/cloud/volcano/env/.env.example` 复制后填入云端真实值 +- 仅用于本地测试云端连接 + +**方式二:Secret 注入(推荐)** +- 使用火山云或其他云平台的 Secret 管理服务 +- 通过 CI/CD 或容器运行时注入环境变量 +- 代码无需修改,通过环境切换 + +### 5. 应用特定配置 + +**Flutter 配置**: +- `configs/flutter/dev.json` —— 开发环境 +- `configs/flutter/prod.json` —— 生产环境 +- 通过 `--dart-define` 在构建时注入 + +## 安全原则 + +### Public vs Secret + +**Public 变量**: +- 可公开的服务地址和端口号 +- 前端可直接使用的配置 +- 示例:`PUBLIC_API_BASE_URL`、`API_PORT` + +**Secret 变量**: +- 密钥、密码、连接串 +- 仅服务端使用的敏感信息 +- 示例:`DATABASE_URL`、`REDIS_URL`、`JWT_SECRET` + +### Flutter 配置限制 + +**严格规则**: +- Flutter 只能使用以 `PUBLIC_` 开头的变量 +- 严禁将任何 secret 信息注入到 Flutter +- 通过 `--dart-define` 在构建时注入,运行时不可修改 + +**示例**: +```bash +# ✅ 正确:Flutter 使用 PUBLIC_ 变量 +flutter run --dart-define=PUBLIC_API_BASE_URL=http://localhost:8000 + +# ❌ 错误:Flutter 不能使用 secret +flutter run --dart-define=DATABASE_URL=postgresql://user:pass@localhost/db +``` + +### 后端配置读取 + +**严格规则**: +- `apps/api` 和 `apps/worker` 只能通过环境变量读取配置 +- 不得直接读取 `infra/local/*.env` 文件路径 +- 使用 Python 的 `os.getenv()` 或环境变量库 + +**示例**: +```python +# ✅ 正确:通过环境变量读取 +import os + +database_url = os.getenv("DATABASE_URL") + +# ❌ 错误:直接读取本地配置文件 +from dotenv import load_dotenv +load_dotenv("configs/env/.env") # 禁止 +``` + +## 使用方式 + +### 1. 本地开发 + +1. 复制环境变量模板: + ```bash + make env + ``` + +2. 启动依赖服务: + ```bash + make up + ``` + +3. 启动后端服务: + ```bash + make api-dev + ``` + +4. 启动 Flutter 应用: + ```bash + make flutter-dev + ``` + +### 2. 云端部署 + +1. 准备云端环境变量: + - 将 `.env.example` 中的变量映射到云平台 Secret +- 或创建 `infra/cloud/volcano/env/.env`(仅用于本地测试) + +2. 修改配置指向云端: + ```bash + # 示例:修改 .env 指向火山云托管地址 + REDIS_URL=redis://volcano-redis:6379/0 + MILVUS_URI=https://volcano-milvus:19530 + DATABASE_URL=postgresql://user:pass@volcano-postgres/db + ``` + +3. 部署应用: + - 通过 CI/CD 自动注入环境变量 + - 或通过云平台控制台配置 + +## 配置优先级 + +从高到低: +1. 运行时环境变量(最高) +2. 环境文件(`.env`) +3. 配置文件(`configs/flutter/*.json`) + +## 常见问题 + +### Q: 如何切换环境? + +**本地开发**:使用 `configs/env/.env` +**云端部署**:使用云平台 Secret 注入 + +### Q: 如何确保 secret 不泄露? + +- 所有 secret 变量使用 `CHANGE_ME` 占位符 +- 本地配置文件(`configs/env/.env`、`infra/local/env/.env`)添加到 `.gitignore` +- 云端使用密钥管理服务注入 + +### Q: 如何测试云端配置? + +1. 在本地创建 `infra/cloud/volcano/env/.env`(不提交) +2. 修改变量指向云端地址 +3. 导出环境变量测试: + ```bash + export $(cat infra/cloud/volcano/env/.env | xargs) + ``` + +### Q: Compose 如何读取配置? + +- 使用 `--env-file infra/local/env/.env` 注入本地 Supabase 栈变量 +- Compose 文件为 `infra/local/docker-compose.yml` +- 应用代码通过 `os.getenv()` 读取本地应用变量 diff --git a/docs/rules/repo-structure.md b/docs/rules/repo-structure.md new file mode 100644 index 0000000..1551fc2 --- /dev/null +++ b/docs/rules/repo-structure.md @@ -0,0 +1,21 @@ +# 目录结构规则 + +## 顶层目录(必须遵守) + +仓库根目录只能包含以下目录: + +- `apps/` —— 可运行应用(Flutter / FastAPI / Worker) +- `infra/` —— 基础设施(本地 docker / 云部署 / 迁移) +- `configs/` —— 配置规范与公共配置模板(不含密钥) +- `tools/` —— 脚本与生成器 +- `docs/` —— 文档与规则 +- `.github/`(可选,用于 CI/CD) +- `README.md` +- `Makefile`(可选) + +## 禁止事项 + +- 禁止在根目录直接出现:`backend/`、`ui/`、`docker/`、`scripts/` 等非规范目录 +- 所有业务代码必须放在 `apps/` 目录下 +- 所有配置文件必须放在 `configs/` 目录下 +- 所有基础设施相关代码必须放在 `infra/` 目录下 diff --git a/docs/runbooks/cloud-volcano.md b/docs/runbooks/cloud-volcano.md new file mode 100644 index 0000000..3e127e6 --- /dev/null +++ b/docs/runbooks/cloud-volcano.md @@ -0,0 +1,25 @@ +# 火山云部署指南 + +## 准备工作 + +1. 火山云账号 +2. 配置云端环境变量 +3. 准备镜像仓库 + +## 环境变量模板 + +云端模板文件位置: + +```bash +cp infra/cloud/volcano/env/.env.example infra/cloud/volcano/env/.env +``` + +## 部署流程 + +待补充详细步骤... + +## 注意事项 + +- 确保所有敏感信息使用环境变量或密钥管理 +- 遵循最小权限原则 +- 配置适当的监控和日志 diff --git a/docs/runbooks/local-dev.md b/docs/runbooks/local-dev.md new file mode 100644 index 0000000..4698821 --- /dev/null +++ b/docs/runbooks/local-dev.md @@ -0,0 +1,272 @@ +# 本地开发指南 + +## 前置要求 + +- Docker 和 Docker Compose +- Flutter SDK +- Python 3.11+ + +## 快速开始 + +### 1. 配置环境变量 + +```bash +make env +``` + +按照提示创建并编辑应用配置 `configs/env/.env`,并创建 Supabase 本地栈配置 `infra/local/env/.env`。 + +创建配置文件: + +```bash +cp configs/env/.env.example configs/env/.env +cp infra/local/env/.env.example infra/local/env/.env +``` + +确保以下变量配置正确: +- `DATABASE_URL`(连接到 localhost:54322) +- `REDIS_URL`(连接到 localhost:6379) +- `MILVUS_URI`(连接到 localhost:19530) + +### 2. 启动依赖服务 + +```bash +make up +``` + +这将启动以下服务: +- **Redis**:端口 6379 +- **Milvus**:端口 19530 (gRPC) / 19111 (HTTP) +- **Postgres**:端口 54322 + +### 3. 检查服务状态 + +```bash +make ps +``` + +确保所有服务显示为 `Up` 状态。 + +### 4. 查看服务日志 + +```bash +# 查看所有服务日志 +make logs + +# 查看特定服务日志 +make logs SERVICE=redis +make logs SERVICE=milvus +make logs SERVICE=db +``` + +## 启动应用 + +### 启动 FastAPI 后端 + +```bash +make api-dev +``` + +或手动启动: + +```bash +cd apps/api + +# 创建虚拟环境(首次) +python -m venv .venv +source .venv/bin/activate + +# 安装依赖(首次) +pip install -r requirements.txt + +# 启动服务 +uvicorn src.main:app --host 0.0.0.0 --port 8000 --reload +``` + +后端启动后,访问: +- API 文档:http://localhost:8000/docs +- ReDoc:http://localhost:8000/redoc + +### 启动 Flutter 应用 + +```bash +make flutter-dev +``` + +或手动启动: + +```bash +cd apps/mobile + +# 安装依赖(首次) +flutter pub get + +# 启动开发服务器 +flutter run --dart-define=PUBLIC_API_BASE_URL=http://localhost:8000 + +# 或指定设备 +flutter run -d chrome --dart-define=PUBLIC_API_BASE_URL=http://localhost:8000 +``` + +构建 Android APK: + +```bash +flutter build apk --dart-define=PUBLIC_API_BASE_URL=http://localhost:8000 +``` + +## 初始化 Milvus + +```bash +make milvus-init +``` + +或手动运行初始化脚本: + +```bash +bash tools/scripts/init_milvus.sh +``` + +## 常用命令 + +### 依赖服务管理 + +```bash +# 启动服务 +make up + +# 停止服务 +make down + +# 重启服务 +make down && make up + +# 查看状态 +make ps + +# 查看日志 +make logs + +# 清理数据(警告:会丢失数据) +make clean +``` + +### 应用管理 + +```bash +# 启动后端 +make api-dev + +# 启动前端 +make flutter-dev + +# 配置环境变量 +make env +``` + +## 常见问题 + +### 端口冲突 + +如果启动依赖服务时出现端口冲突: + +1. 检查端口占用: + ```bash + # 检查 6379(Redis) + lsof -i :6379 + + # 检查 54322(Postgres) + lsof -i :54322 + + # 检查 19530(Milvus) + lsof -i :19530 + ``` + +2. 停止占用端口的进程,或修改 `infra/local/docker-compose.yml` 中的端口映射 + +### 容器未健康 + +如果服务状态显示 `Up (health: starting)` 但一直未变成 `Up (healthy)`: + +1. 查看服务日志: + ```bash + make logs SERVICE= + ``` + +2. 检查依赖服务是否正常启动: + ```bash + make ps + ``` + +3. 重启服务: + ```bash + docker compose -f infra/local/docker-compose.yml --env-file infra/local/env/.env restart + ``` + +### 后端无法连接数据库 + +1. 检查 Postgres 是否正常启动: + ```bash + make ps + ``` + +2. 检查环境变量配置: + ```bash + cat configs/env/.env | grep DATABASE_URL + ``` + +3. 确保数据库 URL 格式正确: + ``` + postgresql://postgres:postgres@localhost:54322/postgres + ``` + +### Flutter 无法连接后端 + +1. 确保后端服务已启动并监听在 8000 端口: + ```bash + curl http://localhost:8000/docs + ``` + +2. 检查 Flutter 的 API_BASE_URL 是否正确注入: + ```bash + flutter run --dart-define=PUBLIC_API_BASE_URL=http://localhost:8000 + ``` + +3. 如果使用模拟器,确保能访问 localhost: + - Android 模拟器:使用 `10.0.2.2` 代替 `localhost` + - iOS 模拟器:使用 `localhost` 即可 + +### Milvus 连接失败 + +1. 检查 Milvus 服务是否健康: + ```bash + make ps + ``` + +2. 等待 Milvus 完全启动(可能需要 1-2 分钟): + ```bash + make logs SERVICE=milvus + ``` + +3. 测试连接: + ```bash + curl http://localhost:19530/healthz + ``` + +## 清理环境 + +```bash +# 停止所有服务 +make down + +# 清理数据卷(警告:会丢失所有数据) +make clean + +# 完全清理(包括未使用的镜像) +docker system prune -a +``` + +## 下一步 + +- 阅读架构文档:`docs/architecture/overview.md` +- 了解配置规则:`docs/rules/config-rules.md` +- 查看技术栈决策:`docs/adr/0001-tech-stack.md` diff --git a/infra/README.md b/infra/README.md new file mode 100644 index 0000000..443449f --- /dev/null +++ b/infra/README.md @@ -0,0 +1,89 @@ +# Infra + +Local Docker environment for Supabase stack and supporting services. + +## Quick Start + +1. Copy environment file: + ```bash + cp infra/env/.env.example infra/env/.env.local + ``` + +2. Update `infra/env/.env.local` with your secrets and configurations. + **Important**: Ensure all required fields are filled (no `CHANGE_ME` values). + +3. Start services: + ```bash + docker compose --env-file infra/env/.env.local -f infra/local/docker-compose.yml up -d + ``` + +## Access + +- **Supabase Studio**: http://localhost:8001 + Credentials from `DASHBOARD_USERNAME` and `DASHBOARD_PASSWORD` in `.env.local` +- **Qdrant**: http://localhost:6333 +- **Redis**: localhost:6379 + +## Troubleshooting + +### Port Conflicts + +If `localhost:8001` is unreachable, check for port conflicts: + +**Linux/WSL:** +```bash +ss -ltnp 'sport = :8001' +``` + +**Windows:** +```powershell +Get-NetTCPConnection -LocalPort 8001 | Select-Object LocalAddress, LocalPort, State, OwningProcess +Get-Process -Id (Get-NetTCPConnection -LocalPort 8001).OwningProcess +``` + +If a Windows service occupies the port, either: +- Stop the conflicting service +- Change `KONG_HTTP_PORT`, `API_EXTERNAL_URL`, and `SUPABASE_PUBLIC_URL` in `.env.local` + +### Environment Variables Not Applied + +If Docker reports warnings about missing variables, verify: +- The `--env-file` path is correct +- All required variables are set in `.env.local` (no empty values) + +### Service Health + +Check service status: +```bash +docker compose --env-file infra/env/.env.local -f infra/local/docker-compose.yml ps +``` + +View logs: +```bash +docker compose --env-file infra/env/.env.local -f infra/local/docker-compose.yml logs +``` + +## Services + +| Service | Description | +|------------|---------------------------------------| +| kong | API gateway (8001/8443) | +| studio | Supabase UI (via Kong) | +| auth | Authentication (GoTrue) | +| db | PostgreSQL database | +| rest | PostgREST API | +| realtime | Realtime subscriptions | +| storage | Storage API | +| functions | Edge functions | +| analytics | Logflare logging | +| vector | Log aggregator | +| supavisor | Database connection pooler | +| qdrant | Vector database | +| redis | Cache/message broker | + +## Important Notes + +- Never commit `.env.local` to version control +- Always use `--env-file` when running docker compose +- Port conflicts on Windows (especially 8000) can prevent Kong from starting +- Kong configuration is auto-generated from templates on container start diff --git a/infra/env/.env.example b/infra/env/.env.example new file mode 100644 index 0000000..2e35f6b --- /dev/null +++ b/infra/env/.env.example @@ -0,0 +1,90 @@ +# Local Docker (Supabase stack) environment example +# Copy to infra/env/.env.local and fill values +# Do not commit real secrets + +############ +# Secrets +############ +POSTGRES_PASSWORD=CHANGE_ME +JWT_SECRET=CHANGE_ME +ANON_KEY=CHANGE_ME +SERVICE_ROLE_KEY=CHANGE_ME +DASHBOARD_USERNAME=supabase +DASHBOARD_PASSWORD=CHANGE_ME +SECRET_KEY_BASE=CHANGE_ME +VAULT_ENC_KEY=CHANGE_ME +PG_META_CRYPTO_KEY=CHANGE_ME + +############ +# Database +############ +POSTGRES_HOST=db +POSTGRES_DB=postgres +POSTGRES_PORT=5432 + +############ +# Supavisor -- Database pooler +############ +POOLER_PROXY_PORT_TRANSACTION=6543 +POOLER_DEFAULT_POOL_SIZE=20 +POOLER_MAX_CLIENT_CONN=100 +POOLER_TENANT_ID=local-tenant +POOLER_DB_POOL_SIZE=5 + +############ +# API Proxy - Kong +############ +KONG_HTTP_PORT=8001 +KONG_HTTPS_PORT=8443 + +############ +# API - PostgREST +############ +PGRST_DB_SCHEMAS=public,storage,graphql_public + +############ +# Auth - GoTrue +############ +SITE_URL=http://localhost:3000 +ADDITIONAL_REDIRECT_URLS= +JWT_EXPIRY=3600 +DISABLE_SIGNUP=false +API_EXTERNAL_URL=http://localhost:8001 +MAILER_URLPATHS_CONFIRMATION="/auth/v1/verify" +MAILER_URLPATHS_INVITE="/auth/v1/verify" +MAILER_URLPATHS_RECOVERY="/auth/v1/verify" +MAILER_URLPATHS_EMAIL_CHANGE="/auth/v1/verify" +ENABLE_EMAIL_SIGNUP=true +ENABLE_EMAIL_AUTOCONFIRM=false +SMTP_ADMIN_EMAIL=admin@example.com +SMTP_HOST=supabase-mail +SMTP_PORT=2500 +SMTP_USER=fake_mail_user +SMTP_PASS=fake_mail_password +SMTP_SENDER_NAME=fake_sender +ENABLE_ANONYMOUS_USERS=false +ENABLE_PHONE_SIGNUP=true +ENABLE_PHONE_AUTOCONFIRM=true + +############ +# Studio +############ +STUDIO_DEFAULT_ORGANIZATION=Default Organization +STUDIO_DEFAULT_PROJECT=Default Project +SUPABASE_PUBLIC_URL=http://localhost:8000 +IMGPROXY_ENABLE_WEBP_DETECTION=true +OPENAI_API_KEY= + +############ +# Functions +############ +FUNCTIONS_VERIFY_JWT=false + +############ +# Logs / Analytics +############ +LOGFLARE_PUBLIC_ACCESS_TOKEN=CHANGE_ME +LOGFLARE_PRIVATE_ACCESS_TOKEN=CHANGE_ME +DOCKER_SOCKET_LOCATION=/var/run/docker.sock +GOOGLE_PROJECT_ID=GOOGLE_PROJECT_ID +GOOGLE_PROJECT_NUMBER=GOOGLE_PROJECT_NUMBER diff --git a/infra/local/docker-compose.yml b/infra/local/docker-compose.yml new file mode 100644 index 0000000..2dbc627 --- /dev/null +++ b/infra/local/docker-compose.yml @@ -0,0 +1,457 @@ +name: social-app-local + +services: + redis: + image: redis:7-alpine + container_name: social-local-redis + restart: unless-stopped + ports: + - "6379:6379" + volumes: + - redis_data:/data + command: redis-server --appendonly yes + healthcheck: + test: ["CMD", "redis-cli", "ping"] + interval: 5s + timeout: 3s + retries: 5 + + qdrant: + image: qdrant/qdrant:latest + container_name: social-local-qdrant + restart: unless-stopped + ports: + - "6333:6333" + - "6334:6334" + volumes: + - qdrant_data:/qdrant/storage + + studio: + container_name: supabase-studio + image: supabase/studio:2026.01.27-sha-6aa59ff + restart: unless-stopped + healthcheck: + test: + [ + "CMD", + "node", + "-e", + "fetch('http://studio:3000/api/platform/profile').then((r) => {if (r.status !== 200) throw new Error(r.status)})", + ] + timeout: 10s + interval: 5s + retries: 3 + depends_on: + analytics: + condition: service_healthy + environment: + HOSTNAME: "::" + STUDIO_PG_META_URL: http://meta:8080 + POSTGRES_PORT: ${POSTGRES_PORT} + POSTGRES_HOST: ${POSTGRES_HOST} + POSTGRES_DB: ${POSTGRES_DB} + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} + PG_META_CRYPTO_KEY: ${PG_META_CRYPTO_KEY} + DEFAULT_ORGANIZATION_NAME: ${STUDIO_DEFAULT_ORGANIZATION} + DEFAULT_PROJECT_NAME: ${STUDIO_DEFAULT_PROJECT} + OPENAI_API_KEY: ${OPENAI_API_KEY:-} + SUPABASE_URL: http://kong:8000 + SUPABASE_PUBLIC_URL: ${SUPABASE_PUBLIC_URL} + SUPABASE_ANON_KEY: ${ANON_KEY} + SUPABASE_SERVICE_KEY: ${SERVICE_ROLE_KEY} + AUTH_JWT_SECRET: ${JWT_SECRET} + LOGFLARE_API_KEY: ${LOGFLARE_PUBLIC_ACCESS_TOKEN} + LOGFLARE_PUBLIC_ACCESS_TOKEN: ${LOGFLARE_PUBLIC_ACCESS_TOKEN} + LOGFLARE_PRIVATE_ACCESS_TOKEN: ${LOGFLARE_PRIVATE_ACCESS_TOKEN} + LOGFLARE_URL: http://analytics:4000 + NEXT_PUBLIC_ENABLE_LOGS: true + NEXT_ANALYTICS_BACKEND_PROVIDER: postgres + SNIPPETS_MANAGEMENT_FOLDER: /app/snippets + volumes: + - ./supabase/volumes/snippets:/app/snippets:Z + + kong: + container_name: supabase-kong + image: kong:2.8.1 + restart: unless-stopped + ports: + - ${KONG_HTTP_PORT}:8000/tcp + - ${KONG_HTTPS_PORT}:8443/tcp + volumes: + - ./supabase/volumes/api/kong.yml:/home/kong/temp.yml:ro,z + depends_on: + analytics: + condition: service_healthy + environment: + 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,request-termination,ip-restriction + KONG_NGINX_PROXY_PROXY_BUFFER_SIZE: 160k + KONG_NGINX_PROXY_PROXY_BUFFERS: 64 160k + SUPABASE_ANON_KEY: ${ANON_KEY} + SUPABASE_SERVICE_KEY: ${SERVICE_ROLE_KEY} + DASHBOARD_USERNAME: ${DASHBOARD_USERNAME} + DASHBOARD_PASSWORD: ${DASHBOARD_PASSWORD} + entrypoint: bash -c 'eval "echo \"$$(cat ~/temp.yml)\"" > ~/kong.yml && /docker-entrypoint.sh kong docker-start' + + auth: + container_name: supabase-auth + image: supabase/gotrue:v2.185.0 + restart: unless-stopped + healthcheck: + test: + [ + "CMD", + "wget", + "--no-verbose", + "--tries=1", + "--spider", + "http://localhost:9999/health", + ] + timeout: 5s + interval: 5s + retries: 3 + depends_on: + db: + condition: service_healthy + analytics: + condition: service_healthy + environment: + GOTRUE_API_HOST: 0.0.0.0 + GOTRUE_API_PORT: 9999 + API_EXTERNAL_URL: ${API_EXTERNAL_URL} + GOTRUE_DB_DRIVER: postgres + GOTRUE_DB_DATABASE_URL: postgres://supabase_auth_admin:${POSTGRES_PASSWORD}@${POSTGRES_HOST}:${POSTGRES_PORT}/${POSTGRES_DB} + GOTRUE_SITE_URL: ${SITE_URL} + GOTRUE_URI_ALLOW_LIST: ${ADDITIONAL_REDIRECT_URLS} + GOTRUE_DISABLE_SIGNUP: ${DISABLE_SIGNUP} + GOTRUE_JWT_ADMIN_ROLES: service_role + GOTRUE_JWT_AUD: authenticated + GOTRUE_JWT_DEFAULT_GROUP_NAME: authenticated + GOTRUE_JWT_EXP: ${JWT_EXPIRY} + GOTRUE_JWT_SECRET: ${JWT_SECRET} + GOTRUE_EXTERNAL_EMAIL_ENABLED: ${ENABLE_EMAIL_SIGNUP} + GOTRUE_EXTERNAL_ANONYMOUS_USERS_ENABLED: ${ENABLE_ANONYMOUS_USERS} + GOTRUE_MAILER_AUTOCONFIRM: ${ENABLE_EMAIL_AUTOCONFIRM} + GOTRUE_SMTP_ADMIN_EMAIL: ${SMTP_ADMIN_EMAIL} + GOTRUE_SMTP_HOST: ${SMTP_HOST} + GOTRUE_SMTP_PORT: ${SMTP_PORT} + GOTRUE_SMTP_USER: ${SMTP_USER} + GOTRUE_SMTP_PASS: ${SMTP_PASS} + GOTRUE_SMTP_SENDER_NAME: ${SMTP_SENDER_NAME} + GOTRUE_MAILER_URLPATHS_INVITE: ${MAILER_URLPATHS_INVITE} + GOTRUE_MAILER_URLPATHS_CONFIRMATION: ${MAILER_URLPATHS_CONFIRMATION} + GOTRUE_MAILER_URLPATHS_RECOVERY: ${MAILER_URLPATHS_RECOVERY} + GOTRUE_MAILER_URLPATHS_EMAIL_CHANGE: ${MAILER_URLPATHS_EMAIL_CHANGE} + GOTRUE_EXTERNAL_PHONE_ENABLED: ${ENABLE_PHONE_SIGNUP} + GOTRUE_SMS_AUTOCONFIRM: ${ENABLE_PHONE_AUTOCONFIRM} + + rest: + container_name: supabase-rest + image: postgrest/postgrest:v14.3 + restart: unless-stopped + depends_on: + db: + condition: service_healthy + analytics: + condition: service_healthy + environment: + PGRST_DB_URI: postgres://authenticator:${POSTGRES_PASSWORD}@${POSTGRES_HOST}:${POSTGRES_PORT}/${POSTGRES_DB} + PGRST_DB_SCHEMAS: ${PGRST_DB_SCHEMAS} + PGRST_DB_ANON_ROLE: anon + PGRST_JWT_SECRET: ${JWT_SECRET} + PGRST_DB_USE_LEGACY_GUCS: "false" + PGRST_APP_SETTINGS_JWT_SECRET: ${JWT_SECRET} + PGRST_APP_SETTINGS_JWT_EXP: ${JWT_EXPIRY} + command: ["postgrest"] + + realtime: + container_name: realtime-dev.supabase-realtime + image: supabase/realtime:v2.72.0 + restart: unless-stopped + depends_on: + db: + condition: service_healthy + analytics: + condition: service_healthy + healthcheck: + test: + [ + "CMD-SHELL", + 'curl -sSfL --head -o /dev/null -H "Authorization: Bearer ${ANON_KEY}" http://localhost:4000/api/tenants/realtime-dev/health', + ] + timeout: 5s + interval: 30s + retries: 3 + start_period: 10s + environment: + PORT: 4000 + DB_HOST: ${POSTGRES_HOST} + DB_PORT: ${POSTGRES_PORT} + DB_USER: supabase_admin + DB_PASSWORD: ${POSTGRES_PASSWORD} + DB_NAME: ${POSTGRES_DB} + DB_AFTER_CONNECT_QUERY: "SET search_path TO _realtime" + DB_ENC_KEY: supabaserealtime + API_JWT_SECRET: ${JWT_SECRET} + ANON_KEY: ${ANON_KEY} + SECRET_KEY_BASE: ${SECRET_KEY_BASE} + ERL_AFLAGS: -proto_dist inet_tcp + DNS_NODES: "''" + RLIMIT_NOFILE: "10000" + APP_NAME: realtime + SEED_SELF_HOST: "true" + RUN_JANITOR: "true" + DISABLE_HEALTHCHECK_LOGGING: "true" + + storage: + container_name: supabase-storage + image: supabase/storage-api:v1.33.5 + restart: unless-stopped + volumes: + - ./supabase/volumes/storage:/var/lib/storage:z + healthcheck: + test: + [ + "CMD", + "wget", + "--no-verbose", + "--tries=1", + "--spider", + "http://storage:5000/status", + ] + timeout: 5s + interval: 5s + retries: 3 + depends_on: + db: + condition: service_healthy + rest: + condition: service_started + imgproxy: + condition: service_started + environment: + ANON_KEY: ${ANON_KEY} + SERVICE_KEY: ${SERVICE_ROLE_KEY} + POSTGREST_URL: http://rest:3000 + PGRST_JWT_SECRET: ${JWT_SECRET} + DATABASE_URL: postgres://supabase_storage_admin:${POSTGRES_PASSWORD}@${POSTGRES_HOST}:${POSTGRES_PORT}/${POSTGRES_DB} + REQUEST_ALLOW_X_FORWARDED_PATH: "true" + FILE_SIZE_LIMIT: 52428800 + STORAGE_BACKEND: file + FILE_STORAGE_BACKEND_PATH: /var/lib/storage + TENANT_ID: stub + REGION: stub + GLOBAL_S3_BUCKET: stub + ENABLE_IMAGE_TRANSFORMATION: "true" + IMGPROXY_URL: http://imgproxy:5001 + + imgproxy: + container_name: supabase-imgproxy + image: darthsim/imgproxy:v3.30.1 + restart: unless-stopped + volumes: + - ./supabase/volumes/storage:/var/lib/storage:z + healthcheck: + test: ["CMD", "imgproxy", "health"] + timeout: 5s + interval: 5s + retries: 3 + environment: + IMGPROXY_BIND: ":5001" + IMGPROXY_LOCAL_FILESYSTEM_ROOT: / + IMGPROXY_USE_ETAG: "true" + IMGPROXY_ENABLE_WEBP_DETECTION: ${IMGPROXY_ENABLE_WEBP_DETECTION} + IMGPROXY_MAX_SRC_RESOLUTION: 16.8 + + meta: + container_name: supabase-meta + image: supabase/postgres-meta:v0.95.2 + restart: unless-stopped + depends_on: + db: + condition: service_healthy + analytics: + condition: service_healthy + environment: + PG_META_PORT: 8080 + PG_META_DB_HOST: ${POSTGRES_HOST} + PG_META_DB_PORT: ${POSTGRES_PORT} + PG_META_DB_NAME: ${POSTGRES_DB} + PG_META_DB_USER: supabase_admin + PG_META_DB_PASSWORD: ${POSTGRES_PASSWORD} + CRYPTO_KEY: ${PG_META_CRYPTO_KEY} + + functions: + container_name: supabase-edge-functions + image: supabase/edge-runtime:v1.70.0 + restart: unless-stopped + volumes: + - ./supabase/volumes/functions:/home/deno/functions:Z + depends_on: + analytics: + condition: service_healthy + environment: + JWT_SECRET: ${JWT_SECRET} + SUPABASE_URL: http://kong:8000 + SUPABASE_ANON_KEY: ${ANON_KEY} + SUPABASE_SERVICE_ROLE_KEY: ${SERVICE_ROLE_KEY} + SUPABASE_DB_URL: postgresql://postgres:${POSTGRES_PASSWORD}@${POSTGRES_HOST}:${POSTGRES_PORT}/${POSTGRES_DB} + VERIFY_JWT: "${FUNCTIONS_VERIFY_JWT}" + command: ["start", "--main-service", "/home/deno/functions/main"] + + analytics: + container_name: supabase-analytics + image: supabase/logflare:1.30.3 + restart: unless-stopped + ports: + - 4000:4000 + healthcheck: + test: ["CMD", "curl", "http://localhost:4000/health"] + timeout: 5s + interval: 5s + retries: 10 + depends_on: + db: + condition: service_healthy + environment: + LOGFLARE_NODE_HOST: 127.0.0.1 + DB_USERNAME: supabase_admin + DB_DATABASE: _supabase + DB_HOSTNAME: ${POSTGRES_HOST} + DB_PORT: ${POSTGRES_PORT} + DB_PASSWORD: ${POSTGRES_PASSWORD} + DB_SCHEMA: _analytics + LOGFLARE_PUBLIC_ACCESS_TOKEN: ${LOGFLARE_PUBLIC_ACCESS_TOKEN} + LOGFLARE_PRIVATE_ACCESS_TOKEN: ${LOGFLARE_PRIVATE_ACCESS_TOKEN} + LOGFLARE_SINGLE_TENANT: true + LOGFLARE_SUPABASE_MODE: true + POSTGRES_BACKEND_URL: postgresql://supabase_admin:${POSTGRES_PASSWORD}@${POSTGRES_HOST}:${POSTGRES_PORT}/_supabase + POSTGRES_BACKEND_SCHEMA: _analytics + LOGFLARE_FEATURE_FLAG_OVERRIDE: multibackend=true + + db: + container_name: supabase-db + image: supabase/postgres:15.8.1.085 + restart: unless-stopped + volumes: + - ./supabase/volumes/db/realtime.sql:/docker-entrypoint-initdb.d/migrations/99-realtime.sql:Z + - ./supabase/volumes/db/webhooks.sql:/docker-entrypoint-initdb.d/init-scripts/98-webhooks.sql:Z + - ./supabase/volumes/db/roles.sql:/docker-entrypoint-initdb.d/init-scripts/99-roles.sql:Z + - ./supabase/volumes/db/jwt.sql:/docker-entrypoint-initdb.d/init-scripts/99-jwt.sql:Z + - ./supabase/volumes/db/data:/var/lib/postgresql/data:Z + - ./supabase/volumes/db/_supabase.sql:/docker-entrypoint-initdb.d/migrations/97-_supabase.sql:Z + - ./supabase/volumes/db/logs.sql:/docker-entrypoint-initdb.d/migrations/99-logs.sql:Z + - ./supabase/volumes/db/pooler.sql:/docker-entrypoint-initdb.d/migrations/99-pooler.sql:Z + - db-config:/etc/postgresql-custom + healthcheck: + test: ["CMD", "pg_isready", "-U", "postgres", "-h", "localhost"] + interval: 5s + timeout: 5s + retries: 10 + depends_on: + vector: + condition: service_healthy + environment: + POSTGRES_HOST: /var/run/postgresql + PGPORT: ${POSTGRES_PORT} + POSTGRES_PORT: ${POSTGRES_PORT} + PGPASSWORD: ${POSTGRES_PASSWORD} + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} + PGDATABASE: ${POSTGRES_DB} + POSTGRES_DB: ${POSTGRES_DB} + JWT_SECRET: ${JWT_SECRET} + JWT_EXP: ${JWT_EXPIRY} + command: + [ + "postgres", + "-c", + "config_file=/etc/postgresql/postgresql.conf", + "-c", + "log_min_messages=fatal", + ] + + vector: + container_name: supabase-vector + image: timberio/vector:0.28.1-alpine + restart: unless-stopped + volumes: + - ./supabase/volumes/logs/vector.yml:/etc/vector/vector.yml:ro,z + - ${DOCKER_SOCKET_LOCATION}:/var/run/docker.sock:ro,z + healthcheck: + test: + [ + "CMD", + "wget", + "--no-verbose", + "--tries=1", + "--spider", + "http://vector:9001/health", + ] + timeout: 5s + interval: 5s + retries: 3 + environment: + LOGFLARE_PUBLIC_ACCESS_TOKEN: ${LOGFLARE_PUBLIC_ACCESS_TOKEN} + command: ["--config", "/etc/vector/vector.yml"] + security_opt: + - "label=disable" + + supavisor: + container_name: supabase-pooler + image: supabase/supavisor:2.7.4 + restart: unless-stopped + ports: + - ${POSTGRES_PORT}:5432 + - ${POOLER_PROXY_PORT_TRANSACTION}:6543 + volumes: + - ./supabase/volumes/pooler/pooler.exs:/etc/pooler/pooler.exs:ro,z + healthcheck: + test: + [ + "CMD", + "curl", + "-sSfL", + "--head", + "-o", + "/dev/null", + "http://127.0.0.1:4000/api/health", + ] + interval: 10s + timeout: 5s + retries: 5 + depends_on: + db: + condition: service_healthy + analytics: + condition: service_healthy + environment: + PORT: 4000 + POSTGRES_PORT: ${POSTGRES_PORT} + POSTGRES_DB: ${POSTGRES_DB} + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} + DATABASE_URL: ecto://supabase_admin:${POSTGRES_PASSWORD}@${POSTGRES_HOST}:${POSTGRES_PORT}/_supabase + CLUSTER_POSTGRES: true + SECRET_KEY_BASE: ${SECRET_KEY_BASE} + VAULT_ENC_KEY: ${VAULT_ENC_KEY} + API_JWT_SECRET: ${JWT_SECRET} + METRICS_JWT_SECRET: ${JWT_SECRET} + REGION: local + ERL_AFLAGS: -proto_dist inet_tcp + POOLER_TENANT_ID: ${POOLER_TENANT_ID} + POOLER_DEFAULT_POOL_SIZE: ${POOLER_DEFAULT_POOL_SIZE} + POOLER_MAX_CLIENT_CONN: ${POOLER_MAX_CLIENT_CONN} + POOLER_POOL_MODE: transaction + DB_POOL_SIZE: ${POOLER_DB_POOL_SIZE} + command: + [ + "/bin/sh", + "-c", + '/app/bin/migrate && /app/bin/supavisor eval "$$(cat /etc/pooler/pooler.exs)" && /app/bin/server', + ] + +volumes: + redis_data: + qdrant_data: + db-config: diff --git a/infra/local/supabase/volumes/api/kong.yml b/infra/local/supabase/volumes/api/kong.yml new file mode 100644 index 0000000..2deb345 --- /dev/null +++ b/infra/local/supabase/volumes/api/kong.yml @@ -0,0 +1,284 @@ +_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://auth:9999/verify + routes: + - name: auth-v1-open + strip_path: true + paths: + - /auth/v1/verify + plugins: + - name: cors + - name: auth-v1-open-callback + url: http://auth:9999/callback + routes: + - name: auth-v1-open-callback + strip_path: true + paths: + - /auth/v1/callback + plugins: + - name: cors + - name: auth-v1-open-authorize + url: http://auth:9999/authorize + routes: + - name: auth-v1-open-authorize + strip_path: true + paths: + - /auth/v1/authorize + plugins: + - name: cors + + ## Secure Auth routes + - name: auth-v1 + _comment: 'GoTrue: /auth/v1/* -> http://auth:9999/*' + url: http://auth:9999/ + routes: + - name: auth-v1-all + strip_path: true + paths: + - /auth/v1/ + plugins: + - name: cors + - name: key-auth + config: + hide_credentials: false + - name: acl + config: + hide_groups_header: true + allow: + - admin + - anon + + ## Secure REST routes + - name: rest-v1 + _comment: 'PostgREST: /rest/v1/* -> http://rest:3000/*' + url: http://rest:3000/ + routes: + - name: rest-v1-all + strip_path: true + paths: + - /rest/v1/ + plugins: + - name: cors + - name: key-auth + config: + hide_credentials: true + - name: acl + config: + hide_groups_header: true + allow: + - admin + - anon + + ## Secure GraphQL routes + - name: graphql-v1 + _comment: 'PostgREST: /graphql/v1/* -> http://rest:3000/rpc/graphql' + url: http://rest:3000/rpc/graphql + routes: + - name: graphql-v1-all + strip_path: true + paths: + - /graphql/v1 + plugins: + - name: cors + - name: key-auth + config: + hide_credentials: true + - name: request-transformer + config: + add: + headers: + - Content-Profile:graphql_public + - name: acl + config: + hide_groups_header: true + allow: + - admin + - anon + + ## Secure Realtime routes + - name: realtime-v1-ws + _comment: 'Realtime: /realtime/v1/* -> ws://realtime:4000/socket/*' + url: http://realtime-dev.supabase-realtime:4000/socket + protocol: ws + routes: + - name: realtime-v1-ws + strip_path: true + paths: + - /realtime/v1/ + plugins: + - name: cors + - name: key-auth + config: + hide_credentials: false + - name: acl + config: + hide_groups_header: true + allow: + - admin + - anon + - name: realtime-v1-rest + _comment: 'Realtime: /realtime/v1/* -> ws://realtime:4000/socket/*' + url: http://realtime-dev.supabase-realtime:4000/api + protocol: http + routes: + - name: realtime-v1-rest + strip_path: true + paths: + - /realtime/v1/api + plugins: + - name: cors + - name: key-auth + config: + hide_credentials: false + - name: 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://storage:5000/*' + url: http://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://analytics:4000/ + routes: + - name: analytics-v1-all + strip_path: true + paths: + - /analytics/v1/ + + ## Secure Database routes + - name: meta + _comment: 'pg-meta: /pg/* -> http://pg-meta:8080/*' + url: http://meta:8080/ + routes: + - name: meta-all + strip_path: true + paths: + - /pg/ + plugins: + - name: key-auth + config: + hide_credentials: false + - name: acl + config: + hide_groups_header: true + allow: + - admin + + ## Block access to /api/mcp + - name: mcp-blocker + _comment: 'Block direct access to /api/mcp' + url: http://studio:3000/api/mcp + routes: + - name: mcp-blocker-route + strip_path: true + paths: + - /api/mcp + plugins: + - name: request-termination + config: + status_code: 403 + message: "Access is forbidden." + + ## MCP endpoint - local access + - name: mcp + _comment: 'MCP: /mcp -> http://studio:3000/api/mcp (local access)' + url: http://studio:3000/api/mcp + routes: + - name: mcp + strip_path: true + paths: + - /mcp + plugins: + # Block access to /mcp by default + - name: request-termination + config: + status_code: 403 + message: "Access is forbidden." + # Enable local access (danger zone!) + # 1. Comment out the 'request-termination' section above + # 2. Uncomment the entire section below, including 'deny' + # 3. Add your local IPs to the 'allow' list + #- name: cors + #- name: ip-restriction + # config: + # allow: + # - 127.0.0.1 + # - ::1 + # deny: [] + + ## Protected Dashboard - catch all remaining routes + - name: dashboard + _comment: 'Studio: /* -> http://studio:3000/*' + url: http://studio:3000/ + routes: + - name: dashboard-all + strip_path: true + paths: + - / + plugins: + - name: cors + - name: basic-auth + config: + hide_credentials: true diff --git a/infra/local/supabase/volumes/db/_supabase.sql b/infra/local/supabase/volumes/db/_supabase.sql new file mode 100644 index 0000000..6236ae1 --- /dev/null +++ b/infra/local/supabase/volumes/db/_supabase.sql @@ -0,0 +1,3 @@ +\set pguser `echo "$POSTGRES_USER"` + +CREATE DATABASE _supabase WITH OWNER :pguser; diff --git a/infra/local/supabase/volumes/db/jwt.sql b/infra/local/supabase/volumes/db/jwt.sql new file mode 100644 index 0000000..cfd3b16 --- /dev/null +++ b/infra/local/supabase/volumes/db/jwt.sql @@ -0,0 +1,5 @@ +\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'; diff --git a/infra/local/supabase/volumes/db/logs.sql b/infra/local/supabase/volumes/db/logs.sql new file mode 100644 index 0000000..255c0f4 --- /dev/null +++ b/infra/local/supabase/volumes/db/logs.sql @@ -0,0 +1,6 @@ +\set pguser `echo "$POSTGRES_USER"` + +\c _supabase +create schema if not exists _analytics; +alter schema _analytics owner to :pguser; +\c postgres diff --git a/infra/local/supabase/volumes/db/pooler.sql b/infra/local/supabase/volumes/db/pooler.sql new file mode 100644 index 0000000..162c5b9 --- /dev/null +++ b/infra/local/supabase/volumes/db/pooler.sql @@ -0,0 +1,6 @@ +\set pguser `echo "$POSTGRES_USER"` + +\c _supabase +create schema if not exists _supavisor; +alter schema _supavisor owner to :pguser; +\c postgres diff --git a/infra/local/supabase/volumes/db/realtime.sql b/infra/local/supabase/volumes/db/realtime.sql new file mode 100644 index 0000000..4d4b9ff --- /dev/null +++ b/infra/local/supabase/volumes/db/realtime.sql @@ -0,0 +1,4 @@ +\set pguser `echo "$POSTGRES_USER"` + +create schema if not exists _realtime; +alter schema _realtime owner to :pguser; diff --git a/infra/local/supabase/volumes/db/roles.sql b/infra/local/supabase/volumes/db/roles.sql new file mode 100644 index 0000000..8f7161a --- /dev/null +++ b/infra/local/supabase/volumes/db/roles.sql @@ -0,0 +1,8 @@ +-- 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'; diff --git a/infra/local/supabase/volumes/db/webhooks.sql b/infra/local/supabase/volumes/db/webhooks.sql new file mode 100644 index 0000000..5837b86 --- /dev/null +++ b/infra/local/supabase/volumes/db/webhooks.sql @@ -0,0 +1,208 @@ +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; diff --git a/infra/local/supabase/volumes/functions/main/index.ts b/infra/local/supabase/volumes/functions/main/index.ts new file mode 100644 index 0000000..3eaaf72 --- /dev/null +++ b/infra/local/supabase/volumes/functions/main/index.ts @@ -0,0 +1,89 @@ +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' + +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 { + const encoder = new TextEncoder() + const secretKey = encoder.encode(JWT_SECRET) + try { + await jose.jwtVerify(jwt, secretKey) + } catch (_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) { + 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}` + + 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' }, + }) + } +}) diff --git a/infra/local/supabase/volumes/logs/vector.yml b/infra/local/supabase/volumes/logs/vector.yml new file mode 100644 index 0000000..1dee9e0 --- /dev/null +++ b/infra/local/supabase/volumes/logs/vector.yml @@ -0,0 +1,247 @@ +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: + kong: '.appname == "supabase-kong"' + auth: '.appname == "supabase-auth"' + rest: '.appname == "supabase-rest"' + realtime: '.appname == "realtime-dev.supabase-realtime"' + storage: '.appname == "supabase-storage"' + functions: '.appname == "supabase-edge-functions"' + db: '.appname == "supabase-db"' + 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 + } + 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 + } + 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) + } + rest_logs: + type: remap + inputs: + - router.rest + source: |- + parsed, err = parse_regex(.event_message, r'^(?P