Skip to content

Repository files navigation

hashboard

Self-hosted, agent-native kanban board + markdown document workspace. Cards carry full-markdown descriptions, standalone documents live alongside boards, and AI agents are first-class principals — same API, same attribution, own identity. See CLAUDE.md for the architecture contract.

Running

npm install
npm run dev                           # dev server

npm run build                         # production build → build/
node --env-file=.env build/index.js   # run it

The app applies pending migrations from drizzle/ on boot, dev and production alike; npm run db:migrate applies them without starting the app.

Configuration lives in .env (see .env.example). Sign-in is OIDC (any provider, e.g. Authentik) and/or local username/password — on a fresh instance the first registered user becomes the superadmin, after which local registration is closed until an admin opens it (PATCH /api/v1/admin/settings).

API reference: /api/docs (interactive), /api/v1/openapi.json (spec), /llms.txt (agent-oriented overview).

Deploying behind a reverse proxy

Two env vars matter when hashboard sits behind a proxy (NPMplus, nginx, …):

  • ORIGIN — the public origin (https://hashboard.example.com). adapter-node cannot infer it, and the OIDC redirect_uri is built from it.
  • ADDRESS_HEADER=X-Forwarded-For (plus XFF_DEPTH=1 for a single trusted proxy) — without it, adapter-node sees every request as coming from the proxy's IP. The login/register rate limiter keys on client IP, so leaving this unset behind a proxy collapses it into one shared bucket: ~10 register attempts per 15 minutes for all visitors combined, and one abusive client can lock out everyone. Set it in production, always.

TLS is mandatory in production. The production build sets Secure on the session cookie, so over plain HTTP on any non-localhost address the browser accepts the cookie and then never sends it back — sign-in appears to do nothing, with no error in the UI or the server log. If sign-in "does nothing" on a LAN address, this is why: put TLS in front or test on localhost.

Supervision (pm2)

ecosystem.config.cjs runs the production build under pm2 — restart on crash, start on boot:

npm ci && npm run build
pm2 start ecosystem.config.cjs
pm2 save          # persist across reboots (once: pm2 startup)

Never run it in pm2 cluster mode: SQLite has one writer and the app is designed as a single process. /api/v1/health is unauthenticated by design and is the healthcheck target.

A deploy is build/ plus package.json, package-lock.json, drizzle/, scripts/migrate.mjs and an npm ci --omit=dev on the target — migration uses drizzle-orm's runtime migrator (at boot, and in npm run db:migrate), so production installs apply their own migrations without dev dependencies.

Docker

The alternative to pm2 — same single-process app, database on a named volume, migrations applied on boot:

docker compose up -d --build

Uncomment and fill ORIGIN (and the reverse-proxy vars above) in compose.yaml first. The port is published on localhost only, because TLS and the public hostname belong to the reverse proxy in front; never scale the service past one replica — same single-writer rule as pm2.

The backup guidance below applies unchanged; from a live container:

docker compose exec hashboard node -e "new (require('better-sqlite3'))(process.env.DATABASE_URL).exec(\"VACUUM INTO '/data/backup.db'\")"

Backups

A backup is two things: the database and the attachment files. Attachment bytes are stored on disk under ATTACHMENTS_DIR, not in the database, so a snapshot of one without the other restores an instance whose download links all fail. The Docker image puts both under /data precisely so that backing up one directory is complete.

The database is a single SQLite file in WAL mode, which means a plain copy of a live database is not safe — committed pages can still sit in the -wal sidecar, so the copy may be stale or torn. Two safe forms:

# live, no downtime, one consistent file out
sqlite3 hashboard.db "VACUUM INTO '/backups/hashboard-$(date +%F).db'"

# or stop the process and copy all three files together
cp hashboard.db hashboard.db-wal hashboard.db-shm /backups/

Then the files, which are immutable once written — a plain recursive copy is safe at any time, and only ever gains entries:

rsync -a data/attachments/ /backups/attachments/

Take the database snapshot first. In that order the worst case is a file with no row pointing at it, which is invisible; the reverse leaves rows promising bytes the backup does not contain.

About

Self-hosted kanban + markdown workspace where AI agents are first-class users. Every URL serves HTML, Markdown or JSON; REST API and MCP server built in. SvelteKit + SQLite, single process.

Topics

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages