Project management system development
PM systems range from universal SaaS (Jira, Asana) to custom solutions. Building custom makes sense when: non-standard workflows, strict data requirements, integration with internal systems. Key entities: Project → Milestone → Task → Subtask, plus cross-cutting: User, Team, Comment, Attachment, TimeLog, Activity.
Architectural decisions
Monolith vs microservices: for projects up to 50,000 active users, modular monolith is right. Microservices justified when components scale independently. 90% of PM systems stay as monoliths.
Event-driven inside monolith: task status changes, assignee changes, deadline shifts — all events triggering side effects (notifications, dashboard updates, activity logs) without direct coupling. Use internal event bus.
Data model
Support arbitrary task hierarchy. Use adjacency list with materialized path instead of pure Nested Sets:
CREATE TABLE tasks (
id BIGSERIAL PRIMARY KEY,
project_id BIGINT NOT NULL REFERENCES projects(id),
parent_id BIGINT REFERENCES tasks(id),
path LTREE NOT NULL, -- PostgreSQL ltree: '1.5.23'
title VARCHAR(500) NOT NULL,
status task_status NOT NULL DEFAULT 'todo',
priority SMALLINT NOT NULL DEFAULT 2,
assignee_id BIGINT REFERENCES users(id),
due_date DATE,
estimate INTEGER, -- in minutes
created_at TIMESTAMPTZ DEFAULT now()
);
CREATE INDEX tasks_path_gist ON tasks USING GIST (path);
Extension ltree enables queries like "all subtasks of task 5" with: WHERE path <@ '1.5'.
Task dependencies — separate table:
CREATE TABLE task_dependencies (
task_id BIGINT REFERENCES tasks(id),
depends_on_id BIGINT REFERENCES tasks(id),
type dep_type NOT NULL, -- 'blocks', 'required_by', 'related'
PRIMARY KEY (task_id, depends_on_id)
);
Check for cycles before saving — depth-first traversal or recursive query.
Workflows and state machines
Workflows are main reason companies build custom PM. Approach: configurable workflows per issue type. Each type (Task, Bug, Epic) has own state machine:
'bug' => [
'states' => ['new', 'triaged', 'in_progress', 'in_review', 'resolved', 'closed', 'reopened'],
'transitions' => [
'triage' => ['from' => ['new'], 'to' => 'triaged'],
'start' => ['from' => ['triaged'], 'to' => 'in_progress'],
'review' => ['from' => ['in_progress'], 'to' => 'in_review'],
'resolve' => ['from' => ['in_review'], 'to' => 'resolved'],
'close' => ['from' => ['resolved'], 'to' => 'closed'],
'reopen' => ['from' => ['resolved', 'closed'], 'to' => 'reopened'],
],
],
Store config in DB (JSON), edit via visual workflow builder — drag-and-drop states.
Real-time updates
PM without realtime is solo work. Use WebSocket stack:
- Server: Laravel Reverb, Soketi (Pusher-compatible), or Ably
-
Channels: private project channels
private-project.{id}, task channelsprivate-task.{id} - Presence: shows who's viewing task — important for collaborative editing
// Frontend: Laravel Echo + React
const channel = window.Echo.private(`project.${projectId}`);
channel
.listen('.task.updated', (e) => {
queryClient.invalidateQueries(['tasks', e.task.id]);
})
.listen('.comment.created', (e) => {
setComments(prev => [...prev, e.comment]);
});
Views: Kanban, list, Gantt, calendar
Kanban: columns = workflow statuses. Drag-and-drop via @dnd-kit/core. Virtualize lists > 50 cards (TanStack Virtual).
List with hierarchy: tree-table with expanding subtasks. Server-side sort/filter.
Gantt: complex component. Build on frappe-gantt or @dhtmlx/gantt. Challenge: display dependencies as arrows and auto-recalc dates on duration change.
Calendar: FullCalendar with custom event rendering.
Time tracking
Optional module: embedded time tracker.
CREATE TABLE time_logs (
id BIGSERIAL PRIMARY KEY,
task_id BIGINT NOT NULL REFERENCES tasks(id),
user_id BIGINT NOT NULL REFERENCES users(id),
started_at TIMESTAMPTZ NOT NULL,
stopped_at TIMESTAMPTZ,
duration INTEGER GENERATED ALWAYS AS (
EXTRACT(EPOCH FROM (stopped_at - started_at))::INTEGER
) STORED,
description TEXT
);
Global UI timer storing state {taskId, startedAt} in localStorage + server sync.
Permissions and access control
RBAC with project-scope roles:
System roles: admin, member
Project roles: owner, manager, developer, viewer, external
Each role has permissions: task:create, task:assign, task:delete, project:settings, member:invite. Use spatie/laravel-permission with custom scoped roles.
Guest access: external users see only selected tasks or read-only. Separate token type with limited permissions.
Notifications
Multi-channel with subscription settings:
| Event | Push | In-app | Slack | |
|---|---|---|---|---|
| Task assigned | ✓ | ✓ | ✓ | optional |
| Deadline 24h away | ✓ | ✓ | ✓ | — |
| Comment mention | ✓ | ✓ | ✓ | optional |
User manages in profile settings. Queue notifications via Laravel Queues, batch emails (not 50 if 50 events in 5 min — group as digest).
Search
Global search across tasks, projects, comments, files. Requires instant response, morphology, comment content search.
PostgreSQL FTS via tsvector works up to ~200K tasks. Beyond that: OpenSearch with Tika for extracting text from PDFs/DOCX.
Analytics and reporting
- Velocity: average closure rate per sprint
- Bottleneck: tasks stuck longest in specific status
- Time report: who logged what time, by project
- Custom dashboards: drag-and-drop widgets, save per user profile, cache (Redis, 5min TTL)
Integrations
-
Git repos (GitHub, GitLab): auto status change on commit (
closes #123), linkback from PR - CI/CD: deploy status in task card
- Slack / Teams: notifications, create task from message via slash command
- Confluence / Notion: bidirectional links
- Calendars: sync deadlines to Google Calendar / Outlook via CalDAV
Build via OAuth 2.0 + webhooks + REST/GraphQL API. Store tokens encrypted.
Performance issues
-
N+1 on task list: each task — assignee, tags, last comment. Eager load with
with()or DataLoader. -
Project progress recalc: don't COUNT every request. Denormalize:
completed_tasks_countfield in projects table, update via DB trigger or app event. -
Large lists: cursor-based pagination instead of offset. Cursor by
(created_at, id)for stable behavior with concurrent adds.
Timeline
| Stage | Content | Duration |
|---|---|---|
| Design | Workflows, roles, integrations, wireframes | 3–4 weeks |
| Core | Projects, tasks, workflows, permissions | 6–8 weeks |
| UI: list + Kanban | Basic views | 4–5 weeks |
| Realtime + notifications | WebSocket, email, push | 2–3 weeks |
| Gantt + calendar | Complex views | 3–4 weeks |
| Time tracking | Timer, logs, reports | 2 weeks |
| Integrations (2–3) | Git + Slack + Calendar | 3–4 weeks |
| Testing, launch | E2E, load testing | 2–3 weeks |
Full project: 22–32 weeks. Iterative launch possible at 10–12 weeks — core without Gantt and integrations.







