High-Performance Kanban

SolidBoard:
Wait-Free Management

A production-grade, Jira-style Kanban application built with SolidJS. Designed for speed, it leverages fine-grained reactivity to deliver a lag-free experience.

O(1) updates, no VDOMCustom drag & drop engineStateless JWT auth

CORE CAPABILITIES

Performance First

Leverages SolidJS for O(1) updates, eliminating wasted renders.

Drag & Drop

Custom logic for smooth task reordering and column movement.

Smart Filtering

Instant multi-criteria filtering using derived signals.

Deep State

Complex nested state management with Solid Stores.

Glass UI

Responsive glassmorphism design with backdrop filters.

Secure Auth

JWT-based session management and protected API routes.

Application Interface

High-performance reactive interface with fine-grained updates.

Kanban Interface
Core Workflow

Kanban Interface

Workspace Dashboard
Management

Workspace Dashboard

Advanced Filtering
Productivity

Advanced Filtering

Task Details
Task Management

Task Details

The constraint

Two runtime dependencies

The frontend's package.json lists solid-js and @solidjs/router. That is the whole list. No component library, no state manager, no drag-and-drop package, no date library, no icon set. Everything below is a consequence of that decision rather than a separate feature.

WHY SOLID AND NOT REACT

A Kanban board is a stress test for a reactive model in a specific way: during a drag, a fast-changing piece of state — which card is held, which gap it is hovering — is read by almost every node on the screen. Under a virtual DOM that is a reconciliation pass per pointer move over the entire board.

Solid compiles the JSX into direct DOM instructions. There is no diff, and a signal read inside a component body does not subscribe the component to it — it subscribes the exact expression that read it. A card turning translucent because it is being dragged updates that one class binding and nothing else, whether the board holds ten cards or three hundred.

The cost is that the ergonomics are unforgiving. Props are getters, so destructuring them silently breaks reactivity, and the entire component body runs exactly once. It is a model that rewards knowing what it is doing and punishes React habits.

WHERE THE STATE LIVES

Boards sit in a Solid store rather than a signal, because store writes are path-addressed. Updating one board's title is a write to that board's title node — anything watching a different board is not notified at all, without any memoisation being written by hand.

Filtering is a derived value rather than an effect that writes more state. One createMemo narrows the task list by search text, then priority, then tag; each column then takes its own slice of that memo and sorts by order. Typing into the search box recomputes one memo and touches only the cards whose membership actually changed.

The input itself is held locally and pushed into the shared query through a 300ms debounce, so the memo chain runs on pauses in typing rather than on keystrokes, while the field itself stays perfectly responsive.

The drag engine

144 lines on top of the browser's own API

The HTML5 drag-and-drop API has a bad reputation, most of it earned by its drag-image handling. Everything else it offers — drag start and end, enter, over, leave and drop, all with a transferable payload — is exactly what a Kanban board needs. What it does not give you is where the card should land, and that is the part worth writing.

FOUR THINGS IT HANDLES

Two signals, not a context

dragState and dropTarget are module-level signals created once outside any component. A card and a column both read them directly, so the drag has no shared ancestor to hang state on and no provider to re-render.

Position comes from the pointer, not the element

getDropPosition measures the cursor's Y against the midpoint of the hovered card's bounding rect and returns 'before' or 'after'. Hovering the top half of a card means above it — which is what people expect, and what you lose if you only track which element is under the pointer.

The same-column off-by-one is handled explicitly

calculateDropIndex increments for an 'after' drop, then decrements again when the move is within one column and the card started above its target — because removing it from its old slot shifts everything below up by one. Getting this wrong is why hand-rolled boards drop cards one place off, downward only.

The drag preview is a clone

createDragImage deep-clones the card, parks it a thousand pixels off-screen, drops it to 80% opacity and rotates it three degrees. The browser snapshots it for the drag image, then it is removed from the document.

DRAGGING WITHOUT A MOUSE

Native drag-and-drop is unusable from a keyboard, and that is normally where a board stops being accessible. So there is a second path into the same move function: with a card focused, Ctrl or Cmd held, the arrow keys move it. Up and down reorder it within its column; left and right send it to the adjacent column. Both paths call moveTask — the keyboard route is not a reduced version of the mouse one.

Every move is spoken. An ARIA live region backed by a signal announces “Moved task up” or “Moved task to In Progress”, and it clears itself before writing so that repeating the same action is announced again instead of being swallowed as an unchanged string.

The rest of the accessibility layer is the unglamorous half: a Tab focus trap that cycles within an open modal, arrow navigation that wraps at both ends with Home and End jumps, due dates expanded into full spoken sentences for screen readers, and priorities read as “Urgent priority” rather than as a colour nobody can hear.

Persistence

Ordering is dense integers

The Express API exposes one endpoint for movement, PUT /api/tasks/:id/move, taking a target column and a target index. Everything else about a task goes through the normal update route, so there is exactly one place where ordering can be got wrong.

Positions are contiguous integers per column, which means a move is never one write. Across columns it is three: the card is repositioned, then everything below its old slot is decremented, then everything at or after its new slot is incremented. Within a column it is two, and the direction decides which range shifts and which way. Each of those shifts is a single updateMany with $inc over a range predicate, rather than a read-modify-write of every affected row.

Two compound indexes exist for exactly these queries: board with column, and board with order.

THE TRADE-OFF

Dense integers are the obvious choice and the slow one. Every move is linear in the size of the affected range, and two people reordering the same column at the same moment can interleave their shifts into an order neither of them asked for.

Fractional or lexicographic ranks are the standard fix: give each card a key strictly between its neighbours', and a move becomes one write to one row with no shifting at all. The price is keys that drift toward unreadable as they subdivide, plus a background rebalance to stop that. For a single-owner board, three bounded writes is a worse algorithm and a much easier system to reason about — the ordering column stays a small integer you can read straight out of the database.

Below that, the data model is deliberately plain. A board embeds its columns as a subdocument array and ships with five by default — Backlog, To Do, In Progress, Review, Done — because columns are owned by exactly one board and never queried independently. Comments embed in their task for the same reason. Priorities are a four-value enum from low to urgent, and every board route re-verifies ownership against the authenticated user before touching anything, so a stateless JWT proves who you are and the route still proves what you may reach.

TECHNICAL ARCHITECTURE

frontend

  • SolidJS

    Fine-grained reactivity, no virtual DOM

  • Solid Router

    The only other runtime dependency

  • Vite

    Build tooling and HMR

backend

  • Node.js & Express

    Scalable REST API

  • MongoDB & Mongoose

    Flexible Document Storage

  • JWT Auth

    Stateless Security

features

  • Drag & Drop API

    Native HTML5 Implementation

  • Solid Stores

    Deeply Nested State Management

  • Glassmorphism

    Modern translucent UI design