Cobra Boards
Team task boards you can run from the web page, or straight from ChatGPT and Claude. Boards → lists → cards, with owner / member / viewer roles, a full change history, blockers, comments with @mentions, notifications, files on cards, a board that updates live, and an AI that suggests but never writes on its own. Everything runs on Dodil: DataK3 stores it (tables, vectors and the files), an appid pool signs people in, Ignite runs it, Dodil Models does the AI, and Dodil Git CI lints, tests and deploys it.
223 tests · 29 MCP action tools + 3 prompts · ruff clean · 98% coverage of the rules and doors
· remix into a fresh org: tested
Someone says "I need a Trello". You say "remix this, it's yours":
dodil git remix eduard/cobra-boards --to-org <your-org> --name <your-repo> # name ≤ 18 chars
| Live | https://cobra-boards-eduard-8080.ignite.dodil.cloud |
| MCP | https://cobra-boards-eduard-8080.ignite.dodil.cloud/mcp |
| Source | https://git.dodil.io/eduard/cobra-boards (MIT) |
More screens: the board · sign-in (the appid pool, branded) · card detail · viewer, read only · My week · phone · themes Daylight and Venom
Judge it in 60 seconds
Demo team: sign in as each role
Open https://cobra-boards-eduard-8080.ignite.dodil.cloud and sign in with any of these. They
share the boards Product Launch (all three) and Ops (owner and member). Password for all
three: CobraDemo-2026!
| Role | Try this | |
|---|---|---|
| Owner | owner@cobra-demo.com | Members: change a role, invite someone. Open #1 Ship v1 → What is blocking this? |
| Member | member@cobra-demo.com | The 🔔 has 8 notifications. Drag a card, comment with @owner, attach a file, archive a card. |
| Viewer | viewer@cobra-demo.com | Read only: cards don't drag, no comment box, no upload. Over MCP every write is refused. |
Two windows, two roles: move a card as the member and watch the owner's board update by itself.
MCP endpoint for ChatGPT / Claude: https://cobra-boards-eduard-8080.ignite.dodil.cloud/mcp
(sign in with one of the accounts above). The demo data was written with
scripts/seed_demo_team.py, through the app's own use cases.
Or start fresh with your own account
- Open the live URL and create an account (the pool allows sign-up). Your first visit gives you a private Welcome to Cobra 🐍 board, already populated: two overdue cards flagged in red, a chain of blockers, a half-done checklist, labels and history. Nobody else sees it.
- Drag a card to another list (a gold line shows where it lands), then open it: the History says who moved it, when, and what changed (To do → Doing).
- Open #1 Launch the new website → "What is blocking this?". It walks the chain (#1 ← #2 QA ← #3 login bug, and #4 security review) and says where to start.
- AI, on Dodil Models only: on a card, ✦ Suggest a checklist (nothing is saved until you pick the steps) and ✦ Find similar cards (vector search over DataK3); on the board, ✦ Daily briefing (the numbers are counted from the board; the model only words them).
- Talk on a card: write a comment with @name (the chips under the box fill it in). The person gets a notification under the 🔔; the card's assignee hears about it too. Attach a file (up to 10 MB, kept in this board's DataK3 bucket); images show a thumbnail.
- Live: open the same board in a second window as someone else and move a card there. Within seconds the first window updates itself and says who moved what (the green live dot).
- Archive, don't delete: Archive card takes it off the board with its number, history and comments; Archived → Restore puts it back. New boards start from a template (Sprint, Launch, Onboarding, Support, or the basic three lists).
- Roles: Members → invite a second email of yours as viewer and sign in with it: the board says read only, cards don't drag, there's no "add card", and every write through the API or MCP is refused (403 / tool error). A board you're not on is a 404, even by name.
- ChatGPT or Claude: add the MCP URL above as a custom connector (OAuth; the app's sign-in
page appears), then ask:
- "What's on my plate this week?"
- "Add a card 'Send invoice' to Doing on the Welcome board, assigned to me, due tomorrow"
- "Move card 3 to Done" · "What's overdue?" · "What is blocking the launch?"
- "Comment on card 2: @bo the receipt email is the last blocker" · "Any notifications?"
- the prompts menu: Weekly review, Stand-up, Plan a goal (it shows the plan and creates the cards only after you say yes)
- signed in as a viewer: "Move card 1 to Done" → Not allowed: viewers can look but can't change the board.
Evidence (live app, real Dodil services)
v2, 3 Nov 2026 · watch in the browser, no sign-in: ChatGPT session · web walkthrough
- 🎬 Web walkthrough v2 (50 s), filmed on the local preview with the demo team, two people at once: a comment with @leo and its notification, Leo moving a card in his own window while this board updates by itself, a file from DataK3 object storage, the archive, board templates, Leo's 🔔, and the same card as a read-only viewer.
- 💬 ChatGPT, live through
/mcp, after Refresh tools picked up the 29 tools: recording and the whole session as one image: a comment whose @member reachedmember@cobra-demo.com("1 person notified"); ChatGPT asking before the invite; plan a goal shown as a draft, created only after "yes" (4 cards, 3 blockers); "what is blocking card 6?" → start with #3; and an archive (asked first:destructiveHint). When I first named a board this account doesn't have, it found only Launch Plan, said so, and changed nothing.
v1, 2 Nov 2026
- 🎬 Web walkthrough (41 s): drag and drop, the blocker chain, the AI checklist, My week, and the same board as a read-only viewer.
- ChatGPT, through the live
/mcpendpoint, signed in as me via the app's appid pool: create a board, add a card, "what's on my plate this week?" and add a card, block it, "what is blocking card 2?", move card 1 to Done, card history. ChatGPT asks the person before every write; the tools' read/write hints are what it reads.
What's inside
| Area | What it does |
|---|---|
| People | Owner invites by email, as member or viewer, changes roles, removes people. Removing someone unassigns their cards, with history. A board has exactly one owner. |
| Boards | Boards → lists → cards. Lists are the board's own (add, rename, reorder, mark as a done list). Cards: title, description, assignee, due date, labels, position. |
| Card numbers | Every card gets a short number on its board (#12), claimed by a plain INSERT so two people adding at once never share one. People and agents say "card 12". |
| History | Every change is one event: who, when, and field: [before, after]. The card row and its event are written in one DataK3 transaction. |
| No lost updates | Each card carries a version. An edit sends the version it read; a stale one is 409 "changed since you opened it" and nothing is written (UPDATE … WHERE version = …). |
| Overdue | Past due and not in a done list. Shown in red on the board, in My week, Overdue, the sidebar count, and the daily briefing. |
| Blockers | "#3 is blocked by #2", same board only, cycles refused. What is blocking? walks the whole chain and names the cards to start with. |
| Checklists | Manual steps, plus an AI suggestion that changes nothing until a person adds what they want. |
| Meaning search | Cards are embedded with Dodil Models (arctic-embed-m-v2, 768-d) into a DataK3 VECTOR(768) column; cosine KNN finds cards that mean the same, only on boards you can see. Re-embedded only when the text changes. |
| Daily briefing | Counts computed from the board (overdue, blocked, due today, finished), worded by kimi-k2.6 with a prompt that forbids inventing cards. Works without the AI, too. |
| Comments & @mentions | Members comment on cards; @bo finds bo@… when only one person on the board starts with "bo" (two matches mention nobody: no guessing). Viewers read comments but can't write them. |
| Notifications | A 🔔 with what changed for you: you were @mentioned, a card was given to you, someone commented on your card. Never for your own actions, only from boards you're still on; mark one or all as read. |
| Files on cards | Up to 10 MB each, 20 per card, stored in the bucket's DataK3 object storage with the app's own service account. Streamed only to people on the board (a stranger gets 404 even with the file id); anything that could run in the page (HTML, SVG…) is served as a download with nosniff and a sandbox CSP. |
| Live board | The open board polls one cheap query (the newest history line). When a teammate changes it, the board reloads in place and a toast says who did what. Works across replicas, no sockets. |
| Archive & templates | No delete, by design: archived cards leave every view (board, My week, Overdue, search, blockers, meaning search), keep their number and history, and come back with Restore. New boards start from 5 templates. |
| MCP | 29 tools, all actions (move_card, add_comment, my_notifications, archive_card, what_is_blocking…), never table access, plus 3 prompts (weekly review, stand-up, plan a goal). Read tools carry readOnlyHint, writes don't. Answers are short text with card numbers, not JSON dumps. |
| Web | A zero-build page (no framework, no third-party assets): drag and drop with a drop line, keyboard access, card detail, My week, Overdue, search by words or by meaning, three themes, a phone layout. |
Asking instead of guessing
People name things loosely, and so do their agents. A board is found by id or title ("ops" finds Ops), a card by number on a board, a list by name ("doing" finds Doing), a date by words ("tomorrow", "+3d", "next week"), an assignee by email or "me". When two things match (two boards called Ops north / Ops south, card #1 on two of your boards), the tool answers with the candidates and asks. It never writes to a guess.
Architecture
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 by using the other door.
app/boards/
domain/model.py Board, BoardList, Card, Event, roles, the rules (no I/O)
application/use_cases.py every action a person can take; resolvers that never guess
application/sample.py the first-visit sample board (built through the use cases)
application/ports.py what the use cases need: a repository, an object store, an assistant
infrastructure/
repository.py DataK3 over the Postgres wire: claims, versioned updates, vectors,
comments, notifications, attachment records, the archive
memory.py the same contract in memory (tests, local preview)
dodil.py service-account token, a reused DataK3 connection, DataK3 object
storage for files, Dodil Models (a failed model call = a clear message)
doors/
web.py JSON API + the page (401 / 403 / 404 / 409 / 422)
mcp.py the MCP tools, acting as the signed-in person
text.py how the board reads in a chat
identity.py who is calling: the gateway's X-Dodil-User, nothing else
static/ the page: index.html, app.js, app.css, the logo
tests/ 223 tests: roles, visibility, conflicts, history, comments, files,
archive, live, MCP tools + prompts, web (CI: ruff + coverage >= 85%)
scripts/live_check.py the same story against REAL DataK3 (tables, vectors, objects) and
Dodil Models, on a test bucket: 27 steps, "ALL GOOD" on 3 Nov 2026
scripts/preview.py local preview with a demo team, no Dodil account
schema.sql 12 tables, natural keys, created by the remix
.dodil/remix.yaml what a remix creates; .dodil/deploy.yaml how CI deploys it
More: docs/DESIGN.md (the decisions and why).
Remix it into your organization
dodil auth login --web
dodil git remix plan eduard/cobra-boards --to-org <your-org> --name <your-repo> # creates nothing
dodil git remix eduard/cobra-boards --to-org <your-org> --name <your-repo>
It creates one bucket (<repo>-data, with the 12 tables from schema.sql), one appid pool
(<repo>-users), one service account (k3.editor on that bucket + ignite.model-user), the repo,
and deploys it. Credentials go straight into the new repo's CI secrets; nothing to copy by hand.
Keep the repo name to 18 characters or less (the bucket name is limited to 25).
After the remix: let people sign in (the one documented step)
- Check the deploy log of the first run. Since dodil git 1.6.11 the deploy registers the app's sign-in callback on the pool itself ("already lists the sign-in callback"). If it says the callback was not registered, run the command printed on that line, once.
- Open the app's URL (
dodil ignite app get <repo>→public_urls) and create an account. The pool allows sign-up by default; if you turned it off, invite people withdodil appid user create <repo>-users <email>. - ChatGPT / Claude: add
https://<app host>/mcpas a custom connector. ChatGPT registers through CIMD and is approved automatically. A client that arrives pending needs one approval:dodil appid client list <repo>-users, thendodil appid client approve <repo>-users <client-id>. - Optional self-check:
dodil ignite invoke <repo>hitsPOST /, which opens the bucket with the app's own credential and returns a row count per table. - Optional look: a new pool's sign-in page carries the platform's default theme. To give it the
Cobra look, put your app's host in
logo_urlin.dodil/branding.json, thendodil appid branding set <repo>-users @.dodil/branding.jsonanddodil appid pool update <repo>-users --display-name "Cobra Boards".
Tested: a remix into a fresh organization
On 3 Nov 2026, after v2 (12 tables), I remixed eduard/cobra-boards (template at f293bfa)
into a second, empty organization (edrax) as cobra-fresh, and checked the copy from outside:
| Check | Result |
|---|---|
dodil git remix … | repo, bucket, pool, service account created; run succeeded — deployed |
dodil ignite app list | edrax/cobra-fresh, deployed |
dodil ignite invoke cobra-fresh | ok: true, all 12 tables present, 0 rows (its own new bucket) |
GET /healthz | {"ok":true} |
GET / signed out | 303 to its sign-in, then appid.dodil.io/edrax/cobra-fresh-users, its own pool |
| sign-in page | 200, the pool's hosted login with sign-up |
GET /api/me, POST /mcp signed out | 401 |
/.well-known/oauth-protected-resource | points ChatGPT / Claude at the new pool |
Nothing was copied by hand: the bucket, pool, service account, CI secrets and deploy all came
from .dodil/remix.yaml. (The first copy, from 2 Nov with 8 tables, passed the same checks.)
Run it locally
python -m venv .venv && .venv/bin/pip install -r app/requirements.txt pytest
cd app && ../.venv/bin/python -m pytest -q ../tests # 223 tests in ~8 s, no account needed
python scripts/preview.py # http://localhost:8775
# act as the demo owner / member / viewer: http://localhost:8775/?as=viewer
Against a test bucket with real Dodil services (never the live -data bucket; the script
refuses one):
BUCKET=<test-bucket> DODIL_SERVICE_ACCOUNT_ID=… DODIL_SERVICE_ACCOUNT_SECRET=… \
python scripts/live_check.py
What I'd improve next
- Push instead of poll: the live board asks every 4 s for one row (cheap, and it works on every replica). Server-sent events would cut that to instant once there is a shared channel.
- Invitations and notifications by email: today they live in the app (and in the agent's
my_notifications); an email digest is the next step. - Board archiving (cards archive today; whole boards don't yet).
- jina-embeddings-v4, the challenge's suggested model, timed out (> 120 s) every time I tried
it on 2 Nov 2026, so meaning search uses
arctic-embed-m-v2, also on Dodil Models. Switching back is one env var (EMBED_MODEL) plus the vector width inschema.sql.
Honest notes (what was hard)
kimi-k2.6rejects a request that carriestemperature(500, "invalid for this model"); the app sends none.- The embeddings model in the brief timed out, so I measured the alternatives and switched.
- A connection per query cost 1–3 s from Europe; one reused connection per worker (under the bucket's concurrency limit) made it ~3× faster.
- In the first live ChatGPT test the agent couldn't say "assigned to me", so it had to look up my email. Now "me" means the signed-in person, on the web and over MCP.
- Claude.ai inside a company organization only lets the org owner add a custom connector, so the live MCP proof above is from ChatGPT; Claude Desktop / Claude Code connect the same way.
- A free-plan org holds one appid pool, so remixing a second copy into the same org stops at "pool limit reached". Remix into a fresh org (as the judging does); that is the path I tested.
- Files: there's no upload helper for apps in the CLI guide, so I probed DataK3 object storage
on a test bucket first. The app's own service account (
k3.editor) can PUT, GET and DELETE objects in its bucket, so files need no extra grant and no public links. - On 3 Nov an embeddings call returned 500 once. A model that doesn't answer now gives the person a clear "try again in a minute" (meaning search, checklist) while the board, plain search and the briefing's counted facts keep working.
- Re-testing the remix on 3 Nov, I deleted the old copy and remixed again with the same name
straight away: the service-account step failed with a platform error (
keycloak error: kc_apply_new_assignment), twice on resume. The same remix under a new name went through at once, so a just-deleted service-account name seems to stay taken for a while. A fresh org (as the judging uses) never meets this. - A remix can't carry the sign-in page's branding (
remix.yamlhas no field for it), so a fresh copy first shows the platform's default login theme; step 5 above is one command.
Licence
MIT. Cobra Boards is my own name and design; it uses no Trello name, logo or artwork.
Dodil partner programme
Applied on 3 Nov 2026 (as EDRAX, Romania); under review.