데이터베이스에 접근 제어를 애플리케이션 레이어에만 의존하면, API 버그 하나로 모든 사용자 데이터가 노출될 수 있다. Row Level Security(RLS) 는 PostgreSQL이 제공하는 행 단위 접근 제어 기능으로, 데이터베이스 자체에서 "어떤 사용자가 어떤 행을 읽고 쓸 수 있는가"를 강제한다. Supabase는 이 RLS를 기본 보안 레이어로 채택하고 있다.
애플리케이션 서버가 없는 Supabase 아키텍처를 생각해보자.
클라이언트
│
▼
Supabase API (PostgREST)
│
▼
PostgreSQL
├── Table: posts
├── Table: comments
└── Table: profiles
클라이언트가 PostgREST를 통해 직접 데이터베이스에 쿼리를 날리는 구조다. 서버 미들웨어가 없으므로 권한 검사는 데이터베이스 안에서 이루어져야 한다. RLS가 비활성화된 테이블은 anon 키만 있으면 전체 행을 읽을 수 있다.
PostgREST — PostgreSQL을 REST API로 자동 노출하는 서버. Supabase가 내부적으로 사용한다.
-- RLS 활성화
ALTER TABLE posts ENABLE ROW LEVEL SECURITY;
-- 기본적으로 모든 접근 차단됨 (Policy 없으면 아무도 못 읽음)
RLS를 활성화하면 Policy가 없는 한 모든 행이 차단된다. 테이블 소유자(superuser)는 예외다.
Policy는 USING 절(읽기/수정 대상 필터)과 WITH CHECK 절(쓰기 허용 조건)로 구성된다.
-- 본인 게시글만 SELECT 허용
CREATE POLICY "select_own_posts"
ON posts
FOR SELECT
USING (auth.uid() = user_id);
-- 본인만 INSERT 허용
CREATE POLICY "insert_own_posts"
ON posts
FOR INSERT
WITH CHECK (auth.uid() = user_id);
-- 본인 게시글만 UPDATE 허용
CREATE POLICY "update_own_posts"
ON posts
FOR UPDATE
USING (auth.uid() = user_id)
WITH CHECK (auth.uid() = user_id);
-- 본인 게시글만 DELETE 허용
CREATE POLICY "delete_own_posts"
ON posts
FOR DELETE
USING (auth.uid() = user_id);
auth.uid — Supabase가 JWT 토큰에서 추출한 현재 사용자의 UUID를 반환하는 함수.
| 명령 | USING | WITH CHECK |
|---|---|---|
| SELECT | 반환할 행 필터 | 사용 안 함 |
| INSERT | 사용 안 함 | 삽입 허용 조건 |
| UPDATE | 수정 대상 행 필터 | 수정 후 값 조건 |
| DELETE | 삭제 대상 행 필터 | 사용 안 함 |
-- 공개된 게시글은 누구나 읽기 가능
CREATE POLICY "public_read_posts"
ON posts
FOR SELECT
USING (is_published = true OR auth.uid() = user_id);
-- 작성자만 수정/삭제
CREATE POLICY "author_modify_posts"
ON posts
FOR ALL
USING (auth.uid() = user_id)
WITH CHECK (auth.uid() = user_id);
FOR ALL은 INSERT/SELECT/UPDATE/DELETE 모두에 적용된다. 단, INSERT에는 USING이 무시되므로 WITH CHECK를 반드시 명시해야 한다.
const { data, error } = await supabase
.from('posts')
.select('*');
// RLS Policy를 통과한 행만 반환됨
서버 측에서 service_role 키를 사용하면 RLS를 우회한다. 클라이언트에 service_role 키를 절대 노출하지 말 것.
RLS는 애플리케이션 버그가 있어도 데이터베이스 레벨에서 권한을 강제하는 마지막 방어선이다. Policy를 테이블별, 작업별로 명확히 분리해두면 권한 로직을 데이터베이스 한 곳에서 관리할 수 있다.