---
name: generate-rls
description: >
  Supabase JWT 기반 Row Level Security(RLS) 시스템 구현 스킬.
  테넌트(회사/조직/팀) 구조에서 관리자가 구성원에게 리소스(게시판/메뉴/기능) 접근 권한을 개별 부여하고,
  Auth Hook으로 JWT에 주입하여 DB 레벨까지 자동 강제하는 시스템.
  --backend=supabase(네이티브 RLS), --backend=fastapi, --backend=nestjs 선택 가능.
  Trigger when: RLS 구현, 권한 관리 시스템, JWT 기반 접근 제어, Supabase Auth Hook,
  사용자별 리소스 권한, row level security, multi-tenant 권한, 관리자가 구성원 권한 부여,
  역할 기반 접근 제어, RBAC, 사용자별 접근 제어.
version: 1.0.0
---

# Generate RLS — JWT 기반 사용자별 권한 관리 시스템

관리자(admin/master)가 구성원(member/employee)에게 리소스(게시판/메뉴/기능) 접근 권한을 개별 부여하고,
Auth Hook으로 JWT에 주입하여 DB 레벨까지 자동 강제하는 시스템을 구현합니다.

> **이름은 비즈니스에 맞게 자유롭게 명명하세요.**
> 이 스킬은 구조와 패턴을 제공합니다. 테이블명·컬럼명·Enum 값은 도메인에 맞게 변경하세요.

---

## 사용법

```
/generate-rls                        # Supabase 네이티브 RLS (기본값)
/generate-rls --backend=supabase     # Supabase 네이티브 RLS
/generate-rls --backend=fastapi      # FastAPI 백엔드 + Supabase Auth/DB
/generate-rls --backend=nestjs       # NestJS 백엔드 + Supabase Auth/DB
```

---

## 도메인 맞춤화 예시

실제 비즈니스에 맞게 이름을 변경하세요:

| 스킬 내 기본 이름 | 커머스 | SaaS | 병원 | 학교 |
|---|---|---|---|---|
| `companies` | `stores` | `workspaces` | `hospitals` | `schools` |
| `master` | `owner` | `admin` | `doctor` | `teacher` |
| `employee` | `staff` | `member` | `nurse` | `student` |
| `boards` | `menus` | `features` | `wards` | `subjects` |
| `board_slug` | `menu_key` | `feature_key` | `ward_code` | `subject_code` |
| `company_type` | `store_type` | `plan_type` | `dept_type` | `school_type` |
| `can_read` | `can_view` | `can_access` | `can_view` | `can_enroll` |
| `can_write` | `can_edit` | `can_manage` | `can_update` | `can_submit` |

---

## 핵심 아키텍처

```
구성원 로그인
  └─▶ Supabase Auth Hook 실행
        └─▶ user_resource_permissions 테이블 조회
              └─▶ JWT app_metadata에 permissions 주입
                    └─▶ 이후 모든 요청: JWT만으로 권한 판단
                          ├─ 서버 컴포넌트 / API Route
                          ├─ Supabase RLS 정책 (DB 레벨)
                          └─ 사이드바 / UI 렌더링
```

JWT payload 결과 (이름은 비즈니스에 맞게 변경):
```json
{
  "app_metadata": {
    "tenant_type": "your_tenant_type",
    "tenant_id": "uuid",
    "role": "member",
    "permissions": [
      { "resource_key": "dashboard", "can_read": true,  "can_write": true  },
      { "resource_key": "reports",   "can_read": true,  "can_write": false }
    ]
  }
}
```

---

## 구현 순서

### Step 1 — 테이블 설계

**필수 테이블 패턴 (이름은 자유롭게):**

