Skip to content

Repository files navigation

🗂️ Planj

A calm, self-hosted kanban planning tool built to be used alongside coding agents. Plan work on a mobile-friendly board, and let any agent plug in over MCP to read tickets, pick up work, comment, and move things along — in real time.

  • Kanban boards with user-defined columns/statuses, multiple projects, drag & drop.
  • Tickets with name, description (full markdown), assignee, priority, comments (markdown), and attachments. Drag an uncategorized card onto a group band to file it without leaving the board.
  • Customizable ticket types per project — add, rename, recolor, and pick an icon for each type from Board settings (or via MCP). Defaults to feature/bug/idea.
  • MCP server built in — agents connect with a token and get full access to tickets, comments, and attachments. When an agent claims a ticket it auto-assigns itself and moves the ticket into the project's Agent Working column.
  • Realtime + reactive — every change syncs live across devices over WebSockets, with optional browser notifications when tickets move or comments arrive.
  • Mobile-first, animated, warm, and minimal. Done tickets hide behind a per-board toggle.

Quick start (Docker)

# 1. (optional) choose a password and a stable session secret
echo "PLANJ_PASSWORD=your-password"        >  .env
echo "PLANJ_SECRET=$(openssl rand -hex 32)" >> .env

# 2. build & run
docker compose up -d --build

# 3. open the app
open http://localhost:8420

If you don't set PLANJ_PASSWORD, Planj generates one and prints it in the logs on first boot (docker compose logs planj). Data (SQLite + attachments) lives in the planj-data volume.

Local development

npm run install:all     # install server + client deps
npm run dev             # server on :8420, web app on :5173 (proxied)

Open http://localhost:5173. The Vite dev server proxies the API, WebSocket, and MCP endpoints to the server, so it behaves like one origin.

Connecting a coding agent (MCP)

  1. In the app, open Agents & MCP and create a token (this auto-registers an agent member).

  2. Point your MCP client at the Streamable HTTP endpoint with the token as a bearer:

    {
      "mcpServers": {
        "planj": {
          "url": "http://localhost:8420/mcp",
          "headers": { "Authorization": "Bearer planj_..." }
        }
      }
    }

Tools the agent gets:

  • Readwhoami, list_projects, get_project, get_board (project + columns + groups + tickets in one call), list_columns, list_groups, list_ticket_types, list_tickets, search_tickets, get_ticket, list_comments, list_members, list_activity, get_attachment.
  • Work the boardcreate_ticket, update_ticket, move_ticket (target a column by toColumnName), claim_ticket and next_ticket (grab the top unclaimed ticket), assign_ticket, release_ticket, delete_ticket, add_comment, update_comment, delete_comment, add_attachment.
  • Shape the boardcreate_project, update_project, create_group, update_group, create_ticket_type, update_ticket_type, delete_ticket_type, create_column, update_column, delete_column.

Columns can be addressed by name (columnName / toColumnName), so agents work in terms of statuses like “In Review” rather than opaque ids. Reaching a Done column clears the working flag. Each project's ticket types are customizable (add/rename/recolor/icon) from Board settings or MCP.

Configuration

Env var Default Purpose
PORT 8420 Port for the web app, API, WebSocket, and MCP.
PLANJ_PASSWORD (generated) Shared password gating human web access.
PLANJ_SECRET (persisted) Signs session cookies. If unset, one is generated and saved in the DB, so logins survive restarts. Set it to manage the value yourself.
PLANJ_PUBLIC_URL http://localhost:8420 Public URL shown in MCP connection hints (set to your https:// domain in production).
PLANJ_TRUST_PROXY 1 Reverse-proxy hops to trust for X-Forwarded-* (real client IP, HTTPS detection). 0 to disable.
PLANJ_HSTS (off) Set 1 to send HSTS — only when always served over HTTPS via the proxy.
PLANJ_CORS_ORIGINS (none) Comma-separated origin allowlist; only needed if the API is fronted from a different origin.
DATA_DIR /data (docker) Where SQLite + uploaded attachments live.

Security defaults

Planj ships hardened for an untrusted network: helmet security headers + a strict same-origin Content-Security-Policy, no cross-origin CORS by default, rate-limited login (brute-force protection on the shared password), httpOnly + SameSite=Lax + Secure (in production) session cookies with server-enforced expiry, and per-agent MCP bearer tokens that can be revoked instantly. Uploaded attachments are served nosniff + sandboxed and non-image types are forced to download, so an uploaded SVG/HTML file can't run script.

The shared password is a low-assurance gate — keep production instances behind your reverse proxy with TLS (and, ideally, on a private network / VPN / IP allowlist).

Behind a reverse proxy (Nginx Proxy Manager, etc.)

Planj is designed to run behind a TLS-terminating reverse proxy. A single Node port serves the web app, API, WebSocket, and MCP, so you only proxy one upstream (host:8420).

  • Enable WebSocket support. In Nginx Proxy Manager, turn on Websockets Support for the proxy host (raw nginx: forward Upgrade / Connection: upgrade and proxy /socket.io/). Without it the realtime/live board won't connect.
  • Forward the standard headers. NPM forwards X-Forwarded-Proto / X-Forwarded-For by default; Planj trusts one hop (PLANJ_TRUST_PROXY=1). This makes Secure cookies and the login rate limiter work correctly.
  • Set PLANJ_PUBLIC_URL to your https:// domain so MCP connection hints are right.
  • Session cookies are Secure in production, so the app must be reached over HTTPS (which the proxy provides). Optionally set PLANJ_HSTS=1 once you're sure it's always HTTPS.

Backups

Data is a single SQLite database (/data/planj.sqlite, WAL mode) plus the uploads/ folder. Back it up with SQLite's online backup (safe while running) rather than a plain cp:

docker compose exec planj sh -c "sqlite3 /data/planj.sqlite \".backup '/data/backup.sqlite'\""
# then copy /data/backup.sqlite and /data/uploads off the host

(If you copy files directly instead, stop the container first and copy planj.sqlite together with its -wal and -shm sidecars.) Planj checkpoints and closes the database cleanly on SIGTERM, so a normal docker compose stop leaves a consistent main file.

Bind-mounting PLANJ_DATA_DIR? The container runs as UID 1000, so the host directory must be writable by it: mkdir -p /your/path && sudo chown 1000:1000 /your/path before first boot. (The managed named volume needs no setup.)

Architecture

A single Node process serves everything: the React web app (built with Vite), a REST API, a Socket.IO realtime layer, and an in-process MCP server (Streamable HTTP, bearer-auth) — all backed by SQLite via better-sqlite3.

client/   Vite + React + TS + Tailwind + framer-motion   (the web app)
server/   Express + better-sqlite3 + Socket.IO + MCP SDK  (API + realtime + MCP)
DESIGN.md the design system (palette, type, motion)

See DESIGN.md for the design system.

About

Kanban for humans and AI to collaborate

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages