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.
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

Workspace Dashboard

Advanced Filtering

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