Browse Templates/Task Management

Task Management

A flexible task management database schema designed for productivity and project tracking applications. Includes user accounts, project organization, tasks with priority and status tracking, a label system with many-to-many task-label associations, and due date management. Suitable for apps like Todoist, Trello, or custom task management tools.

PostgreSQL5 tablesMobile
tasksproductivityproject-managementmobiletodo
Created with ERD Studio

About this schema

Everything here belongs to one person. projects and labels both key on user_id, and tasks reach the user through their project — so this is a personal task app rather than a shared one, and there is no membership table because there is nobody to share with.

Labels are rows while a task's status is a column, and that difference is deliberate. A task has exactly one status and any number of labels, so one is an attribute and the other is a many-to-many through task_labels.

is_completed and completed_at sit beside status rather than inside it. The flag is what a checkbox toggles and what every list filters on; the timestamp is what makes it possible to say what was finished this week, which a status value alone cannot.

Key relationships

users → projects → tasks
Two levels of ownership. A task's user is reached through its project, so moving a project moves everything inside it.
users → labels → task_labels
Labels are defined per person and attached to tasks through a join, so renaming one updates every task that carries it.
tasks ↔ labels via task_labels
Many-to-many, which is the only way a task can be both urgent and errand without inventing a combined value.
tasks.sort_order
Manual ordering inside a project, so a hand-arranged list survives being closed and reopened.
projects.is_archived
Hiding without deleting, so a finished project leaves the sidebar while its tasks stay available to search and reporting.

Design decisions

Labels are a join table, not a text column

Comma-separated tags on the task would be one less table and would make renaming a label a scan-and-rewrite, with nothing preventing two spellings of the same tag. The join costs a query to show a task's labels and makes filtering by label an indexed lookup — which, in an app whose main view is a filtered list, is where the work actually happens.

Completion is stored twice over

is_completed, completed_at and status overlap, and each answers a different question: whether to strike it through, when it was done, and which column it sits in. Deriving the flag from the status would work until a status set grows a value like cancelled, which is neither open nor completed. The cost is three columns that must be written consistently.

Tasks have no parent task

There is no self-reference here, so subtasks are not representable — a checklist has to become its own tasks in the same project. That keeps every list query flat and every completion count honest, which is the right trade for a simple app and the first limitation anyone hits when a task turns out to have three steps.

Projects are archived rather than deleted

is_archived keeps the project and its tasks while removing them from the sidebar. Deleting would cascade away the record of what was done, which is most of what a task app accumulates. It also means the archive is just a filter — no second table, no restore path to maintain.

Tables in this template

usersUsers
6 cols
projectsProjects
7 cols
tasksTasks
11 cols
labelsLabels
5 cols
task_labelsTask Labels
3 cols

Frequently asked

Can two users share a project?
Not as modelled — projects keys on a single user_id. Sharing needs a membership table and a role, which also changes who may complete or delete a task, so it is a larger change than it first looks.
Why is there both a status and an is_completed flag?
The flag is what the checkbox and every list filter use; status carries the fuller state such as in progress. Keeping both avoids making every query interpret a status string to decide whether to draw a line through the title.
How are subtasks handled?
They are not. tasks has no parent_task_id, so a multi-step item becomes several tasks. Adding the self-reference is the usual first extension, and it brings with it a decision about how completion rolls up.
Why does users carry a timezone?
Because a due date is a local concept. A task due today has to stop being due at midnight where the person is, not at midnight UTC, and reminders are sent against the same clock.
Should labels be per user or global?
Per user here, which is why labels carries user_id. Global labels would let one person's rename change what another person's tasks say, and in a single-user app there is no benefit to sharing the vocabulary.