Schema Overview¶
Umbra 의 PostgreSQL schema 구조를 한눈에 정리한 navigation 문서. 9개 도메인 schema + Outbox 의 관계, 쓰기 주체, 참조 방향을 보여준다.
Why schema-per-domain¶
Umbra 는 도메인 경계를 DB 레벨에서도 강제하기 위해 각 도메인마다 별도 PostgreSQL schema 를 가진다 (ADR-0020).
- 도메인 경계 = DB 경계 —
billing.subscriptions같은 명시가 코드 리뷰에서 경계 위반을 드러냄 - sqlc 패키지 분리 —
db/queries/{domain}/과engine/{domain}/adapter/persistence/가 1:1 - Cross-schema FK 는 허용 — 참조 무결성 유지
- 권한 분리 여지 — 향후 PostgreSQL role 로 schema 단위 GRANT 가능
Schema catalog¶
| Schema | Domain type | Purpose | Tables |
|---|---|---|---|
identity | Supporting | Umbra User 계정, Discord OAuth2 세션 | users, sessions |
guild | Supporting | Discord 길드 메타데이터, 설정 | guilds, guild_configs |
member | Supporting | 길드 멤버, 웹 조인 | members, web_join_requests |
licensing | Core | Plan, License (권한) | plans, licenses |
billing | Core | 빌링키, Subscription, 결제 시도 | billing_keys, subscriptions, payment_attempts |
recovery | Core | 길드 상태, 스냅샷, 복구, AntiNuke | guild_roles, guild_channels, guild_permission_overrides, guild_settings, snapshots, restore_jobs, antinuke_incidents |
notification | Supporting | 알림 발송 요청, 사용자 선호도 | requests, preferences |
audit | Generic | 모든 이벤트의 영구 감사 로그 | events |
webhook | Generic | 외부 웹훅 수신 기록 | events |
events | Infrastructure | Outbox (이벤트 전달 매체) | outbox |
총 10개 schema, 23개 테이블.
Cross-schema relationships¶
erDiagram
IDENTITY_USERS ||--o{ GUILD_GUILDS : "owner"
IDENTITY_USERS ||--o{ MEMBER_MEMBERS : "linked to"
IDENTITY_USERS ||--o{ BILLING_BILLING_KEYS : owns
IDENTITY_USERS ||--o{ BILLING_SUBSCRIPTIONS : pays
IDENTITY_USERS ||--o{ AUDIT_EVENTS : "actor"
IDENTITY_USERS ||--|| NOTIFICATION_PREFERENCES : "prefers"
GUILD_GUILDS ||--|| GUILD_CONFIGS : config
GUILD_GUILDS ||--o{ MEMBER_MEMBERS : contains
GUILD_GUILDS ||--o{ LICENSING_LICENSES : licensed
GUILD_GUILDS ||--o{ BILLING_SUBSCRIPTIONS : subscribes
GUILD_GUILDS ||--o{ RECOVERY_SNAPSHOTS : "backed up"
GUILD_GUILDS ||--o{ RECOVERY_RESTORE_JOBS : restored
GUILD_GUILDS ||--o{ RECOVERY_ANTINUKE_INCIDENTS : "detected in"
GUILD_GUILDS ||--o{ AUDIT_EVENTS : "context"
GUILD_GUILDS ||--o{ NOTIFICATION_REQUESTS : "target"
LICENSING_PLANS ||--o{ LICENSING_LICENSES : "plan of"
LICENSING_PLANS ||--o{ BILLING_SUBSCRIPTIONS : "subscribed to"
LICENSING_LICENSES ||--o| BILLING_SUBSCRIPTIONS : "paid by"
BILLING_BILLING_KEYS ||--o{ BILLING_SUBSCRIPTIONS : "charged via"
BILLING_SUBSCRIPTIONS ||--o{ BILLING_PAYMENT_ATTEMPTS : has
RECOVERY_SNAPSHOTS ||--o{ RECOVERY_RESTORE_JOBS : "source"
RECOVERY_SNAPSHOTS ||--o| RECOVERY_ANTINUKE_INCIDENTS : "emergency"
Write ownership¶
"누가 이 테이블에 쓰는가" 를 명확히 하여 경계 위반을 방지:
| Schema.Table | Writer domain | Notes |
|---|---|---|
identity.users | Identity | 로그인, 탈퇴 |
identity.sessions | Identity | 세션 생성/폐기 |
guild.guilds | Guild | 봇 설치/강퇴, owner 변경 |
guild.guild_configs | Guild | 설정 변경 |
member.members | Member + Recovery (Sync) | Live Sync 가 cross-domain 쓰기 |
member.web_join_requests | Member | 웹 조인 플로우 |
licensing.plans | Licensing | Migration 에서만 (seed) |
licensing.licenses | Licensing | Billing 이벤트 반응 |
billing.billing_keys | Billing | 사용자 발급/삭제 |
billing.subscriptions | Billing | 구독 lifecycle |
billing.payment_attempts | Billing | 매 결제 시도 |
recovery.guild_* | Recovery (Sync) | Live Sync 배치 |
recovery.snapshots | Recovery (Snapshot) | 4가지 trigger |
recovery.restore_jobs | Recovery (Restore) | Workflow 상태 |
recovery.antinuke_incidents | Recovery (AntiNuke) | 감지 시 |
notification.requests | Notification | 이벤트 consumer |
notification.preferences | Notification | 사용자 설정 |
audit.events | Audit | 모든 이벤트 구독 |
webhook.events | Webhook | 외부 이벤트 수신 |
events.outbox | 모든 Core 도메인 | 이벤트 발행 |
Cross-domain write 예외: member.members 를 Recovery(Sync) 도 쓰는 이유는 Discord Gateway 이벤트를 Sync 가 받아 Member 도메인을 통해 UPSERT 하기 때문. 단일 트랜잭션 유지.
Read access patterns¶
Core 도메인은 읽기만 허용하는 다른 schema 가 있다. Port 인터페이스를 통해서만 접근:
- Recovery → Guild — 길드 정보 (Port:
GuildReader) - Recovery → Member — 멤버 정보 (Port:
MemberReader) - Recovery → Licensing — 권한 체크 (Port:
LicensingReader) - Billing → Identity — User 정보 (Port:
UserReader) - Billing → Guild — Guild 정보 (Port:
GuildReader) - Billing → Licensing — Plan 정보 (Port:
PlanReader)
반대 방향(Supporting → Core) 은 이벤트 구독으로만 (직접 읽기 최소화).
Schema size estimation (MVP, 1,000 guild 기준)¶
| Schema | Est. size | Notes |
|---|---|---|
identity | 수 MB | 사용자 수 ≈ 1,000~10,000 |
guild | 수 MB | 1,000 row + config |
member | 수백 MB | 길드당 평균 100명 = 100,000 row |
licensing | <1 MB | Plan 3개 + License 1,000 |
billing | 수 MB | Subscription 수백 + PaymentAttempt 수만 |
recovery.guild_* | 수백 MB | 길드당 수백 row × 4 테이블 |
recovery.snapshots | 수 GB | 길드당 1MB × 보관 기간 |
recovery.restore_jobs | 수 MB | 드문 이벤트 |
recovery.antinuke_incidents | <수 MB | 드문 이벤트 |
notification | 수백 MB | 월 수만 건 × 90일 |
audit | 수 GB | 월 수만~수십만 건 × 영구 |
webhook | 수백 MB | 월 수만 건 × 90일 |
events.outbox | 수백 MB | 30일 롤링 |
MVP 전체 약 10GB 이내, Neon 운영에 여유.
Schema list (quick reference)¶
기본 쿼리 범위를 명확히 하기 위한 전체 schema 이름:
search_path 에 의존하지 않음. 모든 SQL 은 {schema}.{table} 로 명시 (ADR-0020 compliance).
See also¶
adr/0020-postgres-schema-per-domain.md— 분리 결정adr/0021-pk-uuid-v7.md— PK 타입data/migration-strategy.md— Atlas 기반 진화 전략- 도메인별 schema 문서:
data/identity-schema.mddata/guild-schema.mddata/member-schema.mddata/licensing-schema.mddata/billing-schema.mddata/recovery-schema.mddata/notification-schema.mddata/audit-schema.mddata/webhook-schema.mddata/events-schema.md