rgboards/rg-boards

Lanes, team task boards you run from the web or from Claude/ChatGPT (MCP). FastAPI + DataK3 + appid + Dodil Models.

A remix template · 0 remixes · Remix rgboards/rg-boards

README.md

Lanes: team task boards on Dodil

Lanes is a small team task board (boards → lists → cards) that runs entirely on Dodil: DataK3 stores everything, an appid user pool signs people in, Ignite runs it, Dodil Models does the AI, and Dodil Git CI deploys it. The same board works from a web page with drag and drop, and from Claude or ChatGPT over MCP.

Someone says "I need a Trello". You say "remix this, it's yours":

dodil git remix rgboards/rg-boards --to-org <your-org> --name <your-repo>   # name ≤ 18 chars
dodil ignite app get <your-repo>          # ~5 min later: deployed; open public_urls[0], sign in

The one post-remix step (checking the sign-in callback) is in After the remix.

The Product Launch board, signed in as the owner


How to judge this in 60 seconds

Live: https://rg-boards-rgboards-8080.ignite.dodil.cloud · MCP: https://rg-boards-rgboards-8080.ignite.dodil.cloud/mcp

Demo login (password LanesDemo-2026! for all three)What it shows
demo.owner@lanes-demo.devOwner of Product Launch and Ops, member of Website Refresh. Manages members.
demo.member@lanes-demo.devMember on Product Launch and Ops (can edit), owner of Website Refresh.
demo.viewer@lanes-demo.devViewer on Product Launch and Website Refresh (read only) and not on Ops at all.
  1. Open the live URL. The signed-out page lists these logins. Sign in as the viewer: the board says Viewer · read only, cards don't drag, there is no "add card", a card opens read-only with a lock note, and Ops is missing from the sidebar. (Same rules hold through MCP and the JSON API: every write answers 403, every board you're not on 404.)
  2. Sign out, sign in as the owner. Product Launch shows red Overdue cards, amber Blocked by #n cards and a stats strip; the sidebar has My cards, Overdue and Blocked across every board. Drag a card to another list (a teal line shows where it lands), then open it: the History timeline shows who moved it and when, and the seeded history reads like a working week. Members lets the owner invite, change roles and remove people.
  3. Open #3 "Ship v1 to production" → "What is blocking this?" It walks the chain (#3 ← #2 QA ← #9 mobile login bug, and #10 security review) and says where to start.
  4. AI extras: in a card, ✦ Turn into checklist and ✦ Find cards like this; on the board, ✦ Daily summary (Dodil Models: kimi-k2.6, jina-embeddings-v4).
  5. Claude or ChatGPT (MCP):
    • Claude (claude.ai): Settings → Connectors → Add custom connector → URL https://rg-boards-rgboards-8080.ignite.dodil.cloud/mcp. Under Advanced settings put OAuth Client ID lanes-ai (a first-party client already approved on the demo pool, so there is no wait for approval). Connect, sign in with a demo login.
    • ChatGPT: Settings → Connectors → Create (developer mode) → the same URL, OAuth, client ID lanes-ai.
    • Then ask:
      • "What's on my plate this week?" (as the member)
      • "Add a card 'Send invoice' to Doing on the Ops board"
      • "Move card 12 to Done on Product Launch"
      • "What's overdue?" · "What is blocking the launch?" (cards can be named by title)
      • As the viewer: "Move card 3 to Done" → refused: viewers can't edit. "Show the Ops board" → no such board.

The pod scales to zero when idle (about 5 minutes without traffic). The first request after a quiet spell starts a pod, which is mostly Ignite pulling the image; give it up to ~15 seconds once, then everything answers in well under a second.

Signed out: what Lanes is and the demo logins

Every requirement, and where it lives

