A small, self-hosted task board that Claude can read and write.
Projects are columns, tasks are cards. Each card has a subject, a body, who it's assigned to, who created it, a due date, a priority, and a running list of notes. People add tasks on the site or straight from a Claude conversation, and everyone on a project sees the same board.
Node 22 · Express · EJS · PostgreSQL · Docker. One container plus Postgres; no build step.
- Board - one column per project you belong to, cards sorted by due date then priority. Overdue and due-today cards are flagged; "Assigned to me" and "Show done" toggles. Quick-add box at the bottom of every column.
- Cards - subject, body, assignee, creator, due date, priority, notes. Every reassignment, due-date change, done/reopen and move is recorded on the card with who did it and when.
- Projects - whoever creates a project owns it and adds people by email. Adding someone who has never signed in creates their account, so an invite is all a new person needs. Members see only their projects; site admins see all. Owners can transfer ownership, remove members, and delete the project.
- Claude - the site is also an MCP server with its own OAuth 2.1
authorization server. Each person adds it once as a custom connector in Claude (web, desktop, mobile, or Claude
Code), approves it, and from then on Claude can list, add, assign, note, complete and move tasks as that person.
The
/connectpage in the app has the exact steps. - Sign-in - no passwords. Email one-time codes, passkeys (Windows Hello, Face ID, security keys), Google, and
Microsoft (personal and work/school accounts). Only people on the allowlist (
users) can get in; project owners add to it when they add members, admins can edit it at/admin/users.
git clone https://github.com/tylamb1/task && cd task
cp .env.example .env # set SITE_URL, BOOTSTRAP_ADMIN_EMAIL, and (for real email) RESEND_API_KEY
docker compose up -dOpen SITE_URL (default http://localhost:3000). The first sign-in as BOOTSTRAP_ADMIN_EMAIL becomes the admin: use
"Email me a code" - without RESEND_API_KEY the code is printed to the container log (docker compose logs web).
For anything public put a TLS proxy (Caddy, Traefik, nginx, Dokploy...) in front of port 3000. Passkeys, Google and
Microsoft sign-in, and Claude's connector all require https, and SITE_URL must be the public https address.
npm install
cp .env.example .env
docker compose up db -d # Postgres on localhost:5432
set -a; . ./.env; set +a # or use a .env loader of your choice
npm run dev # http://localhost:3000, restarts on file changesMigrations in migrations/*.sql run automatically at startup, once each, in filename order.
npm test # pages that need no database
DATABASE_URL=postgres://task:task@localhost:5432/task npm test # plus the database, OAuth and MCP testsThe database tests truncate the tables they use; point them at a throwaway database.
Everything is an environment variable; .env.example documents each one.
| Variable | Purpose |
|---|---|
DATABASE_URL |
Postgres connection string |
SITE_URL |
Public origin, e.g. https://task.example.com. Drives cookies, passkeys, OAuth redirect URIs and the MCP URL |
APP_NAME |
Name shown in the header, emails and to Claude (default Task) |
TZ |
Time zone for "today" and "overdue" |
BOOTSTRAP_ADMIN_EMAIL |
Becomes the first admin when the users table is empty |
RESEND_API_KEY, MAIL_FROM |
Outbound email via Resend. Blank = print to console |
GOOGLE_CLIENT_ID/SECRET |
Enables "Continue with Google". Redirect URI: SITE_URL/auth/google/callback |
MICROSOFT_CLIENT_ID/SECRET |
Enables "Continue with Microsoft". Redirect URI: SITE_URL/auth/microsoft/callback. Register the app for "any organizational directory and personal Microsoft accounts"; set MICROSOFT_TENANT to a tenant id to restrict it |
Running two copies (say one for home and one for work) is two deployments of the same image with different .env
files and databases.
Signed-in people get the steps at /connect. In short: Settings → Connectors → Add custom connector, paste
SITE_URL/mcp, leave the client id/secret blank, click Connect, sign in, click Allow. For Claude Code:
claude mcp add --transport http task SITE_URL/mcp then /mcp.
How it works: /.well-known/oauth-protected-resource/mcp points Claude at this site's authorization server
(/.well-known/oauth-authorization-server). Claude registers itself (/register, RFC 7591), sends the person to
/authorize (PKCE, S256), the person signs in and approves, Claude exchanges the code at /token, and calls /mcp
with the bearer token. Access tokens last an hour, refresh tokens 90 days (sliding). Tokens are stored hashed. People
can see and disconnect their connections on /connect.
Tools: list_projects, list_tasks, get_task, add_task, update_task, complete_task, reopen_task,
add_note, delete_task, create_project, add_project_member. Projects are referred to by name, tasks by #id,
people by name or email, and "me" is the signed-in person.
A useful line in a Claude Project's instructions: "Tasks for this project go in the Task project called Condo."
server.js starts the app (runs migrations first, creates the bootstrap admin)
src/app.js Express setup and route order (sessions → OAuth/MCP → same-origin check → browser routes)
src/db.js shared pg pool -> db.query(sql, params)
src/migrate.js applies migrations/*.sql once each, in order
src/render.js res.page(view, locals) renders a view inside views/layout.ejs
src/auth/ sign-in: email codes, passkeys, Google/Microsoft, sessions, CSRF guard
src/data/ users, projects (membership), tasks (cards and notes) - all SQL lives here
src/routes/ board, projects, tasks, connect pages
src/oauth/ the authorization server (provider.js) and the consent page
src/mcp/ the MCP endpoint and its tools
src/admin.js /admin/users
migrations/ 001 auth tables, 002 projects/tasks/notes, 003 OAuth clients/codes/tokens
views/, public/ EJS templates and static files (one stylesheet, no front-end build)
test/ node --test; database tests skip unless DATABASE_URL is set
- Create a Postgres service; copy its internal connection string.
- Create an Application from this repo, build type Dockerfile, env from
.env.examplewithDATABASE_URLfrom step 1 andSITE_URL=https://your.domain. - Add the domain with HTTPS on, container port 3000. Deploy. Check
https://your.domain/healthz.
Every push to main then redeploys.
MIT