- Go 49.6%
- Svelte 28.5%
- TypeScript 17.6%
- CSS 2.5%
- Dockerfile 0.7%
- Other 1.1%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
Three-stage Dockerfile (SPA build, CGO-free Go build with the embedweb tag, alpine runtime with CA certs) whose entrypoint chowns the bind- mounted /data and drops to an unprivileged user — bind mounts stay the backup story. Compose maps ./data and reads an optional .env. README covers the no-credit-card Brevo SMTP setup with SPF/DKIM deliverability notes, Unsplash keys, live backups, the theme-scheme recipe, and the complete API reference for external apps. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VvxAC6h2E7wxjhLXStXAQY |
||
| server | ||
| web | ||
| .env.example | ||
| .gitignore | ||
| docker-compose.yml | ||
| docker-entrypoint.sh | ||
| Dockerfile | ||
| LICENSE | ||
| Makefile | ||
| README.md | ||
Bread
A self-hosted kanban board that stays out of your way. Svelte 5 + Go + SQLite, shipped as a single container. Boards sync live between everyone looking at them; your data is one directory you can copy to back up.
- Boards with unlimited columns and cards, drag-and-drop for both (with a dedicated touch flow), and an owner-set lock that freezes column structure.
- Cards carry a title, markdown description, image attachments, and per-board custom properties (text, number, select, checkbox, date).
- Sharing: invite people by email; every move appears for everyone instantly over WebSockets.
- Backgrounds: built-in colors, or Unsplash photos with author credit.
- Two color schemes — Bakery and Porcelain — each with light and dark modes, chosen per account. All colors are CSS tokens in one file, so new schemes are one CSS block away.
- API-first: everything the UI does is a documented JSON API; personal access tokens let you build external apps against it.
Run it
git clone <this repo> bread && cd bread
cp .env.example .env # optional; empty works for a local try-out
docker compose up --build
Open http://localhost:8080, create an account, and you have a board. All
state lives in ./data (bread.db + uploads/).
Without SMTP configured, Bread prints outgoing email (verification links,
invites) to the container log — docker compose logs -f bread — which is
fine for trying it out and useless for production.
Configuration
Everything is environment variables, read from .env (never committed — the
repo ignores it). See .env.example for the full annotated
list. The short version:
| Variable | What it does |
|---|---|
PORT |
Listen port (default 8080) |
DATA_DIR |
Where SQLite + uploads live (default ./data, /data in Docker) |
APP_URL |
Public URL used inside emails |
SECURE_COOKIES |
true behind HTTPS |
SMTP_HOST/PORT/USERNAME/PASSWORD/FROM |
Any SMTP provider |
UNSPLASH_ACCESS_KEY |
Enables photo backgrounds |
Email that doesn't land in spam (and needs no credit card)
Bread speaks plain SMTP, so any provider works. A good free start is Brevo — 300 emails/day, no card required:
- Create a Brevo account and, under SMTP & API → SMTP, generate an SMTP key.
- Put it in
.env:SMTP_HOST=smtp-relay.brevo.com SMTP_PORT=587 SMTP_USERNAME=<your brevo login email> SMTP_PASSWORD=<the smtp key> SMTP_FROM=Bread <bread@yourdomain.com> APP_URL=https://bread.yourdomain.com - Deliverability: authenticate your sending domain in Brevo (they walk you through adding SPF and DKIM DNS records). This is the part that keeps verification emails out of spam — do it before inviting real people. Sending from a domain you own beats sending from a freemail address.
Bread's emails are deliberately plain text with a single link — the format spam filters distrust least.
Unsplash backgrounds
Create a free app at unsplash.com/developers
(no card) and set UNSPLASH_ACCESS_KEY. The key stays on the server; the
frontend only talks to Bread's proxy. Photos show author attribution on the
board, and selection fires the download event the Unsplash terms require.
Without a key, the color backgrounds still work.
Backups
DATA_DIR is the whole story: bread.db (SQLite, WAL mode) plus uploads/.
# Consistent snapshot while the app is running:
sqlite3 data/bread.db ".backup data-backup/bread.db"
cp -r data/uploads data-backup/uploads
Or stop the container and copy data/ wholesale. Restoring is copying it
back. For inspection: sqlite3 data/bread.db .tables.
Development
Requirements: Go 1.27+, Node 22+.
make dev-server # Go API on :8080 (console email driver)
make dev-web # SvelteKit dev server on :5173, proxies /api
make test # Go + Vitest suites
make build # single binary with the SPA embedded -> bin/bread
The repo layout: server/ is the Go module (config, sqlite+migrations,
auth, http handlers, websocket hub, embedded SPA), web/ is the SvelteKit
SPA (theme tokens in src/app.css, board logic in src/lib/board/).
Adding a color scheme
- Add
[data-scheme='yours']and[data-scheme='yours'][data-mode='dark']blocks inweb/src/app.cssdefining the same token set. - Register it in
schemesinweb/src/lib/theme.svelte.ts.
That's it — the settings page picks it up, and the server stores any scheme name without needing changes.
API
Base URL /api/v1. Authenticate with the session cookie (browser) or a
personal access token created under Settings → API tokens:
curl -H "Authorization: Bearer brd_..." https://your-bread/api/v1/projects
Tokens are shown once and stored hashed. Errors are {"error": "message"}
with a fitting status code.
| Method & path | Purpose |
|---|---|
POST /auth/register {email,password,displayName} |
Create account (starts a session) |
POST /auth/login / POST /auth/logout |
Session in / out |
GET /auth/me / PATCH /auth/me |
Profile incl. themeScheme, themeMode |
POST /auth/verify {token} · POST /auth/forgot {email} · POST /auth/reset {token,password} |
Email flows |
GET/POST /tokens · DELETE /tokens/{id} |
Personal access tokens |
GET/POST /projects |
List / create boards |
GET /projects/{id} |
Whole board: project, columns, cards, propertyDefs, members |
PATCH /projects/{id} {name?,isLocked?} · DELETE /projects/{id} |
Update / delete |
POST /projects/{id}/background {type:"color"|"unsplash", value} |
Set background |
GET /projects/{id}/members · POST /projects/{id}/invites {email} · DELETE /projects/{id}/members/{userId} |
People |
POST /invites/accept {token} |
Join a board from an invite email |
POST /projects/{id}/columns {name} · PATCH /columns/{id} · DELETE /columns/{id} |
Columns (locked boards refuse create/delete) |
POST /columns/{id}/move {index} |
Reorder (refused when locked) |
POST /columns/{id}/cards {title} |
Create card |
PATCH /cards/{id} {title?,description?,properties?} · DELETE /cards/{id} |
Update / delete card |
POST /cards/{id}/move {toColumnId,index} |
Move card |
POST /projects/{id}/properties {name,kind,options?} · PATCH /properties/{id} · DELETE /properties/{id} |
Custom properties |
POST /cards/{id}/attachments (multipart file) · GET/DELETE /attachments/{id} |
Images (PNG/JPEG/GIF/WebP ≤ 10 MB) |
GET /unsplash/search?q= · POST /unsplash/track-download |
Background photos |
GET /ws?project={id} |
WebSocket of board events (?token= for PATs) |
index in move calls counts the target list without the moving item.
Realtime events are {type, projectId, originId, actorId, payload} where
type is e.g. card.moved, column.created, project.updated — send an
X-Bread-Origin header with your mutations and skip events echoing your own
originId.
Security notes
Passwords are argon2id with per-user salts; sessions, API tokens, and email
tokens are stored as SHA-256 digests; login timing doesn't reveal whether an
account exists; attachments are content-sniffed (SVG rejected) and served
auth-checked with nosniff; board ids return 404 rather than 403 to
non-members.
License
MIT — see LICENSE.