npm package: @sunboyoo/supabase-rbac
Description: Industrial-grade RBAC helper for Supabase multi-app projects (JWT role_ids + optional permission RPC + React hooks + pg manager + CLI).
Latest version: 1.1.0

# @sunboyoo/supabase-rbac 集成指南（README）

> 工业级、多 App、多模块 RBAC（Supabase + PostgreSQL + TypeScript/React）
> **数据库 RLS 才是最终权威**；前端仅用于 UI 显隐（可选），避免“前端鉴权等于安全”的误区。

---

## 目录

* [功能概览](#功能概览)
* [核心设计原则](#核心设计原则)
* [安装与初始化](#安装与初始化)
* [Supabase Dashboard 必做配置](#supabase-dashboard-必做配置)
* [数据库部署与验证](#数据库部署与验证)
* [前端集成（React）](#前端集成react)
* [服务端/管理端集成（Node）](#服务端管理端集成node)
* [可选增强：permission-name 语义化判定](#可选增强permission-name-语义化判定)
* [最佳实践：权限命名与角色管理](#最佳实践权限命名与角色管理)
* [常见问题（FAQ）](#常见问题faq)
* [安全边界与威胁模型](#安全边界与威胁模型)
* [版本与兼容性](#版本与兼容性)

---

## 功能概览

* ✅ **多应用支持（app_id）**：同一 Supabase 项目支持多个业务 App/模块。
* ✅ **自动合并 global 角色**：`app1` + `global` 角色集合合并用于判定。
* ✅ **数据库强制鉴权（RLS）**：策略调用 `rbac.has_perm(app_id, perm_name)`。
* ✅ **JWT 注入 role_ids（UUID）**：通过 Supabase Auth Custom Access Token Hook 注入。
* ✅ **React 集成**：`RBACProvider` + `useRBAC()`。
* ✅ **Server 管理端**：`RBACManager`（pg 直连，不依赖 PostgREST 暴露 rbac schema）。
* ✅ **CLI 初始化**：`rbac-init` 将 migrations 复制到项目 `supabase/migrations`。
* 🧩（可选）**permission-name 前端语义化判定**：通过受控 RPC 拉取“我的权限名列表”。

---

## 核心设计原则

### 1) DB 是权威：安全必须由 RLS 强制执行

* 前端的 `hasRole()` / `hasPermission()` **只能用于 UI 显隐或交互优化**。
* **真正的安全**来自业务表的 RLS：没有权限时数据库返回 403/0 rows。

### 2) RBAC schema 默认不暴露

* 强烈建议：Supabase Dashboard → **API → Exposed Schemas** 中移除 `rbac` / `hooks`。
* 这样可最大化降低：

  * 权限字典枚举
  * 意外暴露 RBAC 表
  * 运维期间策略漂移导致的风险

### 3) role_ids 放 JWT，稳定且 rename-safe

* JWT 中存的是 role UUID，而不是 role name：

  * 角色重命名不会影响鉴权
  * 不依赖 role name join
  * token 体积小

---

## 安装与初始化

### 1) 安装

```bash
pnpm add @sunboyoo/supabase-rbac
```

### 2) 初始化 migrations（复制 SQL）

在你的项目根目录执行：

```bash
npx rbac-init
# 或
pnpm exec rbac-init
```

常用参数：

```bash
npx rbac-init --dest ./supabase/migrations
npx rbac-init --dry-run
npx rbac-init --force
npx rbac-init --verbose
```

> ✅ **推荐**：默认不覆盖已有文件（避免误伤本地改动）。
> 如需覆盖，请显式 `--force`。

---

## Supabase Dashboard 必做配置

### 1) 移除 Exposed Schemas

路径：**Supabase Dashboard → API → Exposed Schemas**

* ✅ **保留**：`public`（以及你业务需要的 schema）
* ❌ **移除**：`rbac`、`hooks`

> 这是安全关键点：避免 PostgREST 暴露 RBAC 表/视图。

### 2) 绑定 Custom Access Token Hook

路径：**Dashboard → Auth → Hooks → Custom access token hook**

选择：

* Function：`hooks.custom_access_token_hook`

此 Hook 会在登录/refresh token 时执行，把每个 app 的 role_ids 注入到 JWT claims。

---

## 数据库部署与验证

### 1) 推送迁移

```bash
supabase db push
```

### 2) 验证 Hook 注入是否成功

用户登录后，在前端拿到 session 的 `access_token` 并 decode（仅用于验证）：

你应能看到类似结构（示意）：

```json
{
  "app_metadata": {
    "role_ids": {
      "app1": ["uuid-..."],
      "global": ["uuid-..."]
    }
  }
}
```

> 若看不到 `role_ids`：通常是 **Hook 没绑定** 或用户还未刷新 session。

### 3) 角色变更后必须 refresh token

当你在管理端更新用户角色后：

```ts
await supabase.auth.refreshSession();
```

否则用户 token 仍是旧 claims。

### 4) 业务表 RLS 配置示例

在业务表上配置 RLS 策略调用 `rbac.has_perm()`：

```sql
-- Example business table (app1-private) and RLS policies.
-- In real projects, you usually create one migration per module/table group.

create table if not exists public.orders (
  id           uuid primary key default gen_random_uuid(),
  created_at   timestamptz not null default now(),
  customer     text,
  amount_cents int not null default 0
);

alter table public.orders enable row level security;

drop policy if exists app1_order_select on public.orders;
create policy app1_order_select
on public.orders
for select
using (
  (select rbac.has_perm('app1', 'order.read'))
);

drop policy if exists app1_order_insert on public.orders;
create policy app1_order_insert
on public.orders
for insert
with check (
  (select rbac.has_perm('app1', 'order.create'))
);

drop policy if exists app1_order_update on public.orders;
create policy app1_order_update
on public.orders
for update
using (
  (select rbac.has_perm('app1', 'order.update'))
);

drop policy if exists app1_order_delete on public.orders;
create policy app1_order_delete
on public.orders
for delete
using (
  (select rbac.has_perm('app1', 'order.delete'))
);
```

**要点：**

* 用 `(select rbac.has_perm(...))` 包裹：诱导 PostgreSQL 优化器做 InitPlan（一次计算）
* 即使未完全 InitPlan，`has_perm` 内部也有事务级缓存，避免 N+1 查询
* 客户端正常 CRUD；无权限时：SELECT 返回 0 行，INSERT/UPDATE/DELETE 返回 403

---

## 前端集成（React）

### 1) 包裹 Provider

```tsx
import { RBACProvider } from "@sunboyoo/supabase-rbac/client";
import { supabase } from "./supabaseClient";

export function Root() {
  return (
    <RBACProvider client={supabase} appId="app1">
      <App />
    </RBACProvider>
  );
}
```

### 2) useRBAC 基础用法

```tsx
import { useRBAC } from "@sunboyoo/supabase-rbac/client";

export function Toolbar() {
  const {
    isAuthenticated,
    isRBACReady,
    roleIds,
    hasRole,
    refresh
  } = useRBAC();

  if (!isRBACReady) return null;

  const ADMIN_ROLE_ID = "00000000-0000-0000-0000-000000000001"; // 示例：建议来自你的常量映射

  return (
    <div>
      {isAuthenticated && <span>Signed in</span>}

      {hasRole([ADMIN_ROLE_ID]) && (
        <button>Admin Only</button>
      )}

      <button onClick={() => refresh(true)}>Force Refresh Token</button>
    </div>
  );
}
```

### 3) 推荐：role UUID 不要硬编码

你有 3 种推荐方式：

1. **Seed 输出常量文件**（推荐）
2. 管理端查询后写入配置/环境变量
3. 业务端后端 API 下发（适用于动态系统）

---

## 服务端/管理端集成（Node）

> RBACManager 采用 **pg 直连数据库**，不依赖 PostgREST 暴露 `rbac` schema，符合安全最佳实践。

### 1) 初始化

```ts
import { RBACManager } from "@sunboyoo/supabase-rbac/server";

const rbac = new RBACManager({
  connectionString: process.env.SUPABASE_DB_URL!, // 推荐使用 Supabase 提供的数据库连接串
  rbacSchema: "rbac" // 可省略
});
```

### 2) 创建权限/角色/绑定

```ts
const appId = "app1";

// 0) 确保 app 已存在（外键约束）
await rbac.createApp({ id: appId, name: "App 1" });

// 1) 创建权限字典
const permId = await rbac.createPermission({
  appId,
  name: "order.delete",
  description: "Delete orders"
});

// 2) 创建角色
const roleId = await rbac.createRole({
  appId,
  name: "Admin"
});

// 3) 绑定角色-权限
await rbac.grantPermissionToRole({ appId, roleId, permissionId: permId });

// 4) 给用户分配角色
await rbac.assignRole({ userId: targetUserId, roleId });
```

### 3) 关闭连接

```ts
await rbac.close();
```

---

## 可选增强：permission-name 语义化判定

### 为什么是“可选”？

* 优点：业务代码更可维护：`hasPermission('order.delete')`
* 代价：增加一个 RPC 查询（你需要接受额外 DB 负载 + 一定暴露面）

### 如何启用

1. 确保数据库已部署 permission-name RPC（例如 `get_my_permissions`）
2. Provider 开启 permissions 选项：

```tsx
<RBACProvider
  client={supabase}
  appId="app1"
  permissions={{
    enabled: true,
    rpcName: "get_my_permissions",
    ttlMs: 60000,
    nonBlocking: true
  }}
>
  <App />
</RBACProvider>
```

### RPC 参考实现（可选）

将 RPC 放在你 **已暴露的 schema**（例如 `public`），RBAC 表仍保持隐藏：

```sql
create or replace function public.get_my_permissions(target_app_id text)
returns table(permission_name text)
language sql
security definer
set search_path = ''
as $$
  select distinct p.name as permission_name
  from rbac.user_roles ur
  join rbac.roles r on r.id = ur.role_id
  join rbac.role_permissions rp on rp.role_id = r.id and rp.app_id = r.app_id
  join rbac.permissions p on p.id = rp.permission_id and p.app_id = r.app_id
  where ur.user_id = auth.uid()
    and r.app_id in (target_app_id, 'global');
$$;

grant execute on function public.get_my_permissions(text) to authenticated;
```

> 注意：如需自定义 `global` key，请同步调整 RPC 与前端 `globalAppId`。

使用：

```tsx
const { hasPermission, isPermissionReady } = useRBAC();
if (!isPermissionReady) return null;

return hasPermission("order.delete") ? <DeleteBtn /> : null;
```

### 强提醒：permission-name 仍然不是安全边界

即使前端显示了按钮，最终能否 DELETE 仍由 RLS 决定。

---

## 最佳实践：权限命名与角色管理

### 权限命名规范

* 全小写
* 点号分层
* 稳定、不频繁改动

示例：

* `order.read`
* `order.create`
* `order.update`
* `order.delete`
* `payroll.export`

### global-mirror 语义

* `global` 是真实 app_id（不是 NULL）
* 如果 global 角色要跨 App 赋权：

  * 必须在 `rbac.permissions(app_id='global')` 中创建“同名镜像权限”

---

## 常见问题（FAQ）

### Q1：为什么前端看不到 role_ids？

* Hook 未绑定或用户未 refresh session。
* 解决：

  1. Dashboard 绑定 hook
  2. 用户端执行 `supabase.auth.refreshSession()`

### Q2：为什么 server.ts 用 pg 而不是 supabase-js？

因为你最佳实践要求 **rbac schema 不暴露**，而 supabase-js（PostgREST）访问不了隐藏 schema。pg 直连是最可靠路径。

### Q3：我开启了 hasPermission，但 RPC 报错怎么办？

* 库会优雅降级：permissionNames 置空，仍可用 hasRole。
* 你应检查：

  * permission-name RPC 是否已部署
  * rpcName 是否匹配
  * GRANT EXECUTE 是否给 authenticated

---

## 安全边界与威胁模型

* ✅ DB/RLS：安全权威
* ⚠️ Client gating：只做体验
* ✅ RBAC schema 不暴露：降低枚举风险
* ⚠️ 可选 RPC：增加暴露面，但控制在“仅返回自己权限”范围内
* ⚠️ 不要给不受信任用户直连 SQL 或“SQL 执行器”类 RPC；`has_perm` 使用事务级缓存（`set_config`/`current_setting`），需在受控 SQL 环境使用

---

## 版本与兼容性

* Node：建议 18+
* React：>=16.8
* supabase-js：^2.x
* PostgreSQL：Supabase 托管 PG（带 pgcrypto）

---