```sql
-- 1. 테넌트/조직 테이블 (companies → tenants, organizations, stores 등)
CREATE TABLE tenants (
  id          uuid PRIMARY KEY DEFAULT gen_random_uuid(),
  name        text NOT NULL,
  tenant_type text NOT NULL,  -- Enum 또는 text: 비즈니스 유형 분류
  created_at  timestamptz DEFAULT now()
);

-- 2. 사용자 프로필 (auth.users와 1:1)
CREATE TABLE profiles (
  id        uuid PRIMARY KEY REFERENCES auth.users(id) ON DELETE CASCADE,
  tenant_id uuid REFERENCES tenants(id) ON DELETE SET NULL,
  name      text NOT NULL,
  email     text NOT NULL,
  role      text NOT NULL DEFAULT 'member',  -- 'admin' | 'member' (또는 Enum)
  created_at timestamptz DEFAULT now()
);

-- 3. 리소스 테이블 (boards → menus, features, pages, modules 등)
CREATE TABLE resources (
  id          uuid PRIMARY KEY DEFAULT gen_random_uuid(),
  slug        text UNIQUE NOT NULL,   -- JWT 권한 배열의 키
  name        text NOT NULL,
  tenant_type text NOT NULL,          -- 어느 테넌트 유형의 리소스인지
  created_at  timestamptz DEFAULT now()
);

-- 4. 사용자별 리소스 권한 (핵심 설정 테이블)
CREATE TABLE user_resource_permissions (
  id         uuid PRIMARY KEY DEFAULT gen_random_uuid(),
  user_id    uuid NOT NULL REFERENCES profiles(id) ON DELETE CASCADE,
  resource_id uuid NOT NULL REFERENCES resources(id) ON DELETE CASCADE,
  can_read   boolean NOT NULL DEFAULT true,
  can_write  boolean NOT NULL DEFAULT false,
  granted_by uuid NOT NULL REFERENCES profiles(id),  -- 권한 부여한 관리자
  UNIQUE(user_id, resource_id)
);

-- 5. 데이터 테이블 (RLS 적용 대상)
CREATE TABLE posts (
  id          uuid PRIMARY KEY DEFAULT gen_random_uuid(),
  resource_id uuid NOT NULL REFERENCES resources(id) ON DELETE CASCADE,
  author_id   uuid REFERENCES profiles(id) ON DELETE SET NULL,
  title       text NOT NULL,
  content     text NOT NULL,
  created_at  timestamptz DEFAULT now()
);
```

상세 RLS 정책 → `references/schema.md`

**필수 보안 원칙:**
- 회원가입 시 `role`은 서버에서 `'member'`로 강제 (클라이언트 입력 금지)
- `profiles_update` RLS에서 `role`, `tenant_id` 변경 불가
- 권한 INSERT RLS에서 **동일 테넌트 구성원 + 동일 테넌트 리소스** 검증 필수

### Step 2 — Auth Hook 구현

```sql
CREATE OR REPLACE FUNCTION public.custom_access_token_hook(event jsonb)
RETURNS jsonb LANGUAGE plpgsql SECURITY DEFINER SET search_path = public AS $$
DECLARE
  v_user_id   uuid;
  v_profile   profiles%ROWTYPE;
  v_tenant    tenants%ROWTYPE;
  v_perms     jsonb;
BEGIN
  v_user_id := (event->>'user_id')::uuid;

  SELECT * INTO v_profile FROM profiles WHERE id = v_user_id;
  IF v_profile.id IS NULL THEN RETURN event; END IF;  -- 가입 직후 race condition

  SELECT * INTO v_tenant FROM tenants WHERE id = v_profile.tenant_id;
  IF v_tenant.id IS NULL THEN RETURN event; END IF;

  IF v_profile.role = 'admin' THEN
    -- 관리자: 소속 테넌트 전체 리소스, 모두 read+write
    SELECT jsonb_agg(jsonb_build_object(
      'resource_key', r.slug, 'can_read', true, 'can_write', true
    )) INTO v_perms
    FROM resources r WHERE r.tenant_type = v_tenant.tenant_type;
  ELSE
    -- 구성원: 명시적으로 부여된 리소스만
    SELECT jsonb_agg(jsonb_build_object(
      'resource_key', r.slug, 'can_read', p.can_read, 'can_write', p.can_write
    )) INTO v_perms
    FROM user_resource_permissions p
    JOIN resources r ON r.id = p.resource_id
    WHERE p.user_id = v_user_id;
  END IF;

  RETURN jsonb_set(event, '{claims,app_metadata}',
    COALESCE(event->'claims'->'app_metadata', '{}'::jsonb) || jsonb_build_object(
      'tenant_type', v_tenant.tenant_type,
      'tenant_id',   v_tenant.id::text,
      'role',        v_profile.role,
      'permissions', COALESCE(v_perms, '[]'::jsonb)
    )
  );
END;
$$;

GRANT EXECUTE ON FUNCTION public.custom_access_token_hook TO supabase_auth_admin;
REVOKE EXECUTE ON FUNCTION public.custom_access_token_hook FROM authenticated, anon, public;
```

