Skip to content
Featured workTickFlow

Building a task manager, one schema at a time

TickFlow started with student organizations and became a lesson in data modeling, permissions, and the distance between a prototype and a usable service.

Explore the code Sources & scope
An illustration of the idea behind TickFlow.

Student organizations tend to accumulate the same unofficial infrastructure: a group chat, a pinned message that is no longer accurate, and one person who remembers what everyone agreed to do. I wanted a task board that made that work visible without giving the group another complicated system to maintain.

TickFlow is a Next.js application for projects, tickets, assignments, and a shared Kanban board. It uses Prisma and Postgres, with deployment configuration for SCCS infrastructure. The interface came first; connecting it to real data exposed the more interesting questions underneath.

This account follows the public repository through June 2, 2026. It has a database-backed task-management core, but several screens are still prototypes and authentication is unfinished. That distinction is a useful part of the story.

From interface sketches to working mutations

The history begins in late April with a sidebar, themes, dashboard sketches, project pages, and an initial board. Prisma arrived in early May. Ticket and project creation became connected to the database later that month.

PeriodWhat landedWhat changed about the project
April 24–28App scaffold, navigation, themes, dashboard and board sketchesThe workflow became something I could see and click through
May 4–5Prisma setup and relational schemaProjects, people, and assignments acquired an explicit data model
May 24–25Database connection, ticket and project creation, status controlsImportant interactions began writing real records
May 28–30Board refinements, member views, search, navigation, deployment workMultiple views had to agree about the same data
May 30–June 2Release-pipeline revisions and component fixesRunning and maintaining the app became part of the work

This was shared work. My contributions focused on the application and data model; Damian contributed much of the deployment pipeline. The repository is a better guide to that division than treating every file in the stack as something I wrote alone.

The relationships underneath the board

The core schema has five models: User, Project, Task, ProjectMember, and TaskAssignee. A task belongs to one project, while people can belong to several projects and be assigned to several tasks.

User ── ProjectMember ── Project
                          │
                         Task
                          │
User ─── TaskAssignee ─────┘

The two join tables represent different facts. Project membership says someone belongs to the group doing the work. A task assignment says they are responsible for a specific piece of it. Removing an assignment should not remove project membership.

Each join table has a composite primary key, preventing the same user–project or user–task pair from appearing twice. ProjectMember also carries a lead or member role. That creates a place to express permissions later, although storing a role does not enforce one.

My earlier notes described experimenting with an array of assignee IDs. The first committed application schema already uses join tables, so the published history does not establish that earlier iteration. The reason to prefer explicit relationships here is concrete: they give the database foreign keys and uniqueness constraints, and make queries in either direction straightforward. Postgres can query arrays; arrays were not inherently unqueryable.

There is also a smaller identity decision. Projects have both a numeric ID and a human-readable slug. The later navigation work uses the ID in routes, avoiding dependence on display strings containing spaces. The current schema no longer makes the slug unique. That means the two fields should not be treated as interchangeable identifiers.

Creating a ticket without a separate write API

Project and ticket creation use Next.js server actions. A ticket form collects its project, title, description, priority, status, optional due date, and assignees. The action reads that FormData and creates the task with nested assignment records through Prisma.

Project creation follows the same pattern: create the project and its selected memberships in one nested write, then redirect to the project list. The current action assigns each selected person the member role; it does not implement a full role-management workflow.

The actions call revalidatePath after writes so affected pages can fetch updated data. This keeps the mutation layer small. Read-only endpoints still exist for search and the sidebar, so “server actions” does not mean the application has no HTTP API.

The important unfinished work is input handling and authorization. The actions currently use TypeScript casts and basic conversions rather than a comprehensive runtime validation layer. They also lack checks tying the caller to the relevant project. A form that successfully writes to the database is only the beginning of a reliable mutation path.

A board that responds before the database does

The board has six columns:

backlog | todo | inprogress | blocked | done | shipped

I wanted done and shipped to remain separate: finishing a draft and publishing it are different moments. Dashboard totals combine both into completed work, while the board preserves the distinction. blocked is a primary state, which is simple to display but does not preserve whether the task was previously waiting or in progress.

The board loads tasks on the server, including project titles and assignee names, then groups them into columns on the client. Dragging a card immediately removes it from one local group and adds it to another. The status action runs afterward. That optimistic move makes the interface feel responsive.

It also exposes a real gap: the drag handler has no explicit failure rollback. If the update rejects, the local board can temporarily tell a different story from the database. The table views use a separate useOptimistic status dropdown and a pending transition, but neither path is a substitute for a clear error message and reconciliation strategy.

