-- +goose Up -- A per-message bitmask of the seats that have NOT yet read this chat entry: bit i is -- set while seat i still has the message unread, and is cleared when that seat reads it -- (opens the move history / chat, or — for a nudge — takes its move). A text message -- starts with the bits of every seated recipient except the sender; a nudge starts with -- only the awaited player's bit. The mask is inverted so "anything unread" is a plain -- `unread_seats <> 0`, which the lobby badge, the admin filter and the unread gauge all -- use. The read time itself is not retained. See docs/ARCHITECTURE.md §8 (Read receipts). SET search_path = backend, pg_catalog; ALTER TABLE chat_messages ADD COLUMN unread_seats smallint NOT NULL DEFAULT 0; -- Partial index serving the unread scans (per-viewer lobby lookup, admin unread filter, -- the unread-count gauge): only the comparatively few still-unread rows are indexed. CREATE INDEX chat_messages_unread_idx ON chat_messages (game_id) WHERE unread_seats <> 0; -- +goose Down SET search_path = backend, pg_catalog; DROP INDEX chat_messages_unread_idx; ALTER TABLE chat_messages DROP COLUMN unread_seats;