Files
social-app/docs/plans/2026-03-09-cloud-supabase-jwks-migration-plan.md
T
qzl 6fe2e7b6c3 refactor: 迁移本地 Supabase 到云端,使用 JWKS 进行 JWT 验证
- 新增 JwtVerifier 支持 RS256 + JWKS 验证
- 简化 docker-compose,删除本地 Supabase 服务(kong/auth/storage等)
- 删除冗余的 Supabase 配置文件(volumes目录)
- 适配测试用例以支持新配置方式
- 更新运行时文档和迁移计划
2026-03-09 18:03:04 +08:00

11 KiB
Raw Blame History

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 配置契约

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: FAILpublic_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

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: 写失败测试,约束默认推导行为

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

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 验签核心行为)

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 验签逻辑

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

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”。

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

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 结构断言)

添加一个轻量脚本化检查(可在本任务临时执行,不必入库):

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

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

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

git add docs/runtime/runtime-runbook.md
git commit -m "chore: record cloud supabase migration verification"