|
|
|
|
|
|
SESSION_SECRET=$(openssl rand -hex 32) docker run -d \
-p 3003:3003 \
-e PILEO_SESSION_SECRET=$SESSION_SECRET \
-v ./data:/app/data -v ./uploads:/app/uploads \
mauriceboe/pileoThen open http://localhost:3003 and sign up. The first registered user becomes the admin.
Full compose example with secure defaults
services:
pileo:
image: mauriceboe/pileo:latest
container_name: pileo
read_only: true
security_opt:
- no-new-privileges:true
cap_drop:
- ALL
cap_add:
- CHOWN
- SETUID
- SETGID
tmpfs:
- /tmp:noexec,nosuid,size=64m
ports:
- "3003:3003"
environment:
- PILEO_NODE_ENV=production
- PILEO_HOST=0.0.0.0
- PILEO_PORT=3003
- PILEO_SESSION_SECRET=${PILEO_SESSION_SECRET:?Generate with: openssl rand -hex 32}
- PILEO_DB_PATH=/app/data/pileo.db
- PILEO_UPLOAD_DIR=/app/data/uploads
- TZ=${TZ:-UTC}
- LOG_LEVEL=${LOG_LEVEL:-info}
- PILEO_ALLOWED_ORIGINS=${PILEO_ALLOWED_ORIGINS:-}
- PILEO_APP_URL=${PILEO_APP_URL:-}
# Behind a TLS-terminating reverse proxy:
# - PILEO_TRUST_PROXY=1
# - PILEO_FORCE_HTTPS=true
volumes:
- ./data:/app/data
- ./uploads:/app/uploads
restart: unless-stopped
healthcheck:
test: ["CMD", "wget", "-qO-", "http://localhost:3003/api/health"]
interval: 30s
timeout: 10s
retries: 3
start_period: 15sThen:
echo "PILEO_SESSION_SECRET=$(openssl rand -hex 32)" > .env
docker compose up -dHTTPS notes: PILEO_FORCE_HTTPS=true is optional — it adds a 301 redirect, HSTS, CSP upgrade-insecure-requests, and forces the secure cookie flag. Only use it behind a TLS-terminating reverse proxy. PILEO_TRUST_PROXY=1 tells Express how many proxies sit in front so real client IPs and X-Forwarded-Proto work.
A pre-baked starting point is committed as compose.example.yml.
- Server NestJS 11 on Node 20, single bootstrap, no legacy Express stack.
better-sqlite3with Drizzle for typed DB access; the OAuth and settings tables use raw SQL where the JSON-in-TEXT model is more ergonomic. Real-time overws. Logs through Pino. Auth viaexpress-session; OAuth 2.1 + RFC 7591 dynamic registration for the MCP connector. - Client React 19 + Vite 6, Zustand for global state,
react-hook-form+zodResolverfor forms, TipTap for rich text,@dnd-kitfor drag-and-drop,lucide-reactfor icons. All forms validate against the same Zod schemas the server uses. - Shared A single
@pileo/sharedpackage exports the Zod schemas + their inferred TypeScript types + the API-response shapes. The client and server both consume it — no schema drift between wire format and code.
┌─────────────────┐ ┌──────────────────────┐
│ React 19 SPA │ ──REST──> │ NestJS controllers │
│ (Vite, Zustand)│ ──WS─────>│ + WebSocket server │
└─────────────────┘ └──────────────────────┘
│ │
└──────── @pileo/shared ───────┘
Zod schemas + response types
│
┌───────────┴────────────┐
│ SQLite (better-sqlite3) │
└────────────────────────┘
- The client never duplicates a Zod schema. Every payload it sends is validated against a schema exported from
@pileo/shared, every response is typed by the matching API-response shape from the same package. - The server uses the same schemas in NestJS controllers via a
ZodValidationPipe. Wire-contract errors come back in a consistent{ error: { code, message, details } }envelope. - The WebSocket layer carries presence + reactive invalidations (
task.created,column.moved, …). The client refetches the affected query rather than mutating local state from the message.
Pileo ships an authenticated Model Context Protocol endpoint. Any compliant client (claude.ai, Claude Code, the SDK) can:
- Discover it via
/.well-known/oauth-authorization-server - Auto-register as a public OAuth client (no admin approval)
- Complete the PKCE flow with a Pileo-hosted consent page
- Call any of the 27 tools — projects, boards, tasks, labels, custom fields, members, checklists, comments
Example .mcp.json for Claude Code:
{
"mcpServers": {
"pileo": {
"type": "http",
"url": "https://your-pileo.example.com/api/v1/mcp"
}
}
}For server-side automation, mint a per-project API key in Project Settings → API Access and pass it as Authorization: Bearer pil_....
Pileo uses a single-digit rollover scheme:
0.1.0 → 0.1.1 → … → 0.1.9 → 0.2.0
0.2.0 → … → 0.9.0 → 1.0.0
1.0.0 → 1.0.1 → …
Every push to main automatically bumps the patch (or rolls into minor / major as needed), commits the bump as chore(release): vX.Y.Z, tags the commit, and publishes mauriceboe/pileo:X.Y.Z + :latest to Docker Hub.
The local CLI for the same bump:
node scripts/bump-version.mjs# Install (workspaces: client, server, packages/shared)
npm install
# Run server (port 3000) and client (port 5173 with /api proxy)
npm run dev --workspace=@pileo/server
npm run dev --workspace=@pileo/clientTests:
npm test --workspace=@pileo/server
npm test --workspace=@pileo/client
npm run test:coverage --workspace=@pileo/clientBuild the production image locally:
docker build -t pileo:dev .
docker run --rm -p 3003:3003 -e PILEO_SESSION_SECRET=dev pileo:devPatches welcome. Please:
- Run both test suites locally (
npm testin each workspace) - Keep
@pileo/sharedschemas as the single source of truth — never define a Zod schema or API-response type insideclient/srcorserver/src - Follow the existing prefix style for commit messages:
feat(...),fix(...),refactor(...),test(...),chore(...)
Issues and discussions live on GitHub. For security reports, email instead of opening a public issue.
AGPL v3 — same as TREK. Self-host, modify, deploy. If you run a modified version as a network service, share your changes.