Supabase와 스키마 설계: 파티셔닝 및 정규화
Source: Dev.to
Day 6 – Schema Design with Supabase
PostgreSQL은 스키마를 지원하는데, 이는 테이블을 그룹화하기 위한 네임스페이스 역할을 합니다.
주요 사용 사례는 다음과 같습니다:
- 멀티 테넌시 – 고객마다 별도 스키마를 두어 완전한 데이터 격리를 구현.
- 접근 제어 – 스키마 단위로 권한을 설정.
- 확장 격리 –
pgvector와 같은 확장을 전용 스키마에 보관.
많은 프로젝트에서는 모든 테이블이 기본 public 스키마에 존재합니다:
-- Default is public schema
SELECT * FROM public.users;
-- Create a separate schema
CREATE SCHEMA app_auth;
SELECT * FROM app_auth.users;
단일 public 스키마의 문제점
- 테이블 수가 늘어날수록 가시성이 떨어짐.
- 어떤 테이블이 어느 기능에 속하는지 파악하기 어려워짐.
- 권한 관리가 복잡해짐.
이를 해결하기 위해 기능별로 스키마를 나누고 app_ 접두사를 추가했습니다.
The app_ Prefix
Supabase는 auth, storage, public 같은 시스템 스키마를 예약해 둡니다.
사용자 정의 스키마에 app_(application의 약자) 접두사를 붙이면:
- Supabase 시스템 스키마와 명확히 구분됨.
- pgAdmin 등 도구에서 알파벳 순으로 먼저 나타나 쉽게 찾을 수 있음.
Supabase 시스템 스키마
├── auth # Supabase authentication
├── storage # Supabase storage
├── public # Default
Application schemas
├── app_auth # Custom authentication
├── app_billing # Billing
├── app_content # Content management
├── app_admin # Admin functions
├── app_ai # AI features
├── app_social # Social features
├── app_system # System logs, etc.
Supabase의 시스템 스키마는 전혀 사용하지 않고 모든 것을 커스텀 스키마에 두어 벤더 락인 위험을 줄입니다. 작은 서비스에서는 스키마가 많아지는 오버헤드가 크게 느껴지지 않을 수도 있지만, SaaS가 성장함에 따라 큰 도움이 됩니다.
Current Schema Overview
| Schema | Responsibility | Example Tables |
|---|---|---|
app_admin | Admin functions | tenants, teams, members |
app_ai | AI features | embeddings, search_vectors |
app_auth | Authentication | users, sessions, accounts |
app_billing | Billing | subscriptions, payment_history |
app_content | Content management | contents, pages, tables |
app_social | Social features | bookmarks, comments, reactions |
app_system | System logs | activity_logs, system_logs |
Partitioning criteria
- Feature cohesion – 관련 테이블을 함께 배치 (
app_auth등). - Change frequency – 자주 변경되는 테이블을 격리.
- Permission boundaries – 관리자 전용 데이터(
app_admin)와 일반 사용자 데이터(app_content)를 분리.
Defining Schemas with Drizzle ORM
// database/app_auth/schema.ts
import { pgSchema } from 'drizzle-orm/pg-core';
export const appAuth = pgSchema('app_auth');
// database/app_auth/users.ts
import { appAuth } from './schema';
import { text, timestamp, uuid } from 'drizzle-orm/pg-core';
export const users = appAuth.table('users', {
id: uuid('id').primaryKey().defaultRandom(),
name: text('name').notNull(),
email: text('email').notNull(),
createdAt: timestamp('created_at').defaultNow(),
updatedAt: timestamp('updated_at').defaultNow(),
});
Directory layout matching schemas
src/database/
├── app_auth/
│ ├── schema.ts # Schema definition
│ ├── users.ts # Table definition
│ ├── sessions.ts
│ └── index.ts # Exports
├── app_billing/
│ ├── schema.ts
│ ├── subscriptions.ts
│ └── index.ts
├── index.ts # Aggregate exports
└── relations.ts # Relation definitions
Normalization & Denormalization
인디 프로젝트라도 기본 정규화(1NF‑3NF)를 적용하면 좋습니다:
- 1NF – 반복 그룹을 없애고 CSV 필드 대신 조인 테이블 사용.
- 2NF – 부분 종속성을 제거; 복합 키의 일부에만 의존하는 컬럼을 분리.
- 3NF – 이행 종속성을 제거; 파생값은 필요할 때만 저장.
Practical tips
- Junction tables – 다대다 관계를 처리 (
content_tags등). - Composite primary keys – 멀티 테넌트 테이블에
(tenant_id, id)사용. - Denormalization – 읽기 위주 쿼리를 위해 집계 카운트(예: 북마크 수) 저장.
Row‑Level Security (RLS)
RLS는 행 단위 접근 제어를 제공합니다. 현재는 애플리케이션 레이어에서 권한을 검증하고 있지만, 스키마가 파티셔닝돼 있으면 각 스키마마다 별도 정책을 적용하기 쉬워져 향후 RLS 적용이 간편해집니다.
Using Custom Schemas with Supabase
커스텀 스키마를 추가하면 다음 3가지를 설정해야 합니다. 그렇지 않으면 “table not found” 오류가 발생합니다.
1. Expose schemas in the Supabase dashboard
Project Settings → Data API → Exposed schemas → 스키마를 추가 (예: public, app_admin, app_ai, …).
2. Include schemas in DATABASE_URL
# .env.local
DATABASE_URL=postgresql://user:password@host:5432/postgres?schema=public,app_admin,app_ai,app_auth,app_billing,app_content,app_social,app_system
3. Configure Drizzle ORM’s schemaFilter
// drizzle.config.ts
import type { Config } from 'drizzle-orm';
export default {
schema: [
'./src/database/app_admin/index.ts',
'./src/database/app_auth/index.ts',
'./src/database/app_billing/index.ts',
// …
],
dialect: 'postgresql',
dbCredentials: {
url: process.env.DATABASE_URL!,
},
// Multiple schema support
schemaFilter: [
'public',
'app_admin',
'app_ai',
'app_auth',
'app_billing',
'app_content',
'app_social',
'app_system',
],
} satisfies Config;
Takeaways
What works well
- 기능 기반 스키마를 통한 명확한 책임 분리.
- 디렉터리 구조가 스키마 이름과 일치해 가독성 향상.
- 기본 정규화와 선택적 비정규화를 통한 성능 최적화.
Cautions
- 스키마가 너무 많으면 복잡도가 증가할 수 있음.
- Supabase 설정에서 커스텀 스키마를 반드시 노출할 것.
- 작은 프로젝트라도 향후 확장을 고려한 스키마 설계가 도움이 됨.
What’s next
내일 포스트에서는 Database ID Design – UUID, CUID2, 순차 ID 등 선택에 대해 다룰 예정입니다.
Related posts in this series
- 12/5 – Git Branch Strategy: A Practical Workflow for Indie Development
- 12/7 – Database ID Design: Choosing Between UUID, CUID2, Sequential IDs, and More