Skip to content

Status Table

The message_status table — tracking tick boundaries per user per conversation

Two integers per conversation per user. Everything else is derived.


Schema

message_status table (DynamoDB)
───────────────────────────────────────────────────────────
PK: user_id          (who we're tracking status for)
SK: conversation_id  (which conversation)
last_delivered_seq   (double tick boundary)
last_read_seq        (blue tick boundary)

Example rows for Bob across multiple conversations:

user_id    conversation_id    last_delivered_seq    last_read_seq
bob        conv_abc123        44                    42
bob        conv_def456        17                    17
bob        conv_ghi789        8                     8

Reading this: - In conv_abc123, Bob has received messages up to seq=44, but only read up to seq=42. Messages 43 and 44 are delivered but unread (double tick, not blue). - In conv_def456 and conv_ghi789, Bob has both received and read everything up to the latest seq.


Separation of concerns — two tables, two responsibilities

This table is separate from pending_deliveries. They solve different problems:

pending_deliveries    → "what messages does Bob still need to receive?"
                        used when Bob reconnects after being offline
                        PK=receiver_id, SK=conversation_id, first_undelivered_seq

message_status        → "what is the tick state of Alice's sent messages?"
                        used to show ticks on Alice's side
                        PK=user_id, SK=conversation_id, last_delivered_seq, last_read_seq

pending_deliveries is Bob's inbox state — what he hasn't received yet. message_status is Alice's display state — what tick to show on her messages.

Mixing these two would couple delivery tracking with UI rendering. Keep them separate.


Deriving tick state from the boundaries

Given these two numbers, the tick state of any message Alice sent is:

def tick_state(seq, last_delivered_seq, last_read_seq):
    if seq <= last_read_seq:
        return BLUE_TICK
    elif seq <= last_delivered_seq:
        return DOUBLE_TICK
    else:
        return SINGLE_TICK

Alice's client receives the two numbers once and applies this logic locally. No per-message queries. No per-message status columns.


Writes to this table

Three events trigger writes:

1. Message delivered to Bob's device (double tick)

Bob's client acks seq=44 over WebSocket
→ server: UPDATE message_status
    SET last_delivered_seq = 44
    WHERE user_id=bob AND conversation_id=conv_abc123

2. Bob opens the chat (blue tick)

Bob opens conv_abc123
→ Bob's client sends: "read up to seq=44"
→ server: UPDATE message_status
    SET last_read_seq = 44
    WHERE user_id=bob AND conversation_id=conv_abc123

3. Bob replies (implicit read)

Bob sends a message in conv_abc123
→ all of Alice's previous messages are implicitly read
→ server: UPDATE message_status
    SET last_read_seq = latest_seq_before_bob_reply
    WHERE user_id=bob AND conversation_id=conv_abc123

All three are single-row updates. One write per event, regardless of how many messages are in the conversation.


Initial row creation

When Alice sends the first message in a conversation and Bob is offline, there is no row in message_status yet. The row is created on first delivery ack or first read event — whichever comes first.

Interview framing

"Instead of a status column per message, we track two sequence number boundaries per user per conversation — last_delivered_seq and last_read_seq. Tick state for any message is derived from these two numbers at render time. Every status event is a single row update, not a bulk operation."