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_passhttp://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.