chatmu · postgres · dbmate

스키마 지도

워크스페이스 사이클이 끝난 시점의 스키마다. 정본은 db/migrations/ 이고, 이 그림은 거기서 뽑았다 — npm run db:migrate && npm run db:diagram.

표 13개 마이그레이션 20개 이번 사이클이 더한 표 4개 뽑은 날 2026-08-23

관계 그림

생성된 그대로다(graphviz). 넓어서 가로·세로로 밀어 볼 수 있다 — db/.diagram/schema.dbml 을 dbdiagram.io 에 붙여넣으면 인터랙티브하게 열린다. ⛔ 이 파일들은 커밋하지 않는다: 정본이 둘이 되면 마이그레이션만 고친 날 그림이 조용히 갈린다.

dbml actors       actors        id     bigint (!) display_name     text (!) created_at     timestamp (!) status_message     text profile_body     text profile_body_format     text bots       bots        actor_id     bigint (!) owner_actor_id     bigint (!) bot_token_hash     text (!) deactivated_at     timestamp actors:e->bots:w * 1 direct_rooms       direct_rooms        actor_a_id     bigint (!) actor_b_id     bigint (!) created_at     timestamp (!) room_id     text (!) workspace_id     text (!) actors:e->direct_rooms:w * 1 actors:e->direct_rooms:w * 1 messages       messages        id     bigint (!) actor_id     bigint (!) body     text (!) client_message_id     uuid created_at     timestamp (!) edited_at     timestamp deleted_at     timestamp body_format     text (!) room_id     text (!) actors:e->messages:w * 1 room_participants       room_participants        actor_id     bigint (!) joined_at     timestamp (!) last_read_message_id     bigint room_id     text (!) actors:e->room_participants:w * 1 users       users        login_id     text (!) password_hash     text (!) created_at     timestamp (!) password_changed_at     timestamp (!) role     text (!) actor_id     bigint (!) actors:e->users:w * 1 workspace_members       workspace_members        workspace_id     text (!) actor_id     bigint (!) joined_at     timestamp (!) actors:e->workspace_members:w * 1 channel_rooms       channel_rooms        room_id     text (!) workspace_id     text (!) name     text (!)    room_id, workspace_id     room_slugs       room_slugs        slug     text (!) room_id     text (!) retired_at     timestamp workspace_id     text (!)    room_id, workspace_id     channel_rooms:e->room_slugs:w * 1 rooms       rooms        created_at     timestamp (!) id     text (!) rooms:e->channel_rooms:w * 1 rooms:e->direct_rooms:w * 1 rooms:e->messages:w * 1 rooms:e->room_participants:w * 1 users:e->bots:w * 1 workspace_slugs       workspace_slugs        slug     text (!) workspace_id     text (!) retired_at     timestamp workspaces       workspaces        id     text (!) name     text (!) created_at     timestamp (!) workspaces:e->channel_rooms:w * 1 workspaces:e->direct_rooms:w * 1 workspaces:e->workspace_members:w * 1 workspaces:e->workspace_slugs:w * 1

공통 — 모두가 가리키는 것

actors

발화할 수 있는 주체. 사람이든 봇이든 여기 한 줄이다.

  • pk id · bigint
  • uniq display_name
  • status_message · profile_body(+format)

rooms

Message 가 쌓이는 장소. 남은 것은 공통뿐 — 이름도 종류도 여기 없다.

  • pk id · text (무작위 8자리)
  • idx (created_at, id) — 목록의 정렬 축

messages

발화. 작성자는 언제나 actor 이고, 정렬·커서의 정본은 id 다.

  • pk id · bigint
  • fk room_id → rooms · actor_id → actors
  • idx (room_id, id) · (room_id, client_message_id)

room_participants

이 방을 읽고 있다. 볼 자격이던 뜻이 이번에 바뀌었고, 읽음 커서가 여기 산다.

  • pk (room_id, actor_id)
  • last_read_message_id · joined_at

확장 표 — 차이만 갖는다

공통 표에 nullable 칸을 섞는 대신 차이를 옆 표로 뺀다. 그러면 그 칸들을 NOT NULL 로 둘 수 있어 어긋난 조합이 표현될 수 없다. 사람·봇이 먼저 이 길을 갔고(ADR 0011), 이번에 방이 같은 모양이 됐다.

users

사람의 차이 = 로그인. 이 행이 있으면 사람이다.

  • pk actor_id → actors
  • uniq login_id
  • password_hash · password_changed_at · role

bots

봇의 차이 = 주인과 토큰. 울타리에 종속되지 않는다 — 사람처럼 워크스페이스에 불려 든다.

  • pk actor_id → actors
  • fk owner_actor_id → users (봇이 봇을 못 가진다)
  • bot_token_hash · deactivated_at

channel_rooms 이번

채널의 차이 = 울타리와 이름. 둘 다 NOT NULL 인 것이 이 표의 값이다.

  • pk room_id → rooms
  • fk workspace_id → workspaces
  • uniq (workspace_id, room_id) — 슬러그가 이걸 짝으로 건다

direct_rooms 바뀜

이 방은 이 두 사람의 1:1 이다. 울타리 칸이 생겼다 — 같은 두 사람이 워크스페이스마다 다른 대화를 갖는다.

  • pk room_id → rooms
  • fk workspace_id · actor_a_id · actor_b_id
  • uniq (workspace_id, actor_a, actor_b)
  • chk actor_a_id < actor_b_id

울타리 — 이번 사이클이 세운 층

workspaces 이번

Room 들을 담는 울타리. id 는 방과 같은 무작위 8자리 — 주소가 개수를 안 말한다.

  • pk id · text
  • name · created_at

workspace_members 이번

이 표가 인가다. 멤버면 그 안의 채널을 전부 본다. 가리키는 것은 actor 라 봇도 같은 길로 든다.

  • pk (workspace_id, actor_id)
  • idx actor_id — 「내가 속한 울타리」

workspace_slugs 이번

울타리의 주소 — /w/soriteam. 전역 유일이다(그 위에 아무것도 없다).

  • pk slug
  • uniq workspace_id where retired_at is null

room_slugs 바뀜

방의 주소. 유일성이 울타리 안으로 좁아졌다 — 다른 팀이 같은 deploy-alerts 를 쓴다.

  • pk (workspace_id, slug)
  • fk (workspace_id, room_id) → channel_rooms
  • uniq room_id where retired_at is null

이 그림에서 읽히는 규칙 다섯