Supabase 대시보드 → Authentication → Hooks → Custom Access Token 연결 필수.

상세 구현 → `references/auth-hook.md`

### Step 3 — RLS 정책

JWT에서 직접 권한 판단 — 런타임 DB 쿼리 없음:

```sql
-- 데이터 SELECT: JWT permissions 배열에서 can_read 확인
CREATE POLICY "data_select" ON posts FOR SELECT TO authenticated
USING (
  EXISTS (
    SELECT 1 FROM jsonb_array_elements(
      COALESCE(auth.jwt()->'app_metadata'->'permissions', '[]'::jsonb)
    ) AS perm
    WHERE perm->>'resource_key' = (SELECT slug FROM resources WHERE id = posts.resource_id)
      AND (perm->>'can_read')::boolean = true
  )
);

-- 권한 부여: 동일 테넌트 구성원 + 동일 테넌트 리소스 검증 필수
CREATE POLICY "permissions_insert_admin" ON user_resource_permissions
FOR INSERT TO authenticated
WITH CHECK (
  auth.jwt()->'app_metadata'->>'role' = 'admin'
  AND granted_by = auth.uid()
  -- 대상 구성원이 같은 테넌트인지
  AND EXISTS (
    SELECT 1 FROM profiles p WHERE p.id = user_id
      AND p.tenant_id::text = (auth.jwt()->'app_metadata'->>'tenant_id')
  )
  -- 리소스가 같은 테넌트 유형인지
  AND EXISTS (
    SELECT 1 FROM resources r WHERE r.id = resource_id
      AND r.tenant_type = (auth.jwt()->'app_metadata'->>'tenant_type')
  )
);
```

전체 RLS 정책 → `references/schema.md`

### Step 4 — 프론트엔드 JWT 디코딩

```typescript
// ✅ getUser() 먼저(서버 검증), 이후 getSession()으로 JWT 디코딩
export async function getJwtAppMetadata() {
  const supabase = await createClient();
  const { data: { user } } = await supabase.auth.getUser();
  if (!user) return null;
  const { data: { session } } = await supabase.auth.getSession();
  if (!session?.access_token) return null;
  const [, b64] = session.access_token.split('.');
  return JSON.parse(Buffer.from(b64, 'base64').toString()).app_metadata;
}
```

백엔드별 상세 → `references/frontend.md`, `references/fastapi.md`, `references/nestjs.md`

---

## 백엔드별 선택 가이드

| | `--backend=supabase` | `--backend=fastapi` | `--backend=nestjs` |
|--|--|--|--|
| Auth | Supabase Auth | Supabase Auth | Supabase Auth |
| 권한 강제 | RLS 정책 자동 | FastAPI Depends | NestJS Guard |
| JWT 검증 | 자동 | `PyJWT` HS256 | `passport-jwt` |
| 상세 참조 | `references/schema.md` | `references/fastapi.md` | `references/nestjs.md` |

---

## 보안 체크리스트

- [ ] 회원가입 API에서 `role: 'member'` 하드코딩 (클라이언트 값 무시)
- [ ] `profiles_update` RLS에서 `role`, `tenant_id` 변경 방지
- [ ] 권한 INSERT RLS에서 동일 테넌트 구성원 + 동일 테넌트 리소스 검증
- [ ] Auth Hook `SECURITY DEFINER` + `supabase_auth_admin`만 GRANT
- [ ] API에서 `getUser()` → `getSession()` 순서 유지 (서버 검증 후 디코딩)
- [ ] HTTPS 적용, XSS 방어 (쿠키 httpOnly)
- [ ] 관리자 계정은 DB 직접 또는 초대 시스템으로만 생성

---

## 참조 문서

- `references/schema.md` — 전체 SQL 스키마 + RLS 정책
- `references/auth-hook.md` — Auth Hook 상세 구현 및 보안
- `references/frontend.md` — Next.js / React 프론트엔드 패턴
- `references/fastapi.md` — FastAPI 백엔드 JWT 검증 및 Guard
- `references/nestjs.md` — NestJS Strategy + Guard 구현
