Multi-Tenant Next.js와 Supabase RLS: 프로덕션 가이드
Supabase RLS가 멀티 테넌시에 적합한 이유
솔직히 말해서, SaaS 앱에서 멀티 테넌시를 처리할 때는 선택지가 많습니다. 테넌트별로 별도의 데이터베이스를 구축할 수 있는데, 이는 조직적 낙원처럼 들리지만 비싸고 관리하기 정말 고통스럽습니다. 또는 별도의 스키마를 사용해볼 수 있는데, 운영 측면에서는 덜 번거롭지만 마이그레이션할 때는 여전히 힘듭니다. 하지만 SaaS 세계의 사랑받는 방식이 하나 있습니다—행 기반 필터링을 포함한 공유 테이블입니다. Supabase는 PostgreSQL 기반의 행 수준 보안(Row Level Security, RLS) 덕분에 이 접근 방식을 매우 간단하게 만듭니다.
그게 왜 중요할까요? 간단합니다. 데이터 필터링이 데이터베이스 수준에서 발생합니다. Next.js API 경로의 WHERE 절에서 실수를 하더라도, 데이터베이스 자체가 안전 장치이기 때문에 밤을 새며 데이터 유출을 걱정할 필요가 없습니다. 그리고 정말로, 요즘 시대에는 그것이 사치가 아니라 필수입니다.
그러나 현실을 직시해봅시다. RLS는 쿼리에 오버헤드를 추가하고, 디버깅을 복잡하게 하며, 마이그레이션 중에 당신을 곤경에 빠뜨릴 수 있습니다. 그렇다면 서로 다른 멀티 테넌시 접근 방식은 어떻게 비교될까요?
| 접근 방식 | 격리 수준 | 비용 | 운영 복잡도 | 쿼리 성능 |
|---|---|---|---|---|
| 테넌트별 데이터베이스 | 완전함 | 높음 ($50-200/테넌트/월) | 매우 높음 | 최고 |
| 테넌트별 스키마 | 강함 | 중간 | 높음 (마이그레이션) | 좋음 |
| 공유 테이블 + RLS | 행 수준 | 낮음 | 중간 | 좋음 (주의사항 있음) |
| 애플리케이션 수준 필터링 | 없음 | 가장 낮음 | 낮음 | 최고 |
10,000명 미만의 테넌트를 보유한 대부분의 SaaS 제품의 경우, RLS를 갖춘 공유 테이블이 최고의 성능을 제공합니다. 이것이 우리가 여기서 들어갈 주제입니다.

