Audit Schema¶
auditschema 는 모든 도메인 이벤트의 영구 감사 로그를 저장한다. 결제 분쟁, 보안 조사, 사용자 활동 내역의 source of truth.
Schema purpose¶
- PostgreSQL schema —
audit - Corresponding domain —
engine/audit/ - sqlc package —
db/queries/audit/
모든 Outbox 이벤트가 한 테이블에 누적된다. 조회 인덱스 설계가 성능 핵심.
Tables¶
audit.events¶
CREATE TABLE audit.events (
id UUID PRIMARY KEY,
outbox_event_id BIGINT NOT NULL,
aggregate_type TEXT NOT NULL,
aggregate_id UUID NOT NULL,
event_type TEXT NOT NULL,
actor_user_id UUID REFERENCES identity.users(id),
actor_type TEXT NOT NULL CHECK (actor_type IN ('user', 'system', 'cron', 'webhook')),
guild_id UUID REFERENCES guild.guilds(id),
payload JSONB NOT NULL,
summary TEXT,
occurred_at TIMESTAMPTZ NOT NULL,
recorded_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
CONSTRAINT events_outbox_unique UNIQUE (outbox_event_id)
);
Columns
| Column | Type | Constraints | Description |
|---|---|---|---|
id | UUID v7 | PK | 감사 이벤트 ID |
outbox_event_id | BIGINT | UNIQUE, NOT NULL | → events.outbox.id (중복 방지) |
aggregate_type | TEXT | NOT NULL | 발행 aggregate 타입 |
aggregate_id | UUID | NOT NULL | entity ID |
event_type | TEXT | NOT NULL | 이벤트명 (CamelCase) |
actor_user_id | UUID | FK nullable | 행위자 (user 타입일 때) |
actor_type | TEXT | CHECK | 행위자 종류 |
guild_id | UUID | FK nullable | 관련 길드 (있을 때) |
payload | JSONB | NOT NULL | 원본 이벤트 payload |
summary | TEXT | — | 대시보드 표시용 요약 |
occurred_at | TIMESTAMPTZ | NOT NULL | 원본 이벤트 발생 시각 |
recorded_at | TIMESTAMPTZ | NOT NULL | 감사 기록 시각 |
Indexes
| Index | Columns | Purpose |
|---|---|---|
events_pkey | (id) | PK |
events_outbox_unique | (outbox_event_id) | 중복 방지 |
events_aggregate_idx | (aggregate_type, aggregate_id, occurred_at DESC) | entity 이력 조회 |
events_guild_idx | (guild_id, occurred_at DESC) WHERE guild_id IS NOT NULL | 길드 타임라인 |
events_actor_idx | (actor_user_id, occurred_at DESC) WHERE actor_user_id IS NOT NULL | 사용자별 활동 |
events_event_type_idx | (event_type, occurred_at DESC) | 타입별 통계/검색 |
events_occurred_at_idx | (occurred_at DESC) | 최근 이벤트 전역 |
Foreign keys
| Column | References | On delete |
|---|---|---|
actor_user_id | identity.users(id) | SET NULL (탈퇴 시 마스킹) |
guild_id | guild.guilds(id) | NO ACTION (Guild 는 soft delete) |
Invariants
- Immutable — INSERT 만, UPDATE/DELETE 금지 (애플리케이션 + 리뷰 강제)
outbox_event_id는 DB 에서 UNIQUE 로 중복 INSERT 거부actor_type = 'user'이면actor_user_id IS NOT NULL권장 (애플리케이션)actor_type = 'system'/'cron'/'webhook'이면actor_user_id IS NULL
Relationships¶
erDiagram
OUTBOX ||--o| EVENTS : "recorded as"
USERS ||--o{ EVENTS : "actor of"
GUILDS ||--o{ EVENTS : "context"
Cross-schema references¶
| From | To | Semantics |
|---|---|---|
audit.events.outbox_event_id | events.outbox.id | 원본 outbox 이벤트 (FK 아님 — outbox 는 archive 대상이라) |
audit.events.actor_user_id | identity.users.id | 행위자 |
audit.events.guild_id | guild.guilds.id | 관련 길드 |
참고: outbox_event_id 는 FK 가 아니다. Outbox 는 30일 후 archive 되어 row 가 삭제되므로 FK 를 걸면 Audit 이 깨진다. UNIQUE 제약만 유지.
Query patterns¶
ListByGuild (대시보드)¶
-- ListByGuild
SELECT *
FROM audit.events
WHERE guild_id = $1
AND ($2::timestamptz IS NULL OR occurred_at < $2)
ORDER BY occurred_at DESC
LIMIT $3;
ListByAggregate (entity 이력)¶
-- ListByAggregate
SELECT *
FROM audit.events
WHERE aggregate_type = $1 AND aggregate_id = $2
ORDER BY occurred_at DESC
LIMIT $3;
ListByUser (User 활동)¶
-- ListByUser
SELECT *
FROM audit.events
WHERE actor_user_id = $1
ORDER BY occurred_at DESC
LIMIT $2;
Insert (Outbox handler)¶
-- InsertAuditEvent
INSERT INTO audit.events (
id, outbox_event_id, aggregate_type, aggregate_id,
event_type, actor_user_id, actor_type, guild_id,
payload, summary, occurred_at, recorded_at
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, NOW())
ON CONFLICT (outbox_event_id) DO NOTHING;
ON CONFLICT DO NOTHING 으로 idempotent.
Data retention¶
- 영구 보관 (MVP)
- Phase 2 에서 1년 이상 데이터 cold storage (S3 / Glacier) 이관 검토
- User 탈퇴 시
actor_user_idSET NULL (GDPR 익명화)
Size estimation¶
- 이벤트 하나 평균 1~3KB (JSONB + 인덱스 포함)
- 일일 이벤트 수 추정 (MVP 100 guild 기준):
- 결제 관련: ~200/day
- Recovery: ~50/day
- Guild/Member: ~300/day
- Notification: ~500/day
- Total: 1,0005,000/day
- 월 수십 MB, 연 수 GB — Neon 용량에 부담 없음
규모 확장 시 (10,000 guild) 일일 수십만 이벤트 → 연 수백 GB. 이 시점에 cold storage 이관 필수.
Migration history¶
| Date | Change | Rationale |
|---|---|---|
| 2026-04-xx | 초기 스키마 | MVP |
See also¶
domain/audit.md— Audit 도메인data/events-schema.md— Outbox 원본architecture/event-flow.md— 이벤트 카탈로그adr/0016-outbox-pattern.md