Bot Installation¶
사용자가 Umbra Bot 을 Discord 길드에 처음 설치하는 흐름. Guild 등록, Free License 자동 생성, AntiNuke workflow 시작(Pro+), 환영 DM 까지의 end-to-end.
Scenario¶
길드 관리자가 Discord 의 "앱 추가" UI 또는 Umbra 웹사이트의 "Add to Discord" 버튼으로 봇을 길드에 초대한다. Discord 가 GUILD_CREATE 이벤트를 Gateway 로 전송하고, Umbra 가 이를 감지해 길드 등록과 초기 설정을 수행한다.
결과: Guild row 생성, Free License 부여, Guild 의 관리자가 Umbra 웹 대시보드에서 확인 가능한 상태.
Actors¶
- Guild Admin — 봇을 초대하는 사용자 (Discord 의
MANAGE_GUILD권한 보유) - Discord Gateway —
GUILD_CREATE이벤트 전송 - umbra-bot — Gateway 이벤트 수신 및 1차 처리
- Guild domain — 길드 등록
- Licensing domain — Free License 자동 생성
- Recovery (AntiNuke) — Plan 에 따라 workflow 시작
- Notification domain — 환영 메시지
- Audit domain — 이벤트 기록
Preconditions¶
- Umbra Bot 이 Discord Developer Portal 에 등록되어 있음
- Bot 이 초대 링크로 필요한 scope 와 permissions 를 요청함 (
bot,applications.commands) - Guild Admin 이
MANAGE_GUILD권한 보유 - 봇이 이 길드에 이전에 있었다면 관련 Guild row 가
bot_removed_at IS NOT NULL상태로 존재
Postconditions¶
guild.guilds에 row 존재 (신규 또는 재설치 복원)guild.guild_configs에 기본 설정 row 존재licensing.licenses에 Free Plan active License 존재events.outbox에BotInstalled이벤트 기록audit.events에 감사 로그 기록- Guild Admin 에게 환영 DM 발송 (거부 가능)
recovery.antinuke_incidents는 비어 있음 (Free 는 AntiNuke 없음)
Sequence¶
sequenceDiagram
participant Admin as Guild Admin
participant Discord
participant Bot as umbra-bot
participant Guild as Guild Service
participant Licensing
participant Outbox as events.outbox
participant Poller as Outbox Poller
participant Notification
participant Audit
Admin->>Discord: Click invite link
(authorize bot)
Discord->>Bot: Gateway: GUILD_CREATE
Bot->>Bot: Parse event
extract installer (Audit Log fallback)
Bot->>Guild: RegisterGuild(discordGuildID, installerDiscordID)
Guild->>Guild: BEGIN TX
Guild->>Guild: UPSERT guild.guilds
(bot_removed_at = NULL)
Guild->>Guild: UPSERT guild.guild_configs
(기본값)
Guild->>Outbox: INSERT BotInstalled event
Guild->>Guild: COMMIT
Guild-->>Bot: Guild{id, isNew}
Bot-->>Discord: (no immediate response; event processed)
Note over Poller: ~2 seconds later
Poller->>Outbox: SELECT unpublished
Outbox-->>Poller: BotInstalled event
Poller->>Licensing: OnBotInstalled(event)
Licensing->>Licensing: BEGIN TX
Licensing->>Licensing: INSERT license (plan=FREE, status=active, expires_at=NULL)
Licensing->>Outbox: INSERT LicenseGranted event
Licensing->>Licensing: COMMIT
Poller->>Notification: OnBotInstalled(event)
Notification->>Notification: INSERT welcome notification request
Notification->>Discord: DM to installer (via asynq worker later)
Poller->>Audit: OnBotInstalled(event)
Audit->>Audit: INSERT audit.events (BotInstalled)
Poller->>Audit: OnLicenseGranted(event)
Audit->>Audit: INSERT audit.events (LicenseGranted)
Step-by-step¶
1. Invite and authorize¶
Guild Admin 이 umbra.ink 의 "Add to Discord" 버튼 또는 OAuth2 URL 로 봇을 초대한다.
https://discord.com/api/oauth2/authorize
?client_id={UMBRA_CLIENT_ID}
&permissions={REQUIRED_PERMISSIONS}
&scope=bot+applications.commands
&guild_id={pre-filled}
Discord 가 권한 확인 화면을 띄우고 Admin 승인 시 봇을 길드에 추가.
2. GUILD_CREATE event received¶
umbra-bot 의 Gateway 핸들러가 GUILD_CREATE 이벤트를 수신한다. 주의: Gateway 는 재연결 시에도 이미 봇이 있는 길드에 대해 GUILD_CREATE 를 전송한다. Umbra 는 이미 설치된 길드 와 신규 설치 를 구분해야 한다.
// apps/bot/internal/handler/gateway.go
func (h *Handler) OnGuildCreate(ev *events.GuildCreate) {
// isNew 는 Discord 의 GUILD_CREATE 구분 플래그 (unavailable=false 이면서 처음 보는 길드)
if isAlreadyTracked(ev.Guild.ID) { return }
installer := h.resolveInstaller(ev.Guild.ID) // Audit Log 조회 fallback
h.guildService.RegisterGuild(ctx, ev.Guild.ID, installer.ID)
}
3. RegisterGuild (Guild domain)¶
Guild Service 가 트랜잭션 내에서:
guild.guildsUPSERT — 이전bot_removed_at있으면 NULL 로 복원, 신규면 INSERTguild.guild_configsUPSERT — 기본값 (notification_channel_id NULL, antinuke_enabled false)BotInstalled이벤트 Outbox INSERT- 트랜잭션 커밋
이 트랜잭션은 반드시 한 번에 COMMIT. 중간 실패 시 전체 롤백.
4. Outbox poller dispatch¶
약 2초 후 Worker 의 Outbox poller 가 BotInstalled 이벤트를 pickup. 구독자:
- Licensing — Free License 생성
- Notification — 환영 DM 요청
- Audit — 감사 로그
각 핸들러는 idempotent. 이벤트가 중복 전달되어도 결과 동일.
5. Free License 생성¶
Licensing handler:
func (h *LicensingHandler) OnBotInstalled(ctx, event) error {
// 이미 active license 있으면 no-op (재설치 시)
existing, _ := h.repo.GetActiveByGuildID(ctx, event.GuildID)
if existing != nil {
return nil
}
freePlan := h.plans.GetByCode(ctx, "FREE")
license := License{
ID: uuid.NewV7(),
GuildID: event.GuildID,
PlanID: freePlan.ID,
Status: "active",
GrantedAt: time.Now(),
ExpiresAt: nil, // Free 는 무기한
}
// 같은 트랜잭션에서 LicenseGranted 이벤트 발행
return h.licenses.InsertWithEvent(ctx, license)
}
6. Welcome DM (Notification)¶
Notification handler 가 Discord DM 요청을 큐에 등록. asynq 워커가 Discord API 로 DM 발송:
안녕하세요, Umbra 가 설치되었습니다.
Free 플랜이 자동으로 활성화되었으며, 웹 조인(join.umbra.ink/{slug}) 기능을
바로 사용할 수 있습니다.
Pro 플랜으로 업그레이드하면 실시간 백업, 복구, Anti-Nuke 보호가 활성화됩니다.
대시보드: app.umbra.ink
7. Audit recording¶
Audit consumer 가 BotInstalled 과 LicenseGranted 두 이벤트를 각각 audit.events 에 INSERT. outbox_event_id UNIQUE 로 중복 방지.
Failure cases¶
Installer identification fails¶
- When —
GUILD_CREATEpayload 에 설치자 ID 없음 (드물지만 가능), Audit Log 접근 권한 없음 - Detection — 봇 권한 부족
- Response —
owner_user_id = NULL로 기록,owner_discord_id만 저장 (길드 owner 로 fallback) - User experience — 환영 DM 은 owner 에게 발송
Bot doesn't have VIEW_AUDIT_LOG¶
- When — 봇 초대 시 권한을 축소해서 초대
- Detection — AntiNuke 를 위한 audit log 접근 불가
- Response — 봇 환영 DM 에 "Anti-Nuke 기능을 위해 VIEW_AUDIT_LOG 권한을 허용해주세요" 안내. Free 에서는 기능 영향 없음
- User experience — 사용자가 권한 추가 후 Pro 업그레이드 시 안내 재표시
Licensing INSERT race (재설치 직후 중복 요청)¶
- When — 짧은 시간에 bot kicked → reinstall 반복
- Detection —
UNIQUE (guild_id) WHERE status IN ('active', 'suspended')partial index 위반 - Response — Licensing handler 의 idempotency check 에서 조용히 no-op
- User experience — 투명 (영향 없음)
Outbox poller 지연¶
- When — Worker 프로세스 과부하
- Detection —
BotInstalled이벤트가 수 분 동안 unpublished 상태 - Response — Worker 스케일 자동 확장 (Fly.io auto-scale). 대시보드에서 "License 생성 중" 일시 표시.
- User experience — 최대 수 초~수십 초 지연 후 정상
Welcome DM 거부¶
- When — 사용자의 Discord DM 설정이 "서버 멤버만 DM 허용 안 함"
- Detection — Discord API 403 응답
- Response — 길드의
notification_channel_id가 설정되어 있으면 해당 채널로 fallback. 없으면 기본 시스템 채널 시도. - User experience — DM 대신 길드 채널 메시지 표시
Edge cases¶
Guild 가 이전에 삭제 상태¶
deleted_at IS NOT NULL인 Guild 에 봇 재설치- 정책:
deleted_atNULL 로 복원하고bot_removed_atNULL. License 도 active 로 재활성 (Free 로 리셋) - 기존 스냅샷은 retention 정책에 따라 이미 삭제되었을 가능성 높음
Bot 강퇴 → 재설치 (Pro 구독 유지 중)¶
BotKicked로 License + Subscription 은 suspended 상태였음- 재설치 시
BotInstalled→ Licensing 이 "suspended 인 License 있는지" 확인 → resume - Billing 도
OnBotInstalled구독하여 suspended subscription 을 active 로 복귀 - 다음 결제 시점은 이전 구독 기준 유지
매우 큰 길드 (10,000+ 멤버)¶
GUILD_CREATEpayload 가 큼 (Discord 가 chunk 로 전달)- Bot 은
guild_id와 기본 메타데이터만 추출해 RegisterGuild 호출, 멤버 목록은 Live Sync 로 처리 - 초기 멤버 전체 sync 는 Live Sync 의 별도 bootstrap 작업 (Phase 2)
웹 조인 slug 충돌¶
- 기본 slug 는 길드 이름 기반 (
my-cool-guild) - 이미 같은 slug 가 있으면
-{random4}접미사 - 충돌이 여러 번이면 fully random slug (
abc-def-ghi형태)
Involved domains¶
| Domain | Role in this flow |
|---|---|
| Guild | 길드 등록 / 메타데이터 관리 (writer) |
| Licensing | Free License 자동 생성 |
| Notification | 환영 DM |
| Audit | 감사 기록 |
| Recovery (AntiNuke) | Pro+ 일 때 workflow 시작 (이 flow 에서는 Free 라 skip) |
See also¶
domain/guild.mddomain/licensing.mdflows/first-subscription.md— Free → Pro 전환의 시작점flows/web-join.md— 설치 후 바로 사용 가능한 기능adr/0011-hybrid-license-model.md