콘텐츠로 이동

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 이름:

identity
guild
member
licensing
billing
recovery
notification
audit
webhook
events

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.md
  • data/guild-schema.md
  • data/member-schema.md
  • data/licensing-schema.md
  • data/billing-schema.md
  • data/recovery-schema.md
  • data/notification-schema.md
  • data/audit-schema.md
  • data/webhook-schema.md
  • data/events-schema.md