Brief itemStatusWhere
People: owner invites members; roles owner / member / viewer✅domain/model.py (Board.add_member, require_owner), use_cases.add_member; web Members dialog; MCP add_member, remove_member, members
Viewer can't edit✅Board.require_edit on every write use case; UI disables editing; test test_viewer_can_read_but_never_edit, both doors in test_web_and_mcp_doors_share_the_rules
Only the owner manages members✅Board.require_owner; test test_only_owner_manages_members
Nobody sees boards they're not on✅use_cases._board / _card / _visible_cards (404, not 403); test test_stranger_sees_nothing; demo: the viewer can't see Ops
Boards → lists → cards with title, description, assignee, due date, labels✅domain/model.py (Board, Card), use_cases.add_card / update_card / assign_card; test test_card_fields_and_history
Cards move between lists, drag and drop on the web✅use_cases.move_card (with before_card for order); static/index.html drag handlers; test test_drag_and_drop_order
History: who, when, what changed✅activity table; use_cases._log_changes (field, old → new) on every change; card History and board History in the UI; MCP card_history, board_activity
MCP with real actions, scoped to the signed-in person✅doors/mcp.py: 29 action tools on the same use cases, with read-only / destructive annotations; identity from the gateway; /mcp answers 401 + RFC 9728 metadata to start sign-in (main.py)
"What's on my plate this week?"✅my_cards(days=7)
"Add a card 'Send invoice' to Doing on the Ops board"✅add_card(board="Ops", title=..., list_name="Doing") (boards by title, lists case-insensitive)
"Move card 12 to Done"✅per-board card numbers; move_card(card=12, list_name="Done")
"What's overdue?"✅overdue()
Web page that does the same things✅doors/web.py + doors/static/index.html (vanilla JS, no build step)
Extra: card → checklist (AI)✅make_checklist / suggest_checklist (Dodil Models kimi-k2.6), tickable items
Extra: find cards like this (by meaning)✅find_similar (jina-embeddings-v4, vectors cached per card in DataK3)
Extra: daily board summary✅daily_summary, written in the background, stored per board per day
Extra: "blocked by" links + "what is blocking the launch?"✅add_blocker (loops refused), what_blocks walks the chain and says where to start; amber Blocked by #n on cards
Extra: overdue flagged automatically✅computed on every read; red Overdue chip and stripe, sidebar badges, overdue tool
Rule 1 public Dodil Git repo, MIT, topics dodil-challenge, dodil-challenge-boards✅this repo, LICENSE
Rule 2 Dodil services only (DataK3, appid, Ignite, Dodil Models)✅infrastructure/dodil.py; no other API keys or databases
Rule 3 deployed by Dodil Git CI to Ignite✅.github/workflows/ci.yml (test → build) + .dodil/deploy.yaml
Rule 4 one MCP endpoint, tools are actions✅/mcp, no table CRUD
Rule 5 remixable, valid .dodil/remix.yaml✅.dodil/remix.yaml + schema.sql; dodil git remix check rgboards/rg-boards → valid
Rule 6 own name and design✅"Lanes", own logo and UI
Tests for the main rules✅tests/test_boards.py + tests/test_edges.py (57 tests: every write as a viewer on both doors, every read as a stranger, bad input, races, AI outage, history of every change type; no Dodil account needed)
Docs: README + design notes✅this file, docs/DESIGN.md

What it does

