02The story
Why I am building it
Five years of WordPress teaches you a great deal about content and comparatively little about application state. WordPress answers most of the hard questions for you — users, roles, storage, routing — and it answers them one particular way.
I wanted a project where none of those answers arrived for free, at a size where the decisions would be real rather than tutorial-shaped. Task and project management is a good choice for that, because it looks simple from the outside and is almost entirely permissions on the inside.
What it has to get right
Who can see this project. Who can move that task. Who can be invited, and by whom. What a stranger holding a link is allowed to open. What happens to a comment when the thing it was attached to is deleted.
Get that layer wrong and every feature above it is unsafe, so it is the layer everything else waits for.
Architecture
Two applications and one contract: a Laravel 12 REST API authenticated with Sanctum, and a Vue 3 single-page application that knows nothing about Laravel beyond the shape of its responses.
Deliberately not Inertia. The point of the exercise was an API I could put a second client in front of — and a front end that has to deal with loading, failure and reconciliation honestly, rather than being handed server-rendered state.
- models
- 14
- migrations
- 28
- API endpoints
- 75
Three decisions worth explaining
Roles composed from capabilities. Off-the-shelf permission packages model a role as a label with a list attached. I needed the inverse: the capability as the unit, roles built out of them, every check asking about a capability rather than a role name. Eighty capabilities across fourteen modules live in a single registry the seeder reads, so adding a new kind of member later is a data change instead of a code change.
Share links as their own concept. A share link is not a user with fewer rights. It is a separate grant against a single attachment, with its own 48-character token, its own expiry, its own on/off switch, its own download-or-view-only flag, and its own access count. Modelling it as a weak user is the mistake that leaks data.
Ten Pinia stores, not one. Each owns a domain and its own loading and error state. A single global store is comfortable for about a week and unreadable by the third feature.
What exists today
Seventy-four single-file components across boards, task detail, comments, invitations and settings. TipTap for rich text, a draggable kanban that updates the store optimistically and reconciles against the API's answer, polymorphic comments with likes, subtasks with their own ordering, project members and labels, email invitations with an accept flow, and tokened share links for attachments.
The part I did not plan and now would not remove is drafts. An unfinished comment is saved against a context key on the server, not in the browser, so it survives a closed tab and follows you to another machine.
What is honestly not finished
Time tracking and the in-app messaging screen are built as front-end prototypes over fixture data — the components are real, the API behind them is not written yet. They are in the application and they are not in this case study's screenshots, because a screenshot of a working timer would be a claim I cannot support.
That is the current shape of the project: the permission layer, the data model and the collaboration features are wired end to end; the two features I prototyped ahead of the backend are waiting for it.
What is left
Those two backends, deployment, and the first users breaking it in ways I have not thought of. When that happens this page gets a URL and an outcome section it has not earned yet.
What the project has already produced is the thing I wanted from it: Laravel, Vue, Pinia and REST are no longer frameworks I have read about. They are frameworks I have made decisions in and had to live with.

