# Cloud Supabase Env Cleanup & JWKS Migration Implementation Plan > **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. **Goal:** 切换到云 Supabase 后,移除本地自托管 Supabase 基础设施变量与编排,保留 Redis + DB + init-job,并将后端 JWT 验签从 `JWT_SECRET` 改为 JWKS 公钥验签。 **Architecture:** 后端配置收敛到“业务运行所需最小集合”(Supabase URL/anon/service role + DB + Redis)。认证链路采用 JWKS 拉取公钥并按 `kid` 验签,替代共享密钥 HS256。Docker 编排只保留业务依赖(redis、db、init-job),不再编排本地 Supabase 全家桶。 **Tech Stack:** FastAPI, Pydantic Settings, PyJWT (PyJWKClient), Docker Compose, pytest --- ### Task 1: 固化云模式配置契约(先测后改) **Files:** - Modify: `backend/tests/unit/test_settings_supabase_env.py` - Modify: `.env.example` **Step 1: 写失败测试,定义新 Supabase 配置契约** ```python def test_social_prefixed_supabase_env_populates_settings(monkeypatch: MonkeyPatch) -> None: monkeypatch.setenv("SOCIAL_SUPABASE__PUBLIC_URL", "https://project.example.supabase.co") monkeypatch.setenv("SOCIAL_SUPABASE__ANON_KEY", "anon-key") monkeypatch.setenv("SOCIAL_SUPABASE__SERVICE_ROLE_KEY", "service-key") monkeypatch.setenv("SOCIAL_SUPABASE__JWT_AUDIENCE", "authenticated") settings = Settings() assert settings.supabase.public_url == "https://project.example.supabase.co" assert settings.supabase.jwt_issuer == "https://project.example.supabase.co/auth/v1" assert settings.supabase.jwks_url.endswith("/auth/v1/.well-known/jwks.json") ``` **Step 2: 运行测试确认失败** Run: `uv run pytest backend/tests/unit/test_settings_supabase_env.py -v` Expected: FAIL(`public_url/jwks_url` 字段不存在或断言失败) **Step 3: 最小改动让测试通过(仅 settings 相关,逻辑改动在后续任务)** 更新 `.env.example` 为云模式最小变量草案(先占位,后续任务会补最终文案): - `SOCIAL_SUPABASE__PUBLIC_URL=` - `SOCIAL_SUPABASE__ANON_KEY=` - `SOCIAL_SUPABASE__SERVICE_ROLE_KEY=` - `SOCIAL_SUPABASE__JWT_AUDIENCE=authenticated` - `SOCIAL_SUPABASE__JWT_ISSUER=`(可选,默认由 PUBLIC_URL 推导) - `SOCIAL_SUPABASE__JWKS_URL=`(可选,默认由 PUBLIC_URL 推导) **Step 4: 运行测试确认通过** Run: `uv run pytest backend/tests/unit/test_settings_supabase_env.py -v` Expected: PASS **Step 5: Commit** ```bash git add backend/tests/unit/test_settings_supabase_env.py .env.example git commit -m "test: define cloud supabase settings contract" ``` ### Task 2: 重构 SupabaseSettings(移除 JWT_SECRET 依赖) **Files:** - Modify: `backend/src/core/config/settings.py` - Modify: `backend/tests/unit/test_settings_supabase_env.py` **Step 1: 写失败测试,约束默认推导行为** ```python assert settings.supabase.jwt_issuer == "https://project.example.supabase.co/auth/v1" assert settings.supabase.jwks_url == "https://project.example.supabase.co/auth/v1/.well-known/jwks.json" assert "jwt_secret" not in settings.model_dump()["supabase"] ``` **Step 2: 运行测试确认失败** Run: `uv run pytest backend/tests/unit/test_settings_supabase_env.py -v` Expected: FAIL **Step 3: 实现最小配置重构** 在 `SupabaseSettings` 中改为: - 必填:`public_url`, `anon_key`, `service_role_key` - 可选:`site_url`, `additional_redirect_urls` - 新增:`jwt_audience`(默认 `authenticated`)、`jwt_issuer`(默认 `${public_url}/auth/v1`)、`jwks_url`(默认 `${jwt_issuer}/.well-known/jwks.json`) - 删除:`jwt_secret`, `public_scheme`, `public_host`, `kong_http_port`, `kong_https_port` **Step 4: 运行测试确认通过** Run: `uv run pytest backend/tests/unit/test_settings_supabase_env.py -v` Expected: PASS **Step 5: Commit** ```bash git add backend/src/core/config/settings.py backend/tests/unit/test_settings_supabase_env.py git commit -m "refactor: migrate supabase config to cloud jwks fields" ``` ### Task 3: 引入 JWKS 验签组件并接入认证依赖 **Files:** - Create: `backend/src/core/auth/jwt_verifier.py` - Modify: `backend/src/v1/users/dependencies.py` - Create: `backend/tests/unit/core/auth/test_jwt_verifier.py` **Step 1: 先写失败测试(JWT 验签核心行为)** ```python def test_verify_token_with_jwks_success(...): claims = verifier.verify(token) assert claims["sub"] == str(user_id) def test_verify_token_rejects_invalid_issuer(...): with pytest.raises(TokenValidationError): verifier.verify(token_with_wrong_iss) ``` **Step 2: 运行测试确认失败** Run: `uv run pytest backend/tests/unit/core/auth/test_jwt_verifier.py -v` Expected: FAIL(模块/类不存在) **Step 3: 实现最小 JWKS 验签逻辑** ```python class JwtVerifier: def __init__(self, jwks_url: str, issuer: str, audience: str) -> None: ... def verify(self, token: str) -> dict[str, Any]: key = self._jwks_client.get_signing_key_from_jwt(token) return jwt.decode( token, key.key, algorithms=["RS256", "ES256"], audience=self._audience, issuer=self._issuer, options={"require": ["sub", "aud", "iss", "exp"]}, ) ``` 在 `get_current_user` 中替换原 `jwt_secret + HS256` 验签,统一映射为现有 401/503 语义。 **Step 4: 运行测试确认通过** Run: `uv run pytest backend/tests/unit/core/auth/test_jwt_verifier.py -v` Expected: PASS **Step 5: Commit** ```bash git add backend/src/core/auth/jwt_verifier.py backend/src/v1/users/dependencies.py backend/tests/unit/core/auth/test_jwt_verifier.py git commit -m "feat: validate access tokens via supabase jwks" ``` ### Task 4: 回归认证路径与 live 测试兼容 **Files:** - Modify: `backend/tests/integration/v1/agent/test_sse_flow_live.py` - Modify: `backend/tests/integration/test_auth_routes.py`(如需) **Step 1: 写失败测试/调整 live 测试生成 token 方式** 将 live 测试从“本地签发 HS256 token”改为“通过真实登录拿 access token”或“无测试账号时 skip”。 ```python if not os.getenv("AGENT_LIVE_EMAIL") or not os.getenv("AGENT_LIVE_PASSWORD"): pytest.skip("missing live supabase credentials") ``` **Step 2: 运行相关测试确认失败(或旧逻辑不适配)** Run: `uv run pytest backend/tests/integration/v1/agent/test_sse_flow_live.py -m live -v` Expected: 在旧代码下不可用/依赖 jwt_secret **Step 3: 完成最小实现改造** - 移除 `config.supabase.jwt_secret` 的测试依赖。 - 保持 `@pytest.mark.live` 行为不变,避免影响常规 CI。 **Step 4: 运行测试确认通过(或受控 skip)** Run: `uv run pytest backend/tests/integration/v1/agent/test_sse_flow_live.py -m live -v` Expected: PASS 或可解释的 SKIP(凭证缺失) **Step 5: Commit** ```bash git add backend/tests/integration/v1/agent/test_sse_flow_live.py backend/tests/integration/test_auth_routes.py git commit -m "test: align live auth flow with cloud supabase tokens" ``` ### Task 5: 裁剪 Docker Compose(移除本地 Supabase,保留 Redis/DB/init-job) **Files:** - Modify: `infra/docker/docker-compose.yml` **Step 1: 写失败验证(compose 结构断言)** 添加一个轻量脚本化检查(可在本任务临时执行,不必入库): ```bash docker compose --env-file .env -f infra/docker/docker-compose.yml config ``` 在改造前记录当前包含的 Supabase 服务(`studio/kong/auth/rest/...`)作为对照。 **Step 2: 执行检查确认当前状态(基线)** Run: `docker compose --env-file .env -f infra/docker/docker-compose.yml config` Expected: 输出包含 Supabase 全家桶服务 **Step 3: 最小实现裁剪** - 删除服务:`studio/kong/mail-templates/auth/rest/realtime/storage/imgproxy/meta/functions/analytics/vector/supavisor` - 保留服务:`redis`, `db`, `init-job` - `init-job` 环境变量移除:`SOCIAL_SUPABASE__ANON_KEY`, `SOCIAL_SUPABASE__SERVICE_ROLE_KEY`, `SOCIAL_SUPABASE__JWT_SECRET` - `db` 服务切换为业务最小化所需配置(仅数据库启动与健康检查必需) **Step 4: 运行 compose 校验** Run: `docker compose --env-file .env -f infra/docker/docker-compose.yml config` Expected: PASS,且仅保留 redis/db/init-job **Step 5: Commit** ```bash git add infra/docker/docker-compose.yml git commit -m "refactor: remove local supabase stack from compose" ``` ### Task 6: 清理环境模板与运行文档 **Files:** - Modify: `.env.example` - Modify: `docs/runtime/runtime-runbook.md` - Modify: `infra/scripts/dev-migrate.sh` **Step 1: 先写文档/模板检查点(人工可核验)** 定义必须满足: - `.env.example` 不再包含本地 Supabase 基础设施变量(logflare/pooler/studio/kong/jwt_secret 等) - 保留并标注后端必需项:`PUBLIC_URL`, `ANON_KEY`, `SERVICE_ROLE_KEY` - runbook 的健康检查改为 Redis/DB/Web,而非 Kong **Step 2: 运行基线检查(改造前)** Run: `uv run pytest backend/tests/unit/test_settings_supabase_env.py -v` Expected: 作为环境模板改造后的回归基线 **Step 3: 最小实现文档更新** - `docs/runtime/runtime-runbook.md`:把“启动基础设施”描述改为 `redis + db`。 - `infra/scripts/dev-migrate.sh`:将提示从“Requires Supabase services”改为“Requires db/redis services”。 - `.env.example`:按云模式分组,明确前端/后端变量边界。 **Step 4: 运行检查确认通过** Run: `docker compose --env-file .env -f infra/docker/docker-compose.yml config` Expected: PASS **Step 5: Commit** ```bash git add .env.example docs/runtime/runtime-runbook.md infra/scripts/dev-migrate.sh git commit -m "docs: update runtime guide for cloud supabase mode" ``` ### Task 7: 全量验证与发布前检查 **Files:** - Modify: `docs/runtime/runtime-runbook.md`(记录验证命令与结果) **Step 1: 运行静态检查** Run: `uv run ruff check backend/src backend/tests` Expected: PASS **Step 2: 运行类型检查** Run: `uv run basedpyright` Expected: PASS **Step 3: 运行测试(按影响面)** Run: `uv run pytest backend/tests/unit/test_settings_supabase_env.py backend/tests/unit/core/auth/test_jwt_verifier.py -v` Expected: PASS Run: `uv run pytest backend/tests/integration/test_users_routes.py backend/tests/integration/test_auth_routes.py -v` Expected: PASS **Step 4: 运行运行时门禁验证** Run: `docker compose --env-file .env -f infra/docker/docker-compose.yml up -d redis db && docker compose --env-file .env -f infra/docker/docker-compose.yml run --rm --build init-job uv run python -m core.runtime.cli bootstrap` Expected: PASS(迁移 + init-data 成功) **Step 5: Commit** ```bash git add docs/runtime/runtime-runbook.md git commit -m "chore: record cloud supabase migration verification" ```