growthGrid

Architecture

Live boards feel simple until two people move the same card. How we made every change a single event that is stored, broadcast and kept in history.

2 min read

Kanban board with live cursors beside an event stream of committed, broadcast changes
On this page

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.

python
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())
The change and its event are committed together. Broadcasting happens only after the commit succeeds.

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.

  1. Write the change and its event in one transaction.
  2. Publish to the board’s channel only after the commit.
  3. Render history straight from the stored events.
  4. Notify watchers from the same event, skipping whoever made the change.
Write the change once, then fan it out.
— growthGrid engineering notes

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.

  • Real-time
  • WebSockets
  • FastAPI
  • PostgreSQL

Share

Insights

Put it into practice

Let’s turn fresh perspectives into practical solutions built around your product and business goals.

Booking new projects for Q4 2026

We reply to every enquiry within one business day.