The drag implementation uses native HTML drag events. I would still want keyboard and touch alternatives before calling that interaction complete. A polished desktop gesture should not be the only way someone can move a task.

Finding work across several views

The project page, issues table, board, dashboard, activity page, and member view all look at overlapping data. Their value is giving people different ways into the same work, not creating six independent sources of truth.

Search adds a small read endpoint. After a 200-millisecond debounce, the header requests case-insensitive title matches, capped at five projects and five tasks. The database queries run concurrently. Selecting a task result opens its containing project; there is no dedicated task-detail route in this path.

That implementation is enough for a small dataset, but its limitations are visible. A newer query cancels the pending debounce timer, not an already-running request. A late response can therefore replace newer results. Request cancellation or a query-version check would make the displayed results agree with what is currently typed.

Route changes need the same care. Search and the sidebar use numeric project IDs, but the activity table still builds project links from slugs. The project page parses the route as a number. That mismatch is an unfinished migration between conventions, and a good candidate for a simple navigation regression check.

Which screens are backed by data?

The repository supports a more precise answer than “the app works”:

SurfaceWhat the code does
Projects and ticketsQueries Postgres; creation and status changes persist through server actions
Dashboard and membersComputes counts and assignment summaries from database records
ActivityShows the newest 50 tasks and current workload totals
My IssuesShows all tasks, with an explicit note that personalization awaits authentication
SettingsMixes a working theme selector and database statistics with local-only preference and security controls

The activity page exists, contrary to my earlier write-up. It is not an event history: rows are ordered by task creation time and show current status. There is no stored sequence of status changes from which to answer “who moved this yesterday?”

Several settings illustrate the difference between a screen and a feature. The workspace-name Save button briefly displays a saved state without persisting a name. Notification switches remain local component state. The security controls do not implement password changes, session revocation, or two-factor authentication. They are interface sketches, not account-management capabilities.

Keeping that distinction explicit matters more than adding another settings tab. Someone should be able to tell whether a control changes the application or merely demonstrates how it might look.

Identity is still the main missing piece

The schema has a password field and membership roles, but the inspected application does not implement a complete sign-in or project-authorization layer. Its “My Issues” page says so directly.

Before using it for private organizational work, I need to connect an authenticated identity to every relevant read and mutation. The app should check membership, enforce whatever a lead can do differently, and restrict assignees according to a deliberate project policy. The current ticket picker receives the full user list rather than filtering it to project members.

I had considered both token-based authentication and database-backed sessions in my earlier notes. The useful next decision is less about the format of a credential than the behavior around it: signing in, signing out, revocation, project membership, and access after someone leaves a group. Those paths need to work together.

Shipping the app meant thinking about storage

The checked-in Swarm configuration runs the application and Postgres behind Traefik, with the database on an internal network. Both services are pinned to a particular SCCS node. Postgres uses a local volume backed by a directory on that host.

The placement constraint makes the storage assumption explicit. Rescheduling the database onto another machine would not automatically move its files. This setup therefore does not provide transparent database failover.

The Swarm service command runs prisma migrate deploy before starting Next.js. If migration fails, the application does not start. That couples schema changes to rollout, but a more mature deployment would expose migration results separately and have a tested recovery procedure.

The release history includes a brief image-watcher approach that was subsequently removed. At the reviewed commit, version tags trigger a GitHub build, a mirror workflow copies to SCCS GitLab, and GitLab builds the image and runs docker stack deploy. The mirror triggers when the build workflow completes; its implementation does not check that the build succeeded, despite a comment implying that guarantee.

Those are configuration facts, not a measured uptime record or a fresh deployment test. They show the operational work around even a small app, including places where a reassuring comment is stronger than the actual condition.

What I would finish next

I would finish identity, membership checks, and runtime validation first. Then I would make unsuccessful mutations visible, reconcile optimistic state, and check navigation across every task view. Only after those paths were dependable would I prioritize notifications or a persistent activity log.

The next product test is modest: can one group use it through a real project without someone having to maintain the board for everyone else? Completion percentages cannot answer that. Neither can a working database write.

TickFlow has been useful precisely because the product is ordinary. It gave me a concrete way to learn relational modeling, server and client state, and deployment assumptions. The remaining work is specific enough to tackle without pretending the prototype is already a finished service.

Sources and scope

This account follows TickFlow at commit a9ab338, the June 2, 2026 main-branch snapshot inspected in September. The repository establishes the April–June implementation timeline. Earlier personal notes supply context where identified; no production database, user activity, or live deployment was inspected for this article.