콘텐츠로 이동

Audit Schema

audit schema 는 모든 도메인 이벤트의 영구 감사 로그를 저장한다. 결제 분쟁, 보안 조사, 사용자 활동 내역의 source of truth.

Schema purpose

  • PostgreSQL schemaaudit
  • Corresponding domainengine/audit/
  • sqlc packagedb/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_id SET 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