Pulseboard Source

A light issue tracker

Track the work, not the tool.

Projects, issues and a board you can drag — without the forty screens of configuration. What is assigned to you, what you looked at last and what you starred, one click away.

43 tests passing Node 22 · TypeScript · Express · Postgres 16
The issue page: description and comments on the left, the fields in a sidebar
The issue page: description and comments on the left, the fields in a sidebar

The hard part

Two people change the same issue at the same moment. What happens?

Most tutorials answer “last write wins” and move on, which in practice means one person’s drag silently undoes the other’s while both screens show something different. Pulseboard answers it in three parts.

1. Positions are fractional indexes, not integers

A card’s place is a base-62 string compared lexicographically. Dropping between "a1" and "a3" mints "a2" and writes one row. The integer alternative renumbers the whole column on every move, so concurrent drops collide on rows neither person touched.

That design has a dependency it does not announce: the keys are compared byte-wise. Postgres only does that if you say so — a text column inherits the database collation, and under en_US.UTF-8 the comparison is case-insensitive, so 'l' < 'V' and the server silently disagrees with the client about card order. The tests passed twenty times locally and failed on the first CI run, because my Postgres was C.UTF-8 and the stock image is not. Fixed by pinning the column to COLLATE "C", with a regression test.

2. Only one end of the drop is trusted

The browser says “put this between A and B”, but by the time that lands, B may have moved and someone else’s card may already be in the gap. The server trusts one anchor and re-reads the other bound under a row lock. Trusting both is what makes two simultaneous drops mint the same key — there is a test that fails against that version and passes against this one.

3. Same card, same moment → the loser is told

Every card carries a version. A stale drag is rejected with the authoritative card, so the optimistic UI snaps back with a toast instead of resurrecting a position someone already moved away from.

4. …and votes deliberately skip all of it

Votes commute — two people voting at the same instant is not a conflict, it is two votes. Knowing which operations need concurrency control is most of the work; adding it everywhere is just latency.

Not concurrency, but worth a line

Removing something from Recent is not deleting it. Two controls that look alike and destroy different amounts of work is the sort of thing people discover once, badly — so they are kept visibly distinct, and a test asserts a dismissed entry leaves the underlying record intact.

Running it

It is a server application — Postgres, sessions, and a database doing the work. This page is a static showcase; the app itself needs somewhere to run. One command does it:

git clone https://github.com/0211IkeVenLouie/pulseboard
cd pulseboard
docker compose up

That brings up Postgres, runs the migrations and seeds a demo with real data in it, so there is something to look at the moment it boots.

See it for yourself

  1. Open the demo workspace — no signup — and land on For you.
  2. Drag an issue across the board, reload, and watch it stay where you put it.
  3. Open the same issue in two tabs, change the priority in both, and see the second one rejected with the current values rather than overwriting the first.
  4. File two issues quickly and check the numbers: no duplicates, no gaps.
  5. Dismiss something from Recent, then open it again from Projects — dismissing a list entry never deletes the work.

Also in this set

Slotline
Availability and booking pages
Evensplit
Shared expenses, minimal settle-up
Honest about what this page is. A static showcase on GitHub Pages. The interesting behaviour described above — the database constraint, the server-side masking, the conflict resolution — happens on a server, and a static page cannot fake it without contradicting every claim on it.