chore: initial commit - Supabase stack with local Docker configuration
This commit is contained in:
+12
@@ -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
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://opencode.ai/config.json",
|
||||||
|
"mcp": {
|
||||||
|
"chrome-devtools": {
|
||||||
|
"enabled": false
|
||||||
|
},
|
||||||
|
"zai-mcp-server": {
|
||||||
|
"enabled": false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
```
|
||||||
@@ -0,0 +1,180 @@
|
|||||||
|
# ============================================
|
||||||
|
# Social App Monorepo - 统一命令入口
|
||||||
|
# ============================================
|
||||||
|
#
|
||||||
|
# 使用方式:
|
||||||
|
# make <command>
|
||||||
|
#
|
||||||
|
# 常用命令:
|
||||||
|
# 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"
|
||||||
@@ -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`
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
# FastAPI 服务占位文件
|
||||||
|
# 后续添加依赖和配置
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
# Flutter 应用占位文件
|
||||||
|
# 后续添加依赖和配置
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
# 异步任务/队列服务占位文件
|
||||||
|
# 预留:后续可能添加
|
||||||
Vendored
+1
@@ -0,0 +1 @@
|
|||||||
|
# 放后端开发项目需要的环境变量
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
"apiBaseUrl": "http://localhost:8000",
|
||||||
|
"environment": "development"
|
||||||
|
}
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
"apiBaseUrl": "https://api.yourdomain.com",
|
||||||
|
"environment": "production"
|
||||||
|
}
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
# OpenAPI 规范占位文件
|
||||||
|
# 后续通过 FastAPI 自动生成
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
# 技术栈选择
|
||||||
|
|
||||||
|
## 背景
|
||||||
|
|
||||||
|
本项目需要构建一个跨平台社交应用,支持本地开发和云端部署。
|
||||||
|
|
||||||
|
## 决策
|
||||||
|
|
||||||
|
1. **前端框架:Flutter**
|
||||||
|
- 跨平台支持(iOS / Android / Web)
|
||||||
|
- 高性能原生渲染
|
||||||
|
- 丰富的 UI 组件
|
||||||
|
|
||||||
|
2. **后端框架:FastAPI**
|
||||||
|
- 高性能异步框架
|
||||||
|
- 自动生成 OpenAPI 文档
|
||||||
|
- 类型安全
|
||||||
|
|
||||||
|
3. **数据库:Supabase(PostgreSQL)**
|
||||||
|
- 开箱即用的 PostgreSQL
|
||||||
|
- 内置认证和权限管理
|
||||||
|
- 实时订阅功能
|
||||||
|
|
||||||
|
4. **缓存:Redis**
|
||||||
|
- 高性能键值存储
|
||||||
|
- 支持多种数据结构
|
||||||
|
|
||||||
|
5. **向量数据库:Milvus**
|
||||||
|
- 高性能向量检索
|
||||||
|
- 支持大规模向量存储
|
||||||
|
- 适合 RAG 和推荐场景
|
||||||
|
|
||||||
|
## 后续考虑
|
||||||
|
|
||||||
|
根据业务发展,可能需要评估:
|
||||||
|
- CDN 方案
|
||||||
|
- 消息队列
|
||||||
|
- 监控和日志系统
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
# 系统架构概述
|
||||||
|
|
||||||
|
## 技术栈
|
||||||
|
|
||||||
|
- **前端**:Flutter
|
||||||
|
- **后端**:FastAPI
|
||||||
|
- **数据库**:Supabase(PostgreSQL)
|
||||||
|
- **缓存**:Redis
|
||||||
|
- **向量数据库**:Milvus
|
||||||
|
- **部署**:Docker + 火山云(未来)
|
||||||
|
|
||||||
|
## 架构特点
|
||||||
|
|
||||||
|
- Monorepo 结构
|
||||||
|
- 微服务架构(API + Worker)
|
||||||
|
- 云原生设计
|
||||||
|
- 支持本地 Docker 开发和云端部署
|
||||||
@@ -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()` 读取本地应用变量
|
||||||
@@ -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/` 目录下
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
# 火山云部署指南
|
||||||
|
|
||||||
|
## 准备工作
|
||||||
|
|
||||||
|
1. 火山云账号
|
||||||
|
2. 配置云端环境变量
|
||||||
|
3. 准备镜像仓库
|
||||||
|
|
||||||
|
## 环境变量模板
|
||||||
|
|
||||||
|
云端模板文件位置:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cp infra/cloud/volcano/env/.env.example infra/cloud/volcano/env/.env
|
||||||
|
```
|
||||||
|
|
||||||
|
## 部署流程
|
||||||
|
|
||||||
|
待补充详细步骤...
|
||||||
|
|
||||||
|
## 注意事项
|
||||||
|
|
||||||
|
- 确保所有敏感信息使用环境变量或密钥管理
|
||||||
|
- 遵循最小权限原则
|
||||||
|
- 配置适当的监控和日志
|
||||||
@@ -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=<service_name>
|
||||||
|
```
|
||||||
|
|
||||||
|
2. 检查依赖服务是否正常启动:
|
||||||
|
```bash
|
||||||
|
make ps
|
||||||
|
```
|
||||||
|
|
||||||
|
3. 重启服务:
|
||||||
|
```bash
|
||||||
|
docker compose -f infra/local/docker-compose.yml --env-file infra/local/env/.env restart <service_name>
|
||||||
|
```
|
||||||
|
|
||||||
|
### 后端无法连接数据库
|
||||||
|
|
||||||
|
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`
|
||||||
@@ -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 <service-name>
|
||||||
|
```
|
||||||
|
|
||||||
|
## 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
|
||||||
Vendored
+90
@@ -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
|
||||||
@@ -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:
|
||||||
@@ -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
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
\set pguser `echo "$POSTGRES_USER"`
|
||||||
|
|
||||||
|
CREATE DATABASE _supabase WITH OWNER :pguser;
|
||||||
@@ -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';
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
\set pguser `echo "$POSTGRES_USER"`
|
||||||
|
|
||||||
|
create schema if not exists _realtime;
|
||||||
|
alter schema _realtime owner to :pguser;
|
||||||
@@ -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';
|
||||||
@@ -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;
|
||||||
@@ -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<boolean> {
|
||||||
|
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' },
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
@@ -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<time>.*): (?P<msg>.*)$')
|
||||||
|
if err == null {
|
||||||
|
.event_message = parsed.msg
|
||||||
|
.timestamp = to_timestamp!(parsed.time)
|
||||||
|
.metadata.host = .project
|
||||||
|
}
|
||||||
|
realtime_logs_filtered:
|
||||||
|
type: filter
|
||||||
|
inputs:
|
||||||
|
- router.realtime
|
||||||
|
condition: '!contains(string!(.event_message), "/health")'
|
||||||
|
realtime_logs:
|
||||||
|
type: remap
|
||||||
|
inputs:
|
||||||
|
- realtime_logs_filtered
|
||||||
|
source: |-
|
||||||
|
.metadata.project = del(.project)
|
||||||
|
.metadata.external_id = .metadata.project
|
||||||
|
parsed, err = parse_regex(.event_message, r'^(?P<time>\d+:\d+:\d+\.\d+) \[(?P<level>\w+)\] (?P<msg>.*)$')
|
||||||
|
if err == null {
|
||||||
|
.event_message = parsed.msg
|
||||||
|
.metadata.level = parsed.level
|
||||||
|
}
|
||||||
|
functions_logs:
|
||||||
|
type: remap
|
||||||
|
inputs:
|
||||||
|
- router.functions
|
||||||
|
source: |-
|
||||||
|
.metadata.project_ref = del(.project)
|
||||||
|
storage_logs:
|
||||||
|
type: remap
|
||||||
|
inputs:
|
||||||
|
- router.storage
|
||||||
|
source: |-
|
||||||
|
.metadata.project = del(.project)
|
||||||
|
.metadata.tenantId = .metadata.project
|
||||||
|
parsed, err = parse_json(.event_message)
|
||||||
|
if err == null {
|
||||||
|
.event_message = parsed.msg
|
||||||
|
.metadata.level = parsed.level
|
||||||
|
.metadata.timestamp = parsed.time
|
||||||
|
.metadata.context[0].host = parsed.hostname
|
||||||
|
.metadata.context[0].pid = parsed.pid
|
||||||
|
}
|
||||||
|
db_logs:
|
||||||
|
type: remap
|
||||||
|
inputs:
|
||||||
|
- router.db
|
||||||
|
source: |-
|
||||||
|
.metadata.host = "db-default"
|
||||||
|
.metadata.parsed.timestamp = .timestamp
|
||||||
|
|
||||||
|
parsed, err = parse_regex(.event_message, r'.*(?P<level>INFO|NOTICE|WARNING|ERROR|LOG|FATAL|PANIC?):.*', numeric_groups: true)
|
||||||
|
|
||||||
|
if err != null || parsed == null {
|
||||||
|
.metadata.parsed.error_severity = "info"
|
||||||
|
}
|
||||||
|
if parsed.level != null {
|
||||||
|
.metadata.parsed.error_severity = parsed.level
|
||||||
|
}
|
||||||
|
if .metadata.parsed.error_severity == "info" {
|
||||||
|
.metadata.parsed.error_severity = "log"
|
||||||
|
}
|
||||||
|
.metadata.parsed.error_severity = upcase!(.metadata.parsed.error_severity)
|
||||||
|
|
||||||
|
sinks:
|
||||||
|
logflare_auth:
|
||||||
|
type: 'http'
|
||||||
|
inputs:
|
||||||
|
- auth_logs
|
||||||
|
encoding:
|
||||||
|
codec: 'json'
|
||||||
|
method: 'post'
|
||||||
|
request:
|
||||||
|
retry_max_duration_secs: 10
|
||||||
|
headers:
|
||||||
|
x-api-key: ${LOGFLARE_PUBLIC_ACCESS_TOKEN?LOGFLARE_PUBLIC_ACCESS_TOKEN is required}
|
||||||
|
uri: 'http://analytics:4000/api/logs?source_name=gotrue.logs.prod'
|
||||||
|
logflare_realtime:
|
||||||
|
type: 'http'
|
||||||
|
inputs:
|
||||||
|
- realtime_logs
|
||||||
|
encoding:
|
||||||
|
codec: 'json'
|
||||||
|
method: 'post'
|
||||||
|
request:
|
||||||
|
retry_max_duration_secs: 10
|
||||||
|
headers:
|
||||||
|
x-api-key: ${LOGFLARE_PUBLIC_ACCESS_TOKEN?LOGFLARE_PUBLIC_ACCESS_TOKEN is required}
|
||||||
|
uri: 'http://analytics:4000/api/logs?source_name=realtime.logs.prod'
|
||||||
|
logflare_rest:
|
||||||
|
type: 'http'
|
||||||
|
inputs:
|
||||||
|
- rest_logs
|
||||||
|
encoding:
|
||||||
|
codec: 'json'
|
||||||
|
method: 'post'
|
||||||
|
request:
|
||||||
|
retry_max_duration_secs: 10
|
||||||
|
headers:
|
||||||
|
x-api-key: ${LOGFLARE_PUBLIC_ACCESS_TOKEN?LOGFLARE_PUBLIC_ACCESS_TOKEN is required}
|
||||||
|
uri: 'http://analytics:4000/api/logs?source_name=postgREST.logs.prod'
|
||||||
|
logflare_db:
|
||||||
|
type: 'http'
|
||||||
|
inputs:
|
||||||
|
- db_logs
|
||||||
|
encoding:
|
||||||
|
codec: 'json'
|
||||||
|
method: 'post'
|
||||||
|
request:
|
||||||
|
retry_max_duration_secs: 10
|
||||||
|
headers:
|
||||||
|
x-api-key: ${LOGFLARE_PUBLIC_ACCESS_TOKEN?LOGFLARE_PUBLIC_ACCESS_TOKEN is required}
|
||||||
|
uri: 'http://kong:8000/analytics/v1/api/logs?source_name=postgres.logs'
|
||||||
|
logflare_functions:
|
||||||
|
type: 'http'
|
||||||
|
inputs:
|
||||||
|
- functions_logs
|
||||||
|
encoding:
|
||||||
|
codec: 'json'
|
||||||
|
method: 'post'
|
||||||
|
request:
|
||||||
|
retry_max_duration_secs: 10
|
||||||
|
headers:
|
||||||
|
x-api-key: ${LOGFLARE_PUBLIC_ACCESS_TOKEN?LOGFLARE_PUBLIC_ACCESS_TOKEN is required}
|
||||||
|
uri: 'http://analytics:4000/api/logs?source_name=deno-relay-logs'
|
||||||
|
logflare_storage:
|
||||||
|
type: 'http'
|
||||||
|
inputs:
|
||||||
|
- storage_logs
|
||||||
|
encoding:
|
||||||
|
codec: 'json'
|
||||||
|
method: 'post'
|
||||||
|
request:
|
||||||
|
retry_max_duration_secs: 10
|
||||||
|
headers:
|
||||||
|
x-api-key: ${LOGFLARE_PUBLIC_ACCESS_TOKEN?LOGFLARE_PUBLIC_ACCESS_TOKEN is required}
|
||||||
|
uri: 'http://analytics:4000/api/logs?source_name=storage.logs.prod.2'
|
||||||
|
logflare_kong:
|
||||||
|
type: 'http'
|
||||||
|
inputs:
|
||||||
|
- kong_logs
|
||||||
|
- kong_err
|
||||||
|
encoding:
|
||||||
|
codec: 'json'
|
||||||
|
method: 'post'
|
||||||
|
request:
|
||||||
|
retry_max_duration_secs: 10
|
||||||
|
headers:
|
||||||
|
x-api-key: ${LOGFLARE_PUBLIC_ACCESS_TOKEN?LOGFLARE_PUBLIC_ACCESS_TOKEN is required}
|
||||||
|
uri: 'http://analytics:4000/api/logs?source_name=cloudflare.logs.prod'
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
{:ok, _} = Application.ensure_all_started(:supavisor)
|
||||||
|
{:ok, version} =
|
||||||
|
case Supavisor.Repo.query!("select version()") do
|
||||||
|
%{rows: [[ver]]} -> Supavisor.Helpers.parse_pg_version(ver)
|
||||||
|
_ -> nil
|
||||||
|
end
|
||||||
|
params = %{
|
||||||
|
"external_id" => System.get_env("POOLER_TENANT_ID"),
|
||||||
|
"db_host" => "db",
|
||||||
|
"db_port" => System.get_env("POSTGRES_PORT"),
|
||||||
|
"db_database" => System.get_env("POSTGRES_DB"),
|
||||||
|
"require_user" => false,
|
||||||
|
"auth_query" => "SELECT * FROM pgbouncer.get_auth($1)",
|
||||||
|
"default_max_clients" => System.get_env("POOLER_MAX_CLIENT_CONN"),
|
||||||
|
"default_pool_size" => System.get_env("POOLER_DEFAULT_POOL_SIZE"),
|
||||||
|
"default_parameter_status" => %{"server_version" => version},
|
||||||
|
"users" => [%{
|
||||||
|
"db_user" => "pgbouncer",
|
||||||
|
"db_password" => System.get_env("POSTGRES_PASSWORD"),
|
||||||
|
"mode_type" => System.get_env("POOLER_POOL_MODE"),
|
||||||
|
"pool_size" => System.get_env("POOLER_DEFAULT_POOL_SIZE"),
|
||||||
|
"is_manager" => true
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
if !Supavisor.Tenants.get_tenant_by_external_id(params["external_id"]) do
|
||||||
|
{:ok, _} = Supavisor.Tenants.create_tenant(params)
|
||||||
|
end
|
||||||
Reference in New Issue
Block a user