# Supabase 前端访问策略

更新时间：2026-06-28

这份文档用于回答一个关键架构问题：Taro/H5/小程序前端到底应该直接调用 Supabase，还是调用我们自己的 `apps/api` 后端？

结论：采用 Supabase-first 的混合架构。优先使用 Supabase Auth、RLS、视图、RPC、Storage 等原生能力；涉及密钥、跨表事务、支付、导入、对象存储签名、CRM、AI 和复杂权限的业务命令，放到 Edge Functions、`apps/api` 或 worker。

## 官方依据

Supabase 官方文档的核心原则：

- 前端可以使用 Supabase client/Data API 访问数据，但前提是启用 RLS，并且只授予最小权限。
  - https://supabase.com/docs/guides/database/secure-data
  - https://supabase.com/docs/guides/database/postgres/row-level-security
- 前端应使用 publishable key；publishable key 可以暴露，但必须配合 RLS 和最小权限。
  - https://supabase.com/docs/guides/database/secure-data
  - https://supabase.com/docs/guides/getting-started/api-keys
- secret key / service role key 永远不能暴露在前端，因为它们会绕过 RLS。
  - https://supabase.com/docs/guides/getting-started/api-keys
- 复杂服务端逻辑、第三方 API、webhook、密钥和数据库连接应放在服务端，例如 Edge Functions、worker 或自有后端。
  - https://supabase.com/docs/guides/functions
- Supabase Auth 使用 JWT，JWT 可以和 RLS、服务端鉴权结合。
  - https://supabase.com/docs/guides/auth
  - https://supabase.com/docs/guides/auth/jwts

## 对本项目的判断

我们的系统不是简单 CRUD 应用，而是面向同行销售的题库 SaaS：

- 多租户隔离。
- 平台超级管理员和租户管理员双后台。
- 租户内自定义角色、运营、教师、销售、代理、学生。
- 订单、支付、激活码、权益、视频次数。
- 题库导入、内容审核、资源台账。
- 阿里云 OSS、腾讯 COS、Supabase Storage 混合对象存储。
- CRM 推送、销售首绑、分佣结算。
- 未来还要接微信/QQ 登录、微信/支付宝支付、AI 报告。

这些都包含复杂业务规则、密钥、幂等、审计和跨表事务。因此不能让 Taro 前端直接写业务表来替代后端。

## 推荐架构

```text
Taro/H5/小程序
  ├─ Supabase client
  │   ├─ Auth session/JWT
  │   ├─ 可选：Realtime
  │   ├─ 可选：严格 RLS 下的 table/view 只读或低风险自助写入
  │   └─ 可选：RPC 调用短事务业务函数
  │
  └─ apps/api / Edge Functions 业务命令层
      ├─ 校验 Supabase JWT/session
      ├─ 解析 tenant
      ├─ 校验角色和 permissions
      ├─ 执行业务事务
      ├─ 写审计日志
      ├─ 签发对象存储 URL
      └─ 调用支付/短信/微信/QQ/CRM/AI provider
```

`apps/api` 可以部署为我们自己的 Node.js API，也可以在部分云原生场景下拆为 Supabase Edge Functions。对当前项目而言，保留 `apps/api` 更适合复杂业务、国内 provider、自托管和后续 worker。

## 功能落位选择

| 场景 | 首选方案 | 说明 |
| --- | --- | --- |
| 公开只读数据 | view/table + RLS | 例如公开 Banner、FAQ、主题配置 |
| 当前用户简单自助读写 | table/view + RLS | 必须有最小 policy 和越权测试 |
| 原子状态变化 | Postgres RPC | 例如激活码兑换、计数扣减、幂等状态流转 |
| 跨表聚合读取 | view/RPC 或 `apps/api` | 简单聚合可用 view，复杂权限用 API |
| 支付/短信/OAuth/CRM/AI | Edge Function 或 `apps/api` | 需要密钥和外部 HTTP 调用 |
| 大批量导入/重试/定时任务 | worker | API 只提交 job 和查询状态 |
| 私有对象存储签名 | Edge Function 或 `apps/api` | 必须先校验 `content_assets` 和权益 |

选择原则：能用 RLS/view/RPC 安全解决的，就不要写一堆重复后端代码；需要密钥、副作用、审计、幂等和第三方 provider 的，就不要放到前端直连表。

## 前端直连 Supabase 的适用范围

| 能力 | 是否建议前端直连 Supabase | 说明 |
| --- | --- | --- |
| Supabase Auth session | 建议 | H5 可直接用 `@supabase/supabase-js`；小程序需先验证运行时兼容性 |
| 获取当前用户 JWT | 建议 | API 请求统一带 `Authorization: Bearer <access_token>` |
| 低风险公开只读数据 | 可选 | 仅限 RLS、grant、索引都成熟后 |
| 原子业务 RPC | 可选 | 仅限函数内部完成 tenant/user/permission 校验 |
| Realtime 通知 | 可选 | 非敏感通知、学习状态刷新可考虑；后台敏感队列不建议前端订阅 |
| Public Storage 公开资源 | 可选 | 真正公开的 Logo、主题图可直连 CDN/公开 bucket |
| 私有资料/PDF/视频 | 不建议 | 必须通过 `content_assets` 权限校验和后端签名 |
| 学习记录/答题/错题/收藏 | 不建议直接写表 | 可通过后端 API 或经过安全评审的 RPC |
| 订单/支付/激活码/优惠券 | 禁止 | 必须走后端事务、验签和幂等 |
| 租户后台配置 | 禁止 | 涉及权限、密钥、审计 |
| 内容导入/题库维护 | 禁止 | 必须走 preview/import/job/issues 管线 |
| 平台超级后台 | 禁止 | 必须走平台管理员鉴权和审计 |

