I tried every self-hosted kanban board I could find. Vikunja, Plane, Planka, Taiga, Flux, half a dozen MCP-flavored task servers. None of them did the one thing I actually needed. So I built my own in an afternoon, called it Thread, and wired it into my AI agent setup. This is the story of what I was looking for, why nothing fit, and what I ended up with.

The problem nobody solves well

As a Product Owner I juggle a lot of large topics at once. The Juniper firewall deployment that’s blocked on Gen1 VM sizes. A database contract renewal with an auto-renew date breathing down my neck. AKS cluster consolidation. GitHub billing model transitions. Risk assessments for the committee. Each of these is a topic with a handful of subtasks, and each subtask has its own status — one thing is done, another is in progress, a third hasn’t started.

What I wanted was simple to describe and apparently impossible to find: one page where I see every topic and its progress, where subtasks have their own status, and where a subtask stays visually attached to its parent.

That last part is the whole ballgame. Azure DevOps does it perfectly — the parent work item becomes a swimlane header, and its child tasks sit in the status columns underneath it, each one independently draggable. You see the hierarchy and the status at the same time. I use it every day at work and it’s the one feature I kept reaching for.

Why the usual suspects didn’t fit

I went through the whole list, and every one of them broke on the same rock.

Vikunja is a genuinely good self-hosted task manager — kanban, list, gantt, table views, the works, and a solid MCP server. But in the kanban view, subtasks render as their own cards sitting right next to the parent, with no visual indication that #2 is a child of #1. The list view shows the hierarchy but hides the status overview. You get one or the other, never both.

Plane is the closest thing to a self-hosted Jira, and it does have a native MCP server. But it’s heavy — API, worker, Redis, Postgres, the lot — and despite all that weight, the subtask relationship on the board is just a little counter badge on the card. Same fundamental problem as Vikunja, more infrastructure to run it.

Planka is a lovely lightweight Trello clone, but subtasks are checklist items on a card. You drag the whole card, never an individual subtask. Wrong model for what I needed.

Taiga gets the closest conceptually — its Scrum board has a real story-to-task relationship — but it’s also not the lightest stack, and the day-to-day feel still wasn’t quite the ADO swimlane I had in my head.

Flux and the other agent-first kanban MCP servers are built for the agent to drive the board, not for a human to actually look at and work from. Lovely for automation, useless as my personal overview.

The honest conclusion after all of that: the tool I wanted — ADO-style parent swimlanes, subtasks with independent status, lightweight, self-hosted, with an MCP server — simply does not exist as an off-the-shelf project. You can have two of those four. Not all four.

Self-hosted is always a hard requirement for me, so “just use ADO” was never going to be the answer. Which left one option.

Building it

I wrote a full spec, fed it to my IDE, and let Claude Opus 4.8 do the heavy lifting through Claude. The stack is deliberately boring and familiar:

  • Next.js 14 (App Router) + TypeScript for both the UI and the REST API
  • PostgreSQL + Prisma for storage, running on my CloudNativePG cluster
  • NextAuth for single-user auth
  • Tailwind and @dnd-kit for the board and drag-and-drop
  • A separate MCP server using the official SDK, sharing the exact same database

The data layer in lib/services/ is the single source of truth for both the REST API and the MCP tools, so there’s no drift between what the UI can do and what an agent can do.

The first version was a classic four-column board — Backlog, Planned, Ongoing, Done — with topic cards that showed a progress bar and subtask count. It worked, but it had the same flaw as everything else: when I dragged a subtask to a different column, it floated free of its parent. Back to square one.

So I rebuilt the board as a swimlane layout. Now there’s one horizontal lane per topic: a collapsible header with the topic’s title, priority dot, due date, labels and progress, and underneath it the four equal-width status columns. A subtask lives in the column matching its status within its parent’s lane, and a custom collision detection rule means it can only ever move between columns on its own row — it physically cannot leave its parent. Drag it from Backlog to Ongoing and it changes status while staying exactly where it belongs.

That was the moment it clicked. This is the ADO taskboard, self-hosted, mine.

What it grew into

Once the core worked, the features came quickly, because each one was a small, well-scoped ask:

  • Collapse lanes individually or all at once, remembered across reloads — essential once you have a lot of topics
  • Sort by priority as a header toggle, critical-to-low
  • Filter by label with clickable chips, so I can narrow to just azure or cost or devops
  • Search the whole board with / to focus — matches topic titles, descriptions, labels and subtask titles, and intelligently narrows a lane to just the matching cards
  • Due dates with at-a-glance badges: red when overdue, amber when due within a week
  • Color per topic to group related work, inherited by the subtask cards
  • Archive and restore topics off the board onto a separate page
  • Comments on both topics and subtasks, links and file attachments (the latter proxied through the app to S3-compatible storage, so the bucket stays private)
  • A dedicated topic page at /topics/{id} as a shareable deep link, with its subtasks laid out as their own four-column board

None of these are revolutionary. The point is that because I own the thing, I can add exactly the feature I want the moment I want it, with no negotiation against someone else’s product roadmap.

The part that makes it mine: the MCP server

Here’s where it ties into the rest of my homelab. I’ve been building out a self-hosted AI agent setup — Ollama, OpenWebUI, and a swarm of specialized agents. The whole reason I care about MCP is so those agents can act on real tooling.

Thread ships with an MCP server exposing 19 tools over both Streamable HTTP and stdio: list and manage topics, subtasks, comments, links and attachments, plus a board summary. It talks to the same Postgres database through the same service layer the UI uses. There’s an optional API key (MCP_API_KEY), checked with a constant-time comparison and presentable as either a Bearer token or an X-API-Key header — the latter being what most agents actually send.

So now an agent can create a topic, break it into subtasks, move things across the board, and leave a comment — and I see all of it instantly in the same UI I work from. The board is a shared surface between me and my agents, not a thing I have to keep in sync by hand.

Running it

It deploys the way everything else in my homelab does. Two container images built and pushed by a Forgejo CI workflow that auto-tags clean incrementing semver and pins the version straight into the Kubernetes manifests. CloudNativePG for the database with WAL backups to S3. Secrets via External Secrets Operator pulling from OpenBao. Traefik ingress, the usual cert-manager TLS. The app runs its migrations as an init container before serving, and the running version shows up right in the UI header so I always know what’s deployed.

For local hacking it’s a docker compose up and you’re running the app, the MCP server and Postgres together.

Was it worth building?

I spent an afternoon and got exactly the tool I’d spent days failing to find. It does the one thing — ADO-style parent swimlanes with independently-draggable subtasks — that no self-hosted project I tried gets right, plus a pile of small features tuned to how I actually work, plus an MCP server that makes it a first-class citizen in my agent setup.

Is it “real” kanban anymore? Honestly, not quite — the swimlane layout is closer to an ADO taskboard than a classic card wall, and I went back and forth on whether that was a feature or a bug. I landed on feature. The card wall was never what I needed; the overview was.

Sometimes the answer to “which self-hosted tool should I use” is “the one you write yourself.” With a capable model in your IDE, that sentence is a lot less daunting than it used to be.