AreaWhat you get
PeopleThe board's creator is its owner and invites people as member or viewer. Viewers read, never edit. Only the owner manages members. Removing someone unassigns their cards.
BoardsBoards → lists (default To do / Doing / Done, add your own) → cards. A card has a number (#12), title, description, assignee, due date and labels. Drag cards between and within lists. Archive instead of delete, so history survives.
HistoryEvery change is recorded: who, when, which field, old → new. Per card and per board.
MCP29 action tools (add_card, move_card, assign_card, my_cards, overdue, what_blocks, …), never table access. Every tool runs as the signed-in person, so the AI only sees that person's boards.
WebThe same actions as a single page: boards sidebar, kanban columns, card editor, members, history, search.
AI extrasTurn a card into a checklist (saved, tickable), find cards like this one (search by meaning with jina-embeddings-v4, cached per card), daily board summary (kimi-k2.6, written once a day and kept).
Dependencies"Blocked by" links between cards (loops refused). what_blocks walks the whole chain and says which open card to start with: "what is blocking the launch?".
OverdueEvery card past its due date and not in Done is flagged on the board, counted in the sidebar, and listed by overdue / What's overdue?.

A card: checklist, blockers and "what is blocking this?", fields on the side, history below

The same board as the viewer: read only

<img src="docs/screenshot-mobile.png" alt="On a phone: one column at a time, the board drawer behind the menu button" width="300">

Things to ask Claude or ChatGPT

  • "What's on my plate this week?" → my_cards(days=7)
  • "Add a card 'Send invoice' to Doing on the Ops board, due Friday, assign it to me" → add_card
  • "Move card 12 to Done" → move_card(card=12, list_name="Done")
  • "What's overdue?" → overdue()
  • "What is blocking the launch?" → search_cards("launch") then what_blocks
  • "Turn card 4 into a checklist" → make_checklist
  • "Find cards like #7" → find_similar
  • "Give me today's summary of the Ops board" → daily_summary
  • "Who changed card 3 and when?" → card_history
  • "Add bo@team.com to Ops as a viewer" → add_member (owner only)

Remix it (one command)

dodil update                      # CLI 0.1.89 or later
dodil git remix rgboards/rg-boards --to-org <your-org> --name <your-repo>

The remix creates in your org: the repo, a DataK3 bucket <repo>-data with every table from schema.sql, a user pool <repo>-users, and a service account (k3.editor on the bucket + ignite.model-user) whose credentials go straight into the repo's CI secrets. CI then tests, builds and deploys it to Ignite. Keep <your-repo> to 18 characters or less (the bucket name <repo>-data must stay within 25).

After the remix: sign in (the one documented step)

The remix plus its first CI run take about 4-6 minutes. Then, in the org you remixed into:

# 1. Wait for the first deploy and get the app's URL (copy it from public_urls; never build it)
dodil ignite app get <your-repo>                  # deployment_state: deployed

# 2. Check the sign-in callback. The deploy registers it on the pool by itself, so this
#    normally already lists https://<host>/.dodil/auth/callback:
dodil appid settings get <your-repo>-users
#    Only if redirect_uris is empty or lacks it (settings set REPLACES the list):
dodil appid settings set <your-repo>-users --redirect-uris https://<host>/.dodil/auth/callback
  1. Open https://<host>/ and sign in. The pool allows public sign-up, so click Sign in and then Create account on the pool's sign-in page to make the first account. You land on an empty Lanes with a Create box: the board you create is yours (owner). Invite teammates from Members; they sign up with that email and see only the boards they are on.

That's it: the tables were created by the remix from schema.sql, and the app checks and adds any missing column on start-up, so there is no SQL to run.

<details><summary>If it doesn't come up</summary>
You seeWhyDo
remix fails at user_pool: pool limit reached for this plana free Dodil org holds one user pool; the org you remixed into already has oneremix into an org without a pool, or delete the unused pool, then dodil git remix resume <run-id>
the sign-in page says the redirect URI is invalidthe callback is not on the poolstep 2 above
the first page load takes 5-15 sIgnite scaled the app to zero and is starting a podwait once; it then answers in well under a second (see Cold start)
{"detail":"sign in first"} on /api/...you are not signed in (expected)open / and sign in
deploy check redsee the checks for the commitdodil git check list <your-repo> <commit-sha>; dodil-ci/deploy says whether the callback was registered
POST / self-checkdodil ignite invoke <your-repo> opens the bucket with the app's own credential and returns a row count per table
</details>

Optional: Claude / ChatGPT and demo data

  • Connector. Add a custom connector with URL https://<host>/mcp. It signs you in through the pool, then arrives pending; approve it once: dodil appid client list <your-repo>-users → dodil appid client approve <your-repo>-users <client-id>. Or skip the approval by registering a first-party client once and giving its id in the connector's Advanced settings → OAuth Client ID:
    dodil appid client register <your-repo>-users --client-id lanes-ai --yes \
      --redirect-uri https://claude.ai/api/mcp/auth_callback \
      --redirect-uri https://claude.com/api/mcp/auth_callback \
      --redirect-uri https://chatgpt.com/connector_platform_oauth_redirect
    
  • Invite instead of public sign-up: dodil appid user create <your-repo>-users <email> --roles member.
  • Demo data. Three realistic boards for three of your users (overdue cards, a blocked-by chain, checklists, a week of history), through the same use cases:
    cd app && pip install -r requirements.txt
    BUCKET=<your-repo>-data DODIL_TOKEN=<your dodil token> \
      python ../scripts/seed_demo.py --owner a@you.com --member b@you.com --viewer c@you.com
    

The signed-out page of a remix shows a "sign in / create an account" welcome; the shared demo logins appear only on the author's instance (DEMO_POOL in .dodil/deploy.yaml).

Health checks: GET /healthz (no database), and dodil ignite invoke <your-repo> (POST /), which opens the bucket with the app's own credential and returns a row count per table.


Run it locally

No Dodil account needed. In-memory store, you act as DEV_USER:

cd app && pip install -r requirements.txt pytest
BOARDS_STORE=memory DEV_USER=you@example.com uvicorn boards.main:app --port 8080
python -m pytest -q ../tests          # 57 tests: roles, visibility, both doors, edges, extras
python ../scripts/ui_check.py         # optional: the page in a real browser (Playwright):
                                      # drag and drop, editor, roles, mobile, empty states

Against your real bucket: set BUCKET=<repo>-data and either the service account (DODIL_SERVICE_ACCOUNT_ID / _SECRET) or DODIL_TOKEN=<your own dodil token>, plus DEV_USER, and drop BOARDS_STORE. DEV_USER is ignored behind the gateway, which always injects the real person.


How it is built

Light domain-driven design. The web routes and the MCP tools are two thin doors onto the same use cases, so every rule lives in one place and can't be skipped through the other door.

app/boards/
  domain/model.py              Board, Card, Role, Person and the rules: viewer can't edit,
                               only the owner manages members, assignees must be on the board,
                               no blocking loops, list names, due dates, labels
  application/use_cases.py     every action: create_board, add_card, move_card, assign_card,
                               update_card, my_cards, overdue, what_blocks, make_checklist,
                               find_similar, daily_summary, ... All reads scoped by the caller.
  application/ports.py         what the use cases need: BoardRepository, Assistant
  infrastructure/repository.py DataK3 over the Postgres wire (sslmode=require)
  infrastructure/memory.py     the same contract in memory (tests, local demo)
  infrastructure/migrations.py additive start-up migrations (create table / add column)
  infrastructure/dodil.py      service-account token, DataK3 connection, Dodil Models
  doors/web.py                 JSON routes + the page       -> use cases
  doors/mcp.py                 MCP tools for Claude/ChatGPT -> the same use cases
  doors/identity.py            who is calling (the gateway's X-Dodil-User header)
  doors/static/index.html      the board UI (vanilla JS, no build step)
  doors/static/landing.html    the signed-out page (what it is, sign in, demo logins)
  main.py                      wires it: /mcp, sign-in metadata (RFC 9728), /healthz, POST /
scripts/seed_demo.py           realistic demo boards + a week of history, via the use cases
scripts/ui_check.py            the page in a real browser: drag and drop, roles, mobile
tests/test_boards.py           the rules through the use cases and through both doors
tests/test_edges.py            every viewer write and stranger read on both doors, bad input,
                               races, AI outage, the history of every kind of change
schema.sql                     the tables, created by the remix
.dodil/remix.yaml              what a remix creates
.dodil/deploy.yaml             how CI deploys every copy
.github/workflows/ci.yml       test -> build -> deploy (Dodil Git CD)

Design decisions: docs/DESIGN.md.

Cold start

Ignite scales the app to zero when nobody uses it, so the first request after a quiet spell starts a pod. Lanes keeps that path short:

  • Nothing slow before the pod is ready. Start-up migrations, the service-account token and the first DataK3 connections run in a background thread; /healthz and the page answer as soon as uvicorn is up. (The remix already created every table from schema.sql, so the migration only adds columns to an older bucket.)
  • A shared pool of warm DataK3 connections instead of one per worker thread, so the first page load (several parallel API calls) doesn't open five TLS connections at once.
  • A small image. Ignite pulls the image again on every cold start, so python:3.12-alpine with binary wheels only, no pip in the final image, and byte-compiled code (the pod's non-root user can't write __pycache__).
  • scaledown_period_secs: 900 in .dodil/deploy.yaml asks Ignite to keep the pod 15 minutes after the last request (on the platform today it still scales down after ~5 minutes).

Measured on the live instance (2026-10-30), first request after scale-to-zero: 12.1-19.6 s before (67 MB image, migrations before the pod was ready), 6.6-12.1 s after (37 MB image; the migrations finish ~2.5-3.5 s after the pod already answers). What remains is Ignite's own pod start and image pull, which varied from 1.1 s to 6.7 s for the same image. dodil ignite app update <app> --auto-min-instances 1 removes the cold start entirely, at the cost of one always-on pod.

Honest notes

  • The remix gate, tested as far as a free org allows. On 2026-10-30 I remixed this repo into my own org as lanes-rt: the repo and the bucket were created and schema.sql made all 8 tables, then the run stopped at the user pool with "pool limit reached for this plan" (a free org holds one pool, and mine already holds rg-boards-users). I deleted the test copy. A fresh org has no pool, so a judge's remix doesn't hit this, but it is the one thing I could not watch end to end. The same remix.yaml + deploy.yaml built and deployed this repo's own instance, and CI's dodil-ci/remix check passes on every commit.
  • Drag and drop is a desktop gesture. On a phone the board scrolls column by column and a card moves from its List field (HTML5 drag events don't fire on touch).
  • Cold start. The pod scales to zero after ~5 idle minutes; the first request after that takes 5-15 s. I did not turn on min-instances (it costs money in the remixer's org).
  • Overdue uses the server's date (UTC). A card due today turns overdue at 00:00 UTC, which is 05:30 in India and 19:00 the day before in New York.
  • AI calls can be slow. kimi-k2.6 is a reasoning model; the daily summary is written in the background and the page polls. If Dodil Models is down, AI actions answer 503 and change nothing; everything else keeps working.

What I'd improve next

  • Real-time updates (the page refreshes after each action today; two people see each other's moves on their next action).
  • Move the embedding cache onto DataK3's native vector column and vsearch instead of cosine in Python, once the board has thousands of cards.
  • A scheduled daily summary and overdue digest per board (Scriptum), instead of on demand.
  • Comments and attachments (DataK3 objects) on cards, and card templates.
  • Per-board roles in appid tenants, so an org admin can see every board.

Licence

MIT. See LICENSE. Built on the Dodil boards-challenge starter (docs/DODIL-BOARDS-CHALLENGE.md is the original brief).