A board that updates live is one of those features people only notice when it’s missing. Someone drags a card to Done, a teammate still sees it in progress, and the next five minutes go on working out who’s right.
When we built Boardly, live updates, ticket history and watcher notifications were three separate lines on the feature list. They turned out to be one design decision.
Every change is an event
Instead of letting each feature detect changes its own way, every write — a move, an edit, a comment, a new assignee — produces one event: who, what, when, and the values before and after. The event is saved in the same transaction as the change itself.
async def move_ticket(ticket_id: int, to_column: str, user: User, db: Session):
ticket = get_ticket(db, ticket_id)
event = TicketEvent(
ticket_id=ticket.id,
actor_id=user.id,
kind="moved",
before={"column": ticket.column},
after={"column": to_column},
)
ticket.column = to_column
db.add(event)
db.commit() # the change and its history land together
await hub.publish(ticket.board_id, event.to_message())History comes for free
Because every event is stored, a ticket’s history is simply its events in order. There’s no separate audit table to keep in sync and nothing to reconstruct from logs. “Who moved this back to In progress?” becomes a query, not an investigation.
Broadcast after commit, never before
The WebSocket hub only publishes events that have been committed, so nobody ever sees a card move that didn’t really happen. Each browser subscribes to the boards it has open, which keeps a busy board from flooding everyone else.
When two people move the same card
Conflicts are rare on a small team, but they happen. Each ticket carries a version number: a move sent with a stale version is rejected and the client fetches the latest ticket. The person sees the card where it really is, and the change that beat them sits at the top of its history.
Notifications are just another subscriber
Watchers subscribe to a ticket. When an event lands, the notifier checks who’s watching and skips the person who made the change — nobody needs to hear about their own edit. Because notifications, history and the live board all read the same event, they can’t disagree.
- Write the change and its event in one transaction.
- Publish to the board’s channel only after the commit.
- Render history straight from the stored events.
- Notify watchers from the same event, skipping whoever made the change.
Write the change once, then fan it out.
Real-time features get messy when each one invents its own idea of what changed. Agree on the event first, and live updates, history and notifications become three views of the same truth.