아키텍처 패턴: 공유 vs 격리
코드를 작성하기 전에도, 테넌트 해석 전략을 선택해야 합니다. 실제로는 주로 두 가지 형태를 만나게 됩니다.
서브도메인 기반 테넌시
tenant-slug.yourapp.com 같은 것을 본 적 있나요? B2B SaaS를 위한 가장 일반적인 패턴에 오신 것을 환영합니다. 세련되어 있고, 전문적이며, 미들웨어에서 테넌트 해석을 매우 간단하게 만듭니다.
경로 기반 테넌시
이것은 기본적인 /org/tenant-slug/dashboard 같은 것입니다. 와일드카드 DNS가 필요 없어서 설정하기 더 쉽고, Vercel 같은 플랫폼에서 커스텀 도메인 없이도 작동합니다. 하지만 솔직하게 말하면: 샌들과 양말을 함께 입는 것 같은 기분입니다. 우리는 보통 프로덕션 B2B 앱에는 서브도메인 기반을, 내부 도구나 MVP에는 경로 기반을 권장합니다. 나중에 전환하고 싶으신가요? 과거의 자신에게 저주를 퍼붓게 될 겁니다—이러한 패턴을 변경하는 것은 쉬운 일이 아닙니다.
테넌트 스키마 설정
여기 세 가지의 다른 프로덕션 배포에서 우리를 실망시키지 않은 스키마 패턴이 있습니다:
-- 핵심 테넌트 테이블
CREATE TABLE organizations (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
name TEXT NOT NULL,
slug TEXT UNIQUE NOT NULL,
plan TEXT NOT NULL DEFAULT 'free',
created_at TIMESTAMPTZ DEFAULT now(),
settings JSONB DEFAULT '{}'
);
-- 멤버십 교차 테이블
CREATE TABLE memberships (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID REFERENCES auth.users(id) ON DELETE CASCADE,
org_id UUID REFERENCES organizations(id) ON DELETE CASCADE,
role TEXT NOT NULL DEFAULT 'member' CHECK (role IN ('owner', 'admin', 'member', 'viewer')),
created_at TIMESTAMPTZ DEFAULT now(),
UNIQUE(user_id, org_id)
);
-- 예: 테넌트 범위의 테이블
CREATE TABLE projects (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
org_id UUID REFERENCES organizations(id) ON DELETE CASCADE NOT NULL,
name TEXT NOT NULL,
description TEXT,
created_by UUID REFERENCES auth.users(id),
created_at TIMESTAMPTZ DEFAULT now()
);
-- org_id 인덱스 — 모든 테넌트 범위의 테이블에 필요합니다
CREATE INDEX idx_projects_org_id ON projects(org_id);
CREATE INDEX idx_memberships_user_id ON memberships(user_id);
CREATE INDEX idx_memberships_org_id ON memberships(org_id);
memberships 테이블은 모든 것을 함께 붙여주는 접착제입니다. 모든 RLS 정책이 이 테이블을 좋아하는 사촌처럼 가리킬 것입니다. 사용자는 여러 조직에 가입할 수 있고, 그들의 역할이 할 수 있는 것과 없는 것을 결정합니다. 그리고 여기 한 가지 지혜의 조각이 있습니다: 항상—정말로, 항상—모든 테넌트 범위의 테이블에서 org_id를 인덱싱하세요. 그렇지 않으면, 데이터로 가득 찬 상태에서 쿼리가 시럽처럼 천천히 흘러가는 것을 지켜보게 됩니다. 우리는 클라이언트의 대시보드가 100,000개의 행으로 50ms에서 8초로 급락했을 때 이것으로 인해 놀랐습니다. 배운 교훈입니다.
실제로 확장되는 RLS 정책
여기가 튜토리얼들이 보통 물러나는 곳이고, 당신을 고립된 상태에서 남겨둡니다. 그들은 auth.uid() = user_id를 던져주고 "행운을 빕니다!"라고 말합니다. 하지만 멀티 테넌트 RLS는 그렇게 단순하게 축약될 수 없습니다.
헬퍼 함수 패턴
왜 모든 정책에 멤버십 체크를 어지럽히나요? 대신 헬퍼 함수를 사용하세요:
-- 헬퍼: 현재 사용자가 조직의 멤버인지 확인
CREATE OR REPLACE FUNCTION public.is_member_of(org UUID)
RETURNS BOOLEAN AS $$
SELECT EXISTS (
SELECT 1 FROM memberships
WHERE user_id = auth.uid()
AND org_id = org
);
$$ LANGUAGE sql SECURITY DEFINER STABLE;
-- 헬퍼: 조직에서 사용자의 역할을 가져오기
CREATE OR REPLACE FUNCTION public.get_role_in(org UUID)
RETURNS TEXT AS $$
SELECT role FROM memberships
WHERE user_id = auth.uid()
AND org_id = org
LIMIT 1;
$$ LANGUAGE sql SECURITY DEFINER STABLE;
왜 SECURITY DEFINER일까요? 함수가 생성자의 권한으로 실행되므로 memberships 테이블에서 RLS를 건너뜁니다. 이것 없이는, RLS가 다른 테이블이 의존하는 멤버십 체크에 충돌할 수 있는 순환 의존성 토끼굴에 빠질 위험이 있습니다.
그리고 STABLE 부분은? 함수의 출력이 단일 쿼리 동안 동일한 입력에 대해 일정하게 유지된다고 쿼리 플래너에 신호를 보내어 좋은 캐싱 이점을 가능하게 합니다. IMMUTABLE을 사용하고 싶으신가요? 안 됩니다. 멤버십은 트랜잭션 사이에 바뀔 수 있습니다.
테넌트 범위의 테이블을 위한 정책
projects 테이블의 일부 정책을 살펴봅시다:
-- RLS 활성화
ALTER TABLE projects ENABLE ROW LEVEL SECURITY;
-- SELECT: 멤버는 조직의 프로젝트를 볼 수 있음
CREATE POLICY "Members can view org projects"
ON projects FOR SELECT
USING (public.is_member_of(org_id));
-- INSERT: 관리자 및 소유자는 프로젝트를 만들 수 있음
CREATE POLICY "Admins can create projects"
ON projects FOR INSERT
WITH CHECK (
public.get_role_in(org_id) IN ('owner', 'admin')
);
-- UPDATE: 관리자 및 소유자는 프로젝트를 업데이트할 수 있음
CREATE POLICY "Admins can update projects"
ON projects FOR UPDATE
USING (public.is_member_of(org_id))
WITH CHECK (
public.get_role_in(org_id) IN ('owner', 'admin')
);
-- DELETE: 오직 소유자만 프로젝트를 삭제할 수 있음
CREATE POLICY "Owners can delete projects"
ON projects FOR DELETE
USING (
public.get_role_in(org_id) = 'owner'
);
멤버십 테이블 자체를 위한 정책
이것은 까다롭습니다. memberships 테이블은 자체 RLS를 가지지만, 헬퍼 함수를 사용할 수 없습니다. 왜냐하면 그들이 차례대로 memberships을 쿼리하기 때문입니다—순환 참조 악몽의 신호:
ALTER TABLE memberships ENABLE ROW LEVEL SECURITY;
-- 사용자는 자신이 속한 조직의 멤버십을 볼 수 있음
CREATE POLICY "Users can view org memberships"
ON memberships FOR SELECT
USING (
org_id IN (
SELECT org_id FROM memberships WHERE user_id = auth.uid()
)
);
-- 오직 소유자만 멤버를 추가할 수 있음
CREATE POLICY "Owners can add members"
ON memberships FOR INSERT
WITH CHECK (
org_id IN (
SELECT org_id FROM memberships
WHERE user_id = auth.uid() AND role = 'owner'
)
);
네, 같은 테이블의 하위 쿼리가 있습니다. 그리고 네, PostgreSQL이 이것을 잘 처리합니다. 하위 쿼리는 정의 중인 정책의 영향을 받지 않으므로 자신의 멤버십을 확인합니다(RLS는 외부 쿼리 주변에만 래핑되기 때문입니다). 하지만 이것을 테스트하세요—진지하게, 프로덕션에서 버그를 찾는 것을 원하지 않습니다.