## Taro 运行时建议

Taro 要同时支持 H5 和微信小程序。Supabase 官方 JavaScript client 是通用 JS SDK，并提供 browser、React Native、自定义 fetch/storage 等配置方式，但微信小程序环境不等同于标准浏览器。

因此建议：

1. H5 端优先使用 `@supabase/supabase-js` 管理 Auth session。
2. 微信小程序端先做兼容性验证：
   - `fetch` 或 request adapter。
   - storage adapter。
   - URL polyfill。
   - token auto refresh。
3. 如果小程序端 `supabase-js` 兼容成本高，则小程序只调用 `apps/api/auth/*`：
   - 前端把微信 `code` 或手机号验证码发给后端。
   - 后端调用 Supabase Auth/Admin 或自有 session 逻辑换取可信 session。
   - 前端保存后端返回的 access token/session。
4. 无论 H5 还是小程序，复杂业务数据默认调用 `apps/api`、Edge Function 或 RPC，不直接写 Supabase 表。

## 推荐前端公开配置

只允许出现在前端构建变量或 H5 `runtime-config.json` 中的公开配置：

```text
TARO_APP_API_BASE_URL=https://api.example.com
TARO_APP_SUPABASE_URL=https://<project-ref>.supabase.co
TARO_APP_SUPABASE_PUBLISHABLE_KEY=sb_publishable_xxx
TARO_APP_TENANT_CODE=<optional>
```

H5 生产部署优先使用每个静态目录自己的 `runtime-config.json`，字段名可用 `apiBaseUrl`、`supabaseUrl`、`supabasePublishableKey`、`tenantCode`。这样学生端、租户后台、平台后台可以共用构建流程，各自按域名目录独立配置。完整规则见 `docs/refactor/taro-h5-deployment.md`。

禁止出现在前端：

```text
SUPABASE_SECRET_KEY
SUPABASE_SERVICE_ROLE_KEY
DATABASE_URL
ALIYUN_OSS_ACCESS_KEY_SECRET
TENCENT_COS_SECRET_KEY
WECHAT_PAY_PRIVATE_KEY
ALIPAY_APP_PRIVATE_KEY
AUTH_SESSION_SECRET
PLATFORM_ADMIN_API_KEY
```

## API 请求目标形态

历史迁移期接口曾允许：

```text
Authorization: Bearer <tk_session>
x-tenant-id: <tenantId>
x-user-id: <userId>
```

新的 Taro client 已经禁止页面使用 `x-user-id` 表示当前用户，并默认采用 Supabase JWT 优先：

```text
Authorization: Bearer <supabase_access_token>
x-tenant-id: <tenantId>   # 可选租户上下文；不是身份来源，必须与 JWT tenant claim 或 membership 匹配
```

`apps/taro/src/services/api.ts` 的 `apiRequest` 默认 `authMode='auto'`：H5 优先发送 Supabase access token，没有 Supabase token 时才兜底迁移期 `tk_` session。公共接口必须显式使用 `authMode='none'`，迁移演练才允许使用 `authMode='legacy'`，云端正式回归可用 `authMode='supabase'` 强制暴露残留 legacy 依赖。`headers.Authorization` 和 `headers['x-tenant-id']` 是保留 header，页面代码不能覆盖。

当前 `apps/api` 已支持 Supabase Auth JWT 验签，并通过 `auth.users.id -> platform_users.auth_user_id -> tenant_memberships` 映射到业务身份。H5/Taro 登录后可以直接把 Supabase access token 放到 `Authorization`。如果 JWT 内没有 `tenant_id` claim，前端仍要根据域名/小程序码解析后的租户传 `x-tenant-id`，后端会校验该用户确实属于该租户。

生产时后端负责：

- 验证 JWT。
- 从 JWT 映射业务 userId。
- 根据 host/tenantCode/JWT claims/请求上下文解析 tenant。
- 校验用户属于该租户。
- 校验角色和权限。
- 执行业务逻辑。

前端不再传 `x-user-id`，也不能靠传 `tenantId` 获得跨租户数据。

生产 API 环境变量至少要配置：

```text
AUTH_JWT_JWKS_URL=https://<supabase-auth-host>/auth/v1/.well-known/jwks.json
# 或自托管/兼容模式下使用强随机 secret
AUTH_JWT_SECRET=<strong-jwt-secret>
AUTH_JWT_AUDIENCE=authenticated
ALLOW_LEGACY_AUTH_HEADERS=false
ALLOW_PLATFORM_ADMIN_KEY=false
```

## 对后续 AI/开发者的硬性约束

- 不要把 Supabase-first 误解成前端直写所有表。
- 不要让 Taro 直接写订单、支付、权益、租户配置、CRM、导入相关表。
- 不要把 service role/secret key 放进 Taro。
- 不要在页面里绕过 `apiRequest` 手写 `Taro.request`、`Authorization`、`x-tenant-id` 或 `x-user-id`。
- 不要为了少写接口而放宽 RLS。
- 新增前端直连 Supabase table/view/RPC 之前，必须先补：
  - 明确 RLS policy。
  - 最小 grant。
  - 跨租户测试。
  - 权限测试。
  - 性能索引。
- 若某个功能需要密钥、webhook、审计、幂等、长任务或第三方 provider，一律放到 `apps/api`、Edge Function 或 worker。
