CASE STUDY 02 / 05
CT Work
Jira-style project management
A project-management tool built into the platform: boards, backlog, sprints, burndown/velocity reports, a JQL-style query language, test management, an AI assistant that only proposes — people decide — and read-only share links for outsiders.
- Status
- In daily use
- Timeline
- Since 23 Sep 2026 (the module’s first commit)
- Platforms
- Web · iOS · public share link
- Role
- One lead engineer: design, build, operations
- Open CT WorkSign-in required
Context & problem
The studio’s delivery process has many stages, each with documents and a quality gate. It needed one place where tasks, gates, risks and documents link together — and where outsiders (clients, lecturers) can follow progress without an account.
It was built inside the platform rather than adopting an external tool so it could share the existing accounts, permissions, AI gateway and mobile app.
Who it serves
Project teams
Plan sprints, move cards, track tests, write weekly reports.
Outside observers
View the board/backlog through a read-only link that expires and can be revoked.
Students on capstone projects
Run a capstone by the course’s grading rules: requirements, gates, defence documents — LabFlow AI is the first project run this way.
Solution & key features
Boards, backlog, sprints, reports
Drag cards across columns and within the backlog, sprints with burndown and velocity, per-project custom workflows, a trash bin and an audit log.
A JQL-style query language
For example: assignee = currentUser() AND status != Done ORDER BY priority. Syntax errors come with a position and a “did you mean…” suggestion.
Test management
Test cases, test plans and test cycles linked to work items.
An AI assistant that only proposes
Q&A, weekly reports, sprint planning, retros, daily briefs, defence practice. Every change the AI wants is a proposal — nothing is written until a person presses “Apply”.
Sharing and integrations
Per-section read-only public links, an iCalendar (.ics) feed for calendar apps, GitHub/GitLab integration, API tokens for scripts.
Edit lock
With the lock on, the server refuses one’s own writes in that project — so a plan can be browsed without accidentally moving cards.
Architecture
Clients
- Web /workNext.js
- iOSCT Work module
- Share linkRead-only, expiring
- .icsCalendar apps
- API tokenScripts
REST · WebSocketEntry
- Work router213 endpoints
- Edit-lock423 while locked
Every command asks for permissionPermissions
- can(role, action)Pure function + matrix, one place only
AuthorisedDomain
- Issues · SprintsLexoRank-style ordering
- JQLParse → tree → Prisma
- Reports · TestsBurndown, velocity
- AIProposal → human applies
- GitHub · GitLab
Events emitted after the writeData & realtime
- PostgreSQL52 Work* models
- Socket.IOOne room per project
- Audit log
Hard technical decisions & trade-offs
01Card order with LexoRank-style string keys
- Problem
- With 1, 2, 3… positions, dropping a card in the middle renumbers the whole column — many writes, and conflicts when two people drag at once.
- Decision
- Each card holds a string between its two neighbours: a drop writes exactly one row. Appending increments a 6-character head (~2 billion appends at constant length). The alphabet is digits and lowercase only so PostgreSQL collation order matches JavaScript comparison.
- Trade-off
- Repeated middle inserts lengthen keys; past 48 characters (the column holds 64) the column has to be rebalanced.
02JQL compiled to Prisma queries, never concatenated SQL
- Problem
- Letting users type free-form queries invites SQL injection if strings are concatenated.
- Decision
- Two layers: the parser is a pure function (string → syntax tree, errors with positions), the compiler turns the tree into Prisma conditions using the project’s lookup tables. Every value goes through Prisma.
- Trade-off
- A grammar to maintain in-house — it has 25 dedicated test cases, and every syntax bug found in real use becomes a new case.
03One single place decides permissions
- Problem
- Two earlier lessons from this very repo: “hiding a button is not blocking the API” and “two places checking one permission become two rules”.
- Decision
- Every route AND every AI-assistant tool asks the same permission layer. The core is a pure function that never touches the database, tested by table; the outer layer loads data then calls the core.
- Trade-off
- New actions must be added to the matrix — a little slower, but impossible to forget.
04Edit lock enforced on the server, not just hidden buttons
- Problem
- People browsing a pre-built plan would accidentally drag cards or edit fields, with no way back.
- Decision
- While locked, the server returns 423 for that person’s writes — drag and drop, bulk edits, the iOS app and the AI “Apply” button all pass through it. A whitelist keeps comments, daily logs, AI questions and filters working.
- Trade-off
- The whitelist must be updated when new write routes appear.
05AI proposes, a person applies
- Problem
- The AI assistant has tools that change the plan; letting it write directly hands decisions to the model.
- Decision
- Each change the AI wants is stored as a proposal in the message; separate endpoints apply or dismiss each proposal.
- Trade-off
- One more click for the user.
Real incidents & lessons
Accidental edits while only browsing
25/09/2026
- What happened
- A user browsing a pre-built plan accidentally dragged cards and edited fields.
- Root cause
- The board allowed writes at all times, with no view-only mode for the project owner.
- Fix & safeguard
- Added a per-project edit lock, enforced on the server (code 423), with an Unlock button in the UI.
`sprint IN openSprints()` failed to parse
- What happened
- A familiar Jira-style filter reported a syntax error while seeding the LabFlow project.
- Root cause
- The parser only accepted a parenthesised list after IN, not a bare function.
- Fix & safeguard
- Fixed the grammar to match Jira and added test cases for all three spellings.
Measured numbers
No user, revenue or client numbers here — only what can be counted from source code and the database. Each row says how it was counted so you can check it. Anything that cannot be counted is not shown.
Module size
Counted by command on the codebase on 04/10/2026
- Dedicated data modelsgrep -c "^model Work" prisma/schema.prisma
- 52
- API endpointsrouter.get/post/…( lines in work.routes.ts
- 213
- UI pagespage.tsx under frontend/src/app/work
- 20
- Backend lines of codewc -l services/work/*.ts + work.routes.ts
- 12,930
- Test files*.test.ts in services/work and routes/work*
- 18
- Test casestest(…)/it(…) lines in those 18 files
- 174
- Module commitsgit rev-list --count HEAD -- (work folders)
- 32
Technology
- Front end
- Next.js · React · TypeScript · TipTap
- Back end
- Express · Prisma · PostgreSQL · Socket.IO
- Integrations
- GitHub · GitLab · iCalendar (.ics) · API token
- Testing
- node:test (tsx --test) · Kiểm thử trên cơ sở dữ liệu thật