테넌트 해석을 위한 Next.js 미들웨어
Next.js 15와 멋진 App Router를 사용하면, 엣지에서 실행되는 미들웨어는 테넌트 해석을 위한 완벽한 관리자입니다. 여기 서브도메인 기반 설정을 위한 우리의 믿을 수 있는 패턴이 있습니다:
// middleware.ts
import { createServerClient } from '@supabase/ssr';
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
const PUBLIC_ROUTES = ['/login', '/signup', '/invite'];
export async function middleware(request: NextRequest) {
const hostname = request.headers.get('host') || '';
const currentHost = hostname.split('.')[0];
// 메인 도메인 및 localhost에서 건너뛰기
const isMainDomain = currentHost === 'app' || currentHost === 'www' || currentHost === 'localhost:3000';
let response = NextResponse.next({
request: { headers: request.headers },
});
const supabase = createServerClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
{
cookies: {
getAll() {
return request.cookies.getAll();
},
setAll(cookiesToSet) {
cookiesToSet.forEach(({ name, value, options }) => {
request.cookies.set(name, value);
response.cookies.set(name, value, options);
});
},
},
}
);
const { data: { user } } = await supabase.auth.getUser();
if (!isMainDomain) {
response.headers.set('x-tenant-slug', currentHost);
if (!user && !PUBLIC_ROUTES.some(r => request.nextUrl.pathname.startsWith(r))) {
return NextResponse.redirect(new URL('/login', request.url));
}
}
return response;
}
export const config = {
matcher: ['/((?!_next/static|_next/image|favicon.ico|api/webhooks).*)'],
};
x-tenant-slug 헤더는 순금입니다. 이를 사용하여 Server Components와 API 경로에 어떤 테넌트를 다루고 있는지 알리세요. 우리와 Next.js 프로젝트에서 협력하고 있다면, 이것을 설정하는 것이 우리의 첫째 날 우선순위입니다.
멀티 테넌트 앱에서의 인증 흐름
Supabase Auth는 멀티 테넌시 게임에서 중립을 유지합니다. 사용자는 글로벌 영역에 존재합니다—테넌트 관계는 당신이 풀어야 할 퍼즐입니다. 여기 우리의 게임 계획입니다:
- 사용자가 가입합니다: auth 사용자를 만들고, 조직을 구축하며, 'owner' 역할이 있는 멤버십을 만듭니다.
- 사용자가 초대받습니다: 관리자가 대기 중인 초대장을 작성하고, 새로운 사용자가 초대 링크를 통해 가입하며, 지정된 역할이 있는 멤버십이 나타납니다.
- 사용자가 로그인합니다: 서브도메인에서 테넌트를 추출하고, 멤버십을 확인하며, 그들을 대시보드로 안내합니다.
// app/api/auth/signup/route.ts
import { createClient } from '@/lib/supabase/server';
import { NextResponse } from 'next/server';
export async function POST(request: Request) {
const { email, password, orgName, orgSlug } = await request.json();
const supabase = await createClient();
// 사용자 가입
const { data: authData, error: authError } = await supabase.auth.signUp({
email,
password,
});
if (authError) return NextResponse.json({ error: authError.message }, { status: 400 });
// 조직 생성을 위해 서비스 역할 클라이언트를 사용합니다 (RLS를 우회합니다)
const adminClient = createAdminClient();
const { data: org, error: orgError } = await adminClient
.from('organizations')
.insert({ name: orgName, slug: orgSlug })
.select()
.single();
if (orgError) return NextResponse.json({ error: orgError.message }, { status: 400 });
// 소유권 멤버십 생성
await adminClient
.from('memberships')
.insert({
user_id: authData.user!.id,
org_id: org.id,
role: 'owner',
});
return NextResponse.json({ org });
}
우리가 가입 중에 서비스 역할 클라이언트에 의존하는 것을 주목하세요. 사용자는 아직 어떤 멤버십도 없으므로, RLS는 조직 생성을 위해 그들을 도움이 되지 않게 만들 것입니다. 이것은 고전적인 부트스트래핑 문제 중 하나입니다—당신의 서비스 역할 키가 당신의 마법 지팡이가 될 것입니다.
그리고 나는 이것을 충분히 강조할 수 없습니다: 절대로, 절대로 당신의 서비스 역할 키를 클라이언트에 노출하지 마세요. 이것은 엄격하게 서버 측 코드를 위한 것입니다.
Server Components와 RLS: SSR 문제
Next.js 15의 Server Components는 서버 바운드이고, 보안 게임을 높입니다. 하지만 Supabase RLS를 사용할 때 한 가지 문제가 있습니다: RLS 정책이 누가 테이블에 있는지 알기 위해 사용자의 세션을 Supabase 클라이언트에 제공해야 합니다.
// lib/supabase/server.ts
import { createServerClient } from '@supabase/ssr';
import { cookies } from 'next/headers';
export async function createClient() {
const cookieStore = await cookies();
return createServerClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
{
cookies: {
getAll() {
return cookieStore.getAll();
},
setAll(cookiesToSet) {
try {
cookiesToSet.forEach(({ name, value, options }) =>
cookieStore.set(name, value, options)
);
} catch {
// 이것은 Server Components에서 실패할 수 있습니다 (읽기 전용)
// 미들웨어가 쿠키 새로고침을 처리합니다
}
},
},
}
);
}
// app/[orgSlug]/projects/page.tsx
import { createClient } from '@/lib/supabase/server';
import { headers } from 'next/headers';
export default async function ProjectsPage() {
const supabase = await createClient();
const headersList = await headers();
const tenantSlug = headersList.get('x-tenant-slug');
// slug에서 조직 ID를 가져옵니다
const { data: org } = await supabase
.from('organizations')
.select('id')
.eq('slug', tenantSlug)
.single();
if (!org) return <div>Organization not found</div>;
// RLS가 자동으로 필터링합니다 — 현재 사용자가 멤버십을 가진
// 프로젝트만 반환합니다
const { data: projects } = await supabase
.from('projects')
.select('*')
.eq('org_id', org.id)
.order('created_at', { ascending: false });
return (
<div>
{projects?.map(project => (
<ProjectCard key={project.id} project={project} />
))}
</div>
);
}
여기 그 비결이 있습니다: 누군가 요청에서 org_id를 조작하더라도, RLS는 굽혀지지 않을 것입니다. 사용자가 멤버가 아니면 프로젝트에 대한 접근을 차단합니다. 기술적으로, .eq('org_id', org.id)는 보안을 위해 중복입니다—RLS가 그것을 처리합니다—하지만 성능과 가독성을 위해 좋습니다.
성능 최적화 및 일반적인 함정
N+1 RLS 쿼리 문제
모든 RLS 정책 체크는 하위 쿼리를 회전시킵니다. 100개의 행을 보고 있을 때 10-row 정책 체크에 연결하면 100번의 멤버십 조회 라운드를 의미합니다. 다행히 PostgreSQL은 충분히 영리해서 캐시합니다—하지만 오버헤드가 있습니다.
수정: 헬퍼 함수에서 STABLE을 사용하세요 (우리가 위에서 설명한 대로). 또한 org_id를 JWT 클레임으로 역정규화하는 것을 생각해보세요:
-- 커스텀 JWT 훅 (Supabase Dashboard > Auth > Hooks)
CREATE OR REPLACE FUNCTION public.custom_access_token_hook(event JSONB)
RETURNS JSONB AS $$
DECLARE
org_ids UUID[];
BEGIN
SELECT array_agg(org_id) INTO org_ids
FROM memberships
WHERE user_id = (event->>'user_id')::UUID;
event := jsonb_set(
event,
'{claims,org_ids}',
to_jsonb(org_ids)
);
RETURN event;
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;
그러면 당신의 RLS 정책은 다음과 같이 됩니다:
CREATE POLICY "Members can view"
ON projects FOR SELECT
USING (
org_id = ANY(
(SELECT array(SELECT jsonb_array_elements_text(
auth.jwt()->'org_ids'
))::UUID[])
)
);
이것은 멤버십 테이블 조회를 완전히 제거합니다. org ID는 JWT에서 직접 옵니다. 주의: JWT 클레임은 로그인 시에 스탬프됩니다. 누군가의 멤버십을 변경하면, 클레임을 동기화하려면 다시 인증해야 합니다. 일반적으로, 그것은 완전히 관리할 수 있습니다—단지 문서에 그것을 유지하세요.
연결 풀링
Supabase는 PgBouncer를 통해 연결 풀링을 제공합니다. Vercel에서 Next.js와 함께 실시간으로 가려면, 기억하세요: API 경로 및 Server Components에 대해 풀러 URL을 사용하세요.
# 일반 운영을 위해 (풀됨)
DATABASE_URL=postgres://user:pass@db.project.supabase.co:6543/postgres
# 마이그레이션 전용 (직접)
DIRECT_URL=postgres://user:pass@db.project.supabase.co:5432/postgres
Supabase의 Pro에서 월 $25를 내는 모든 사람은 풀러를 통해 200개의 동시 연결을 얻습니다. 동시에 1000명 미만의 사용자가 있는 대부분의 SaaS 앱의 경우, 그것은 충분 이상입니다.
당신이 절대 필요한 인덱스
여기 멀티 테넌트 설정에 대한 무차별 인덱스 세트가 있습니다:
-- 모든 테넌트 범위의 테이블에
CREATE INDEX idx_{table}_org_id ON {table}(org_id);
-- 일반적인 쿼리를 위한 복합 인덱스
CREATE INDEX idx_projects_org_created ON projects(org_id, created_at DESC);
-- 멤버십 — RLS에 의해 무겁게 쿼리됨
CREATE INDEX idx_memberships_user_org ON memberships(user_id, org_id);
CREATE INDEX idx_memberships_org_role ON memberships(org_id, role);
EXPLAIN ANALYZE—개발자의 최고의 친구. RLS를 탄 상태에서 쿼리가 어떻게 수행되는지 보세요. 플래너가 올바른 인덱스 없이 무엇을 하기로 결정하는지에 대해 무례한 깨달음을 얻을 수 있습니다.
RLS 정책 테스트
모두가 이것을 건너뛰지만, 데이터 유출에 대한 최고의 안전 장치입니다. 우리는 SQL에서 직접 RLS 정책을 테스트합니다:
-- 특정 사용자로서 테스트
SET request.jwt.claims = '{"sub": "user-uuid-here", "role": "authenticated"}';
SET role = 'authenticated';
-- 이것은 사용자가 접근할 수 있는 프로젝트만 반환해야 합니다
SELECT * FROM projects;
-- 이것은 실패해야 합니다 (사용자가 이 조직의 멤버가 아님)
INSERT INTO projects (org_id, name) VALUES ('other-org-uuid', 'Sneaky Project');
-- 재설정
RESET role;
그리고 중요한 정책을 위해 pgTAP를 잊지 마세요:
BEGIN;
SELECT plan(3);
-- 테스트 컨텍스트를 사용자 A로 설정 (조직 1의 멤버)
SET LOCAL request.jwt.claims = '{"sub": "user-a-uuid"}';
SET LOCAL role = 'authenticated';
SELECT is(
(SELECT count(*) FROM projects WHERE org_id = 'org-1-uuid')::INTEGER,
5,
'User A sees 5 projects in their org'
);
SELECT is(
(SELECT count(*) FROM projects WHERE org_id = 'org-2-uuid')::INTEGER,
0,
'User A sees 0 projects in other org'
);
SELECT throws_ok(
$$INSERT INTO projects (org_id, name) VALUES ('org-2-uuid', 'Hack')$$,
'new row violates row-level security policy',
'User A cannot insert into other org'
);
SELECT * FROM finish();
ROLLBACK;
CI에서 이것을 실행하세요. RLS 정책을 만지는 모든 마이그레이션은 전체 테스트 스위트를 활기차게 운동시켜야 합니다.
프로덕션 배포 체크리스트
배포할 준비가 되셨나요? 이것으로 자신을 안심시키세요:
- 모든 테넌트 데이터를 담는 테이블에서 RLS 활성화됨
- 서비스 역할 키가 서버 측에 저장됨, 클라이언트 근처 없음
-
org_id가 모든 테넌트 범위의 테이블에서 제대로 인덱싱됨 - 멤버십 헬퍼 함수가
SECURITY DEFINER와STABLE로 지정됨 - JWT 커스텀 클레임이 잠겨 로드됨 (JWT 경로에 있는 경우)
- 클라우드 배포를 위해 연결 풀링이 연결되어 있나?
- RLS 정책이 pgTAP 또는 그 kin으로 QA를 거쳤습니다
- RLS가 실행되는 중요 쿼리에 대해
EXPLAIN ANALYZE를 시도했습니다 - 초대/가입 흐름이 멤버십 부트스트랩을 놓치지 않습니다
- auth 엔드포인트에 속도 제한을 하고 있나요? Supabase는 기본 제공 옵션을 제공합니다
- Supabase Dashboard에서
auth스키마 테이블에 대해 RLS를 켰나요 (종종 지뢰밭) - 느린 쿼리에 대해 내장 모니터링을 켜놨나요 (Supabase Dashboard > Database > Query Performance)
멀티 테넌트 제품을 시작하고 이러한 물을 통과한 누군가를 원하나요? 우리의 헤드리스 CMS 개발 솔루션 또는 우리 연락처 페이지를 통한 빠른 대화가 정확히 필요한 것일 수 있습니다.
FAQ
수천 개의 테넌트가 있는 앱에 Supabase RLS를 사용할 수 있나요?
절대적으로. 우리는 5,000명 이상의 테넌트와 수백만 개의 행을 가진 공유 테이블 RLS를 시험했고 숨을 쉬지 못했습니다. 비결? org_id 열에 적절한 인덱싱과 STABLE 헬퍼 함수. 50,000명 이상의 테넌트나 십억 행 호화를 고려하고 있나요? org_id로 테이블을 분할하거나 테넌트별 스키마 설정과 향수에 빠지세요.
사용자가 여러 조직에 속할 때 테넌트 전환을 어떻게 처리하나요?
활성 조직을 쿠키나 URL (서브도메인)에 유지하세요. 조직을 바꾸나요? 서브도메인/쿠키를 조정하고 새로 가져오세요. 활성 조직을 JWT에 튼튼하게 하지 마세요—변경을 위해 다시 로그인을 요구합니다. 당신의 미들웨어가 엿볼 수 있는 쿠키가 가는 방향입니다.
RLS가 없는 테이블에서 활성화하지 않으면 어떻게 되나요?
모든 인증된 사용자는 모든 행을 탭할 수 있습니다. 그것이 PostgreSQL의 기본 입장입니다—RLS가 없는 테이블의 행 수준 제약이 없습니다. Supabase Dashboard는 RLS가 없는 테이블에 플래그를 지정하지만, 이것을 CI에 내장하면 쿼리를 pg_tables와 pg_policies에 도움이 됩니다.
관리 업무를 위해 Supabase의 서비스 역할 키를 사용해야 하나요, 아니면 커스텀 PostgreSQL 역할을 요리해야 하나요?
대부분의 경우, 서비스 역할 키로 충분합니다. 이것은 RLS를 완전히 우회하므로 서버 측 사용만을 위한 최고의 비결입니다. 세분화된 거버넌스가 필요한가요 (예: 모든 조직에서 "admin" 역할을 숨기되 삭제를 배제)? 그것은 커스텀 PostgreSQL 영토입니다—고급이고 일반적으로 복잡한 내부 도구가 요구할 때까지 당신의 레이더 밖입니다.
RLS 정책을 건드리지 않고 데이터베이스 마이그레이션을 어떻게 실행하나요?
Supabase CLI (supabase db push 또는 supabase migration)와 함께 직접 데이터베이스 URL (풀된 것을 건너뜀)이 당신의 등을 가집니다. RLS 정책 편집을 스키마 조정과 동일한 마이그레이션으로 튼튼하게 하세요. 스테이징 프로젝트에 대해 마이그레이션을 테스트 전환해보세요—Supabase는 Pro에서 정확히 이런 목적을 위해 미리보기 분기를 스핀할 수 있게 합니다.
RLS 정책이 다른 API나 서비스의 데이터에 도달할 수 있나요?
아니오. RLS 정책은 SQL에 편안하게 앉아 PostgreSQL로 평가됩니다. 외부 데이터를 확인하고 싶으신가요 (예: 기능 플래그 서비스)? 해당 데이터를 데이터베이스 테이블에 시멘트하세요, 그러면 정책에서 참조하세요. 전형적인 패턴은 Stripe에서 구독 상태를 동기화하여 organizations.plan 열에 저장하는 것입니다.
RLS와 비교한 애플리케이션 계층 필터링의 성능 세금은 무엇입니까?
우리의 Supabase Pro 벤치마크 (2 vCPU, 8GB RAM)에서, RLS는 올바른 인덱스를 가진 기본 멤버십 체크 정책에 대한 쿼리당 추가 1-3ms를 칠합니다. 정책 복잡도나 조인과 함께 광란에 빠지면 5-15ms를 추가할 수 있습니다. JWT 클레임 전술 (토큰의 org_ids 저장)은 하위 쿼리 춤이 없으므로 1ms 아래로 감싸세요. 일반적인 웹 앱의 경우, 그 레이턴시의 물방울은 무시할 수 있습니다.
Supabase Realtime 구독에서 이것이 어떻게 작동하나요?
Supabase Realtime은 RLS 규칙북을 따릅니다. 테이블 변경을 튜닝하면 RLS에 따라 보기에 적격인 행의 이벤트만 잡습니다. 이것은 추가 조정 없이 상자 밖에서 배포합니다. 단지 당신의 클라이언트 측 Supabase가 사용자 세션을 가지고 있는지 확인하세요, @supabase/ssr이 매끄럽게 처리합니다.