Identity¶
Umbra 에서 사용자를 식별하고 인증하는 도메인. Discord 사용자와 1:1 매핑된 내부 User 계정을 관리하며, Discord OAuth2 로 인증하고 세션을 유지한다.
Bounded context¶
- Type — Supporting
- Sibling contexts — Guild, Member, Billing (결제 주체 = User)
- Location in codebase —
engine/identity/
Why this domain exists¶
Discord User ID 를 그대로 외부에 노출하거나 도메인 키로 사용하면 두 가지 문제가 발생한다.
첫째, 사용자 탈퇴/재가입 처리의 모호성. Discord 는 같은 사용자가 계정을 삭제하고 재가입하면 새 ID 를 발급한다. 내부 User 엔티티가 Discord ID 와 직결되면 탈퇴 이력 관리가 어렵다.
둘째, 결제 주체의 독립성. Hybrid 모델(ADR-0011)에서 결제는 User 단위이며 빌링키를 소유한다. Discord ID 가 아니라 Umbra 내부 User ID (UUID v7) 를 사용해야 Toss customerKey 같은 외부 참조를 안정적으로 유지할 수 있다.
따라서 Identity 는 "Discord 세계" 와 "Umbra 세계" 를 연결하는 경계 역할을 한다.
Domain model¶
User¶
Umbra 의 사용자 계정. Discord User 와 1:1 매핑된다.
User
├─ id UUID v7 PK
├─ discord_user_id TEXT Discord snowflake, UNIQUE
├─ username TEXT 최근 Discord username (캐시)
├─ avatar_hash TEXT Discord avatar hash (캐시)
├─ locale TEXT 사용자 언어 설정 (ko, en 등)
├─ created_at TIMESTAMPTZ
├─ updated_at TIMESTAMPTZ
└─ deleted_at TIMESTAMPTZ soft delete (탈퇴 이력)
username과avatar_hash는 표시 목적 캐시. source of truth 는 Discord.deleted_at은 soft delete — 탈퇴 후에도 과거 결제/감사 기록 참조 유지.
Session¶
OAuth2 로그인 후 생성되는 웹 세션.
Session
├─ id TEXT Session ID (cryptographic random)
├─ user_id UUID v7 → users.id
├─ discord_access_token TEXT 암호화
├─ discord_refresh_token TEXT 암호화
├─ access_expires_at TIMESTAMPTZ
├─ user_agent TEXT
├─ ip_address INET (GDPR 고려 — 단기 보관만)
├─ created_at TIMESTAMPTZ
└─ expires_at TIMESTAMPTZ TTL (7일 기본)
Session 은 Redis 에 저장되며 (session:{id}), DB 는 감사 목적의 그림자 레코드.
Aggregates¶
- User aggregate — User + 자신의 Session 목록
- Billing 에서 사용하는 BillingKey 는 여기 aggregate 가 아님 (Billing 도메인 소유)
Invariants¶
- Discord User 1:1 —
UNIQUE(discord_user_id)DB 제약 - Active session ≤ N per user — 과도한 세션 발급 방지 (기본 10개, 초과 시 가장 오래된 세션 폐기)
- Soft delete — User 삭제 시
deleted_at만 설정, row 유지 - Token encryption —
discord_access_token,discord_refresh_token은 항상 암호화 저장
State machine¶
User 의 상태:
stateDiagram-v2
[*] --> Active : OAuth2 first login
Active --> Deleted : User requests deletion
Deleted --> Active : Re-login with same Discord ID
Session 의 상태:
stateDiagram-v2
[*] --> Active : Login
Active --> Expired : TTL reached
Active --> Revoked : Logout or admin action
Expired --> [*]
Revoked --> [*]
Domain events¶
Published¶
| Event | Trigger | Payload | Subscribers |
|---|---|---|---|
UserRegistered | 신규 User 생성 | user_id, discord_user_id | Audit |
UserSessionCreated | 새 세션 생성 | user_id, session_id (IP 마스킹) | Audit |
UserDeleted | User soft delete | user_id | Billing (빌링키 처리), Audit |
Consumed¶
Identity 는 다른 도메인의 이벤트를 구독하지 않는다. 가장 상류 도메인.
Ports¶
Identity 는 Supporting Context 이므로 단순 구조다. 공식 Port 분리는 최소화.
Inbound¶
// engine/identity/service.go
type Service interface {
AuthenticateWithDiscord(ctx, authCode string) (*User, *Session, error)
GetUser(ctx, userID) (*User, error)
GetUserByDiscordID(ctx, discordID string) (*User, error)
CreateSession(ctx, userID) (*Session, error)
RevokeSession(ctx, sessionID) error
DeleteUser(ctx, userID) error
}
Outbound¶
- DiscordOAuth2Client — Discord OAuth2 API 호출 (
engine/identity/adapter/discord/) - UserRepository — sqlc 기반 (
engine/identity/adapter/persistence/) - SessionStore — Redis (
engine/identity/adapter/session/)
Adapters¶
- Persistence —
engine/identity/adapter/persistence/— sqlc 래퍼,identityschema - Discord —
engine/identity/adapter/discord/— Discord OAuth2 token exchange, user fetch - Session —
engine/identity/adapter/session/— Redis set/get/del
Permission model¶
Identity 는 다른 도메인의 권한 기반. 자체 권한은 단순:
- User 자신의 데이터 조회/수정만 허용
- 관리자 기능은 없음 (Umbra 운영자는 별도 시스템 경유)
다른 도메인이 "이 User 가 이 Guild 의 Manage Server 권한을 가진가" 를 확인하려면 Discord API 를 경유 (Identity 가 알지 못함).
Failure modes¶
- Discord OAuth2 장애 — 로그인 불가. 기존 세션은 유효하므로 서비스 가용성은 부분 유지.
- Access token 만료 — Refresh token 으로 자동 갱신. Refresh 도 실패하면 재로그인 요구.
- Redis 장애 — 신규 세션 생성 불가. 기존 DB 에 그림자 레코드로 세션 복구 가능성 (Phase 2 고려).
- 빌링키 연결된 User 삭제 —
UserDeleted이벤트 → Billing 이 빌링키 삭제 + Subscription suspend.
See also¶
data/identity-schema.md— DB 스키마 상세flows/web-join.md— OAuth2 로그인 흐름domain/billing.md— 결제 주체로서 User 사용adr/0011-hybrid-license-model.md— User 의 역할