Self-hosted kanban for teams who own their data. Built with React, TypeScript, Express, and PostgreSQL.
getpliny.com · Self-hosting guide
- Full kanban board with drag-and-drop (desktop & mobile touch)
- Multiple views — board, calendar, list, and analytics dashboard
- Labels, assignees, due dates, checklists, and card descriptions
- Board starring and sort preferences
- Public board sharing — shareable read-only links
- CSV import — create boards from a spreadsheet in 3 steps
- User management — ADMIN and READ roles with board-level permissions
- SSO / OIDC — plug in any OIDC-compatible identity provider
- REST API with personal access tokens
- Real-time updates via WebSocket
- Mobile-responsive with touch-friendly drag handles
- Docker-ready — pulls from GHCR, no build step required
- Docker 20+
- Docker Compose v2+
git clone https://github.com/bshandley/pliny
cd pliny
cp .env.example .env
# Edit .env — set DB_PASSWORD, JWT_SECRET, PLINY_URL, and initial admin credentials
docker compose up -dOpen http://localhost:8080 (or your PLINY_URL). Create your admin account on first launch.
Images are pulled automatically from GitHub Container Registry. Migrations run automatically on startup — no extra steps.
docker compose pull && docker compose up -dMigrations run automatically on restart. That's it.
| Variable | Required | Default | Description |
|---|---|---|---|
DB_PASSWORD |
✅ | — | PostgreSQL password |
JWT_SECRET |
✅ | — | Secret for JWT signing — use a long random string |
PLINY_URL |
✅ | — | Public URL of your instance (e.g. https://pliny.example.com) |
DB_HOST |
db |
PostgreSQL host | |
DB_PORT |
5432 |
PostgreSQL port | |
DB_NAME |
pliny |
Database name | |
DB_USER |
pliny |
Database user | |
PORT |
3001 |
Backend API port | |
INITIAL_ADMIN_USERNAME |
admin |
Username for the initial admin account (only used when no users exist) | |
INITIAL_ADMIN_PASSWORD |
— | Password for the initial admin account | |
SMTP_HOST |
— | SMTP host for email notifications | |
SMTP_PORT |
587 |
SMTP port | |
SMTP_USER |
— | SMTP username | |
SMTP_PASS |
— | SMTP password | |
SMTP_FROM |
— | From address for outbound email | |
S3_ENDPOINT |
— | S3-compatible storage endpoint (uses local disk by default) | |
S3_BUCKET |
— | S3 bucket name | |
S3_ACCESS_KEY |
— | S3 access key | |
S3_SECRET_KEY |
— | S3 secret key | |
OIDC_ISSUER |
— | OIDC provider URL for SSO | |
OIDC_CLIENT_ID |
— | OIDC client ID | |
OIDC_CLIENT_SECRET |
— | OIDC client secret | |
TOTP_ENCRYPTION_KEY |
— | Encryption key for TOTP secrets |
Point your reverse proxy to port 8080 (the client container). WebSocket support is required — forward the Upgrade header.
nginx example:
location / {
proxy_pass http://localhost:8080;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
}Works with nginx, Caddy, Traefik, or any WebSocket-capable proxy.
pliny/
├── server/ # Express + TypeScript backend
│ ├── src/
│ │ ├── routes/ # API routes
│ │ ├── middleware/ # Auth & RBAC
│ │ ├── migrations/ # Database schema (idempotent)
│ │ └── index.ts # Server entry + WebSocket
│ └── Dockerfile
├── client/ # React + TypeScript frontend
│ ├── src/
│ │ ├── components/ # UI components
│ │ ├── api.ts # API client
│ │ └── App.tsx # App root
│ ├── Dockerfile
│ └── nginx.conf
└── docker-compose.yml
All endpoints require a Bearer token (JWT from login, or a personal access token).
| Method | Path | Description |
|---|---|---|
POST |
/api/auth/login |
Login — returns JWT |
POST |
/api/auth/register |
Register new user (ADMIN only) |
| Method | Path | Description |
|---|---|---|
GET |
/api/boards |
List boards |
POST |
/api/boards |
Create board (ADMIN) |
GET |
/api/boards/:id |
Get board with columns & cards |
PUT |
/api/boards/:id |
Update board (ADMIN) |
DELETE |
/api/boards/:id |
Delete board (ADMIN) |
PUT |
/api/boards/:id/star |
Star/unstar board |
GET |
/api/boards/:id/analytics |
Board analytics (activity, assignees) |
| Method | Path | Description |
|---|---|---|
GET |
/api/boards/:id/members |
List members |
POST |
/api/boards/:id/members |
Add member |
DELETE |
/api/boards/:id/members/:userId |
Remove member |
| Method | Path | Description |
|---|---|---|
POST |
/api/columns |
Create column (ADMIN) |
PUT |
/api/columns/:id |
Update column (ADMIN) |
DELETE |
/api/columns/:id |
Delete column (ADMIN) |
| Method | Path | Description |
|---|---|---|
POST |
/api/cards |
Create card (ADMIN) |
GET |
/api/cards/:id |
Get card detail |
PUT |
/api/cards/:id |
Update card (ADMIN) |
DELETE |
/api/cards/:id |
Delete card (ADMIN) |
| Method | Path | Description |
|---|---|---|
POST |
/api/csv/board-import/preview |
Preview CSV import |
POST |
/api/csv/board-import/confirm |
Confirm and create board from CSV |
| Method | Path | Description |
|---|---|---|
GET |
/api/users |
List users (ADMIN) |
PUT |
/api/users/:id |
Update user (ADMIN) |
DELETE |
/api/users/:id |
Delete user (ADMIN) |
GET |
/api/boards/admin/shared |
List all publicly shared boards (ADMIN) |
- ADMIN — Full access. Manages users, boards, members, columns, and cards.
- READ — View-only. Sees only boards they've been added to. Cannot modify anything.
Backend:
cd server
npm install
npm run dev # starts with ts-node-dev
npm run migrate # run migrations against local DBFrontend:
cd client
npm install
npm run dev # Vite dev server on :5173Database:
docker run -d -p 5432:5432 \
-e POSTGRES_DB=pliny \
-e POSTGRES_USER=pliny \
-e POSTGRES_PASSWORD=changeme \
postgres:16Elastic License 2.0 — free to self-host and modify; commercial redistribution and managed service offerings require a separate agreement.