Free & open source forever. No subscriptions. No per-seat pricing. No limits.
A real-time Kanban board you can deploy in seconds. One binary, one command. Built with Go and Vite for a 20 MB memory footprint.
Try it — drag cards between columns








Find any card across all boards in milliseconds. Results appear as you type — no waiting, no page reloads.
Group cards by epic to see progress across workstreams at a glance. Collapse lanes, drag between them, track counts per column.











Switch to list view for a dense overview. Filter by assignee, priority, tag, or search across all cards.




No bloat, no electron, no SaaS lock-in. Just a fast, self-hosted kanban board that runs anywhere.
Server-Sent Events push card updates, comments, and presence to every connected client instantly. No polling, no WebSocket complexity.
Move cards between columns with native HTML5 drag-and-drop. Reorder within columns. Optimistic UI with automatic rollback on conflict.
Four roles — Owner, Admin, Member, Viewer — with enforced permissions. Invite teammates, manage roles, control who can edit.
JWT auth with token rotation. Bcrypt password hashing. Rate limiting. CORS. Security headers. Parameterized queries. No shortcuts.
Subscribe to card and comment events per-board. HMAC-SHA256 signed payloads, automatic retries with exponential backoff, delivery history.
Desktop columns, tablet scroll, mobile single-column with tabs. Touch-friendly 44px+ targets. Dark and light themes with accent colors.
Multiple boards with custom project keys. Configurable columns. Dark/light themes. Accent colors. Density and font size options. Make it yours.
Threaded comments on every card with real-time updates. Track reporters, assignees, due dates, story points, priorities, and tags.
Full CRUD API for boards, cards, comments, webhooks, and users. JWT auth. Search. Presence. Stream events. Build on top of it.
Use PostgreSQL in production with connection pooling. Or SQLite for single-user or development. Auto-detected from the connection URL.
Vanilla ES6+ JavaScript, embedded into the Go binary at compile time. No npm, no webpack, no build step. Edit and reload.
WCAG 2.1 AA. Keyboard navigation with arrow keys. ARIA roles and labels. Screen reader live regions. Reduced motion support.
One command. That's it.
version: "3.8"
services:
lwts:
image: ghcr.io/oceanplexian/lwts:latest
ports:
- "8080:8080"
environment:
DB_URL: postgres://lwts:lwts@db:5432/lwts
JWT_SECRET: change-me-in-production
depends_on:
- db
db:
image: postgres:16-alpine
environment:
POSTGRES_USER: lwts
POSTGRES_PASSWORD: lwts
POSTGRES_DB: lwts
volumes:
- pgdata:/var/lib/postgresql/data
volumes:
pgdata:
$ docker compose up -d
# Open http://localhost:8080
# With SQLite (simplest)
$ docker run -p 8080:8080 \
-e DB_URL=sqlite:///data/lwts.db \
-e JWT_SECRET=change-me \
-v lwts-data:/data \
ghcr.io/oceanplexian/lwts:latest
# With external PostgreSQL
$ docker run -p 8080:8080 \
-e DB_URL=postgres://user:pass@host:5432/lwts \
-e JWT_SECRET=change-me \
ghcr.io/oceanplexian/lwts:latest
# Download the latest release
$ curl -sL https://github.com/oceanplexian/lwts/releases/latest/download/lwts-$(uname -s)-$(uname -m) -o lwts
$ chmod +x lwts
# Run with SQLite (zero setup)
$ DB_URL=sqlite://lwts.db JWT_SECRET=change-me ./lwts
# Or with PostgreSQL
$ DB_URL=postgres://user:pass@localhost:5432/lwts JWT_SECRET=change-me ./lwts
Full REST API with JWT auth. Automate your workflow, build integrations, or drive your board from CI/CD.
$ curl -X POST https://lwts.example.com/api/v1/boards/1/cards \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"title": "Fix login timeout",
"priority": "high",
"tag": "bug",
"column": "todo",
"assignee_id": "usr_abc123"
}'
$ curl -N https://lwts.example.com/api/v1/boards/1/stream?token=$TOKEN
event: card_created
data: {"id":"...","title":"Fix login timeout","column":"todo","key":"LWTS-42"}
event: card_moved
data: {"id":"...","column":"in-progress","position":0,"version":2}
event: comment_added
data: {"id":"...","card_id":"...","body":"On it.","user_id":"..."}
event: user_joined
data: {"user_id":"...","username":"sam","initials":"SR"}
/api/auth/register/api/auth/login/api/auth/refresh/api/auth/me/api/v1/boards/api/v1/boards/api/v1/boards/{id}/cards/api/v1/boards/{id}/cards/api/v1/cards/{id}/move/api/v1/cards/{id}/comments/api/v1/cards/{id}/comments/api/v1/boards/{id}/stream/api/v1/boards/{id}/presence/api/v1/boards/{id}/webhooks/api/v1/search/api/v1/users/api/v1/keys/api/v1/export/api/v1/import/jira/api/v1/import/trello/healthzSubscribe to events per-board. Every delivery is HMAC-SHA256 signed so you can verify authenticity. Failed deliveries retry with exponential backoff. Full delivery history in the UI.
// Headers
X-LWTS-Event: card.completed
X-LWTS-Signature: sha256=a1b2c3...
X-LWTS-Delivery: 550e8400-e29b-41d4-a716-446655440000
// Body
{
"id": "550e8400-...",
"event": "card.completed",
"timestamp": "2025-01-15T10:30:00Z",
"board": {
"id": "board_123",
"name": "Sprint 42"
},
"data": {
"card": {
"key": "LWTS-4",
"title": "Fix SSE heartbeat timeout",
"column": "done"
}
}
}
Simple, auditable, no magic.
go:embednet/http — no router libslog)