To build a Laravel project management tool, you need five parts: projects with members and roles, tasks on a Kanban board, policies that control who can see and change what, real-time updates with Laravel Reverb, and notifications for assignments and due dates. You can code these by hand in a few weeks or generate the base app with AI much faster, then spend your time on the features that make your tool different.
Ravi runs a six-person Laravel agency. In January 2026, he calculated what his team was paying for task management tools: two subscriptions, per-seat pricing, and clients who refused to create yet another login. He didn't need another Trello. He needed a client portal where tasks, files, and approvals lived in one place, with his agency's branding.
That's the real reason to build a project management app in Laravel yourself. Off-the-shelf tools work well for general tasks. A custom tool wins when it fits a specific workflow: client approvals, construction punch lists, editorial calendars, or support escalations.
This guide walks through the data model, permissions, a drag-and-drop Kanban backend, real-time updates, and reminders, with working Laravel code for each.
Key Takeaways
A Laravel project management tool needs five building blocks: projects and members, tasks, policies, real-time updates, and notifications.
Store task order as an integer
positionper status column, and reorder tasks inside a database transaction so concurrent updates don't corrupt the board.Authorize everything through membership. A user who isn't on a project's member list should never be able to load its tasks, comments, or broadcast channel.
Laravel Reverb provides first-party WebSockets, so cards move on everyone's screen without per-message fees from a third-party service.
AI can generate the CRUD, admin, and authentication layers, leaving you to build the workflow that makes your task manager different.
The people in this article's examples are illustrative composites based on common Laravel scenarios.
What Your Laravel Project Management Tool Needs
Before writing code, decide what version one includes. Most Laravel task manager projects fail because they try to copy every Jira feature in the first month.
Feature | Version 1 | Later |
|---|---|---|
Projects with members and roles | Yes | Teams and workspaces |
Tasks with status, assignee, and due date | Yes | Subtasks, labels, time tracking |
Kanban board with drag and drop | Yes | Gantt and calendar views |
Comments | Yes | Mentions, file attachments |
Real-time board updates | Yes | Presence ("Maya is viewing") |
Email and in-app notifications | Yes | Slack or WhatsApp alerts |
Admin panel | Yes | Usage analytics |
Three roles cover most products: owner (manages members and settings), member (creates and edits tasks), and viewer (read-only, ideal for clients).
If you're selling the tool to several companies, plan for multi-tenancy now. Our Laravel multi-tenancy guide compares single-database and database-per-tenant approaches.
Step 1: The Database Schema
Four tables form the core of the app: projects, a project_user pivot table with a role, tasks, and comments.
// database/migrations/xxxx_create_project_management_tables.php
Schema::create('projects', function (Blueprint $table) {
$table->id();
$table->foreignId('owner_id')->constrained('users');
$table->string('name');
$table->text('description')->nullable();
$table->timestamps();
});
// Membership with a role: owner, member or viewer
Schema::create('project_user', function (Blueprint $table) {
$table->foreignId('project_id')->constrained()->cascadeOnDelete();
$table->foreignId('user_id')->constrained()->cascadeOnDelete();
$table->string('role')->default('member');
$table->primary(['project_id', 'user_id']);
$table->timestamps();
});
Schema::create('tasks', function (Blueprint $table) {
$table->id();
$table->foreignId('project_id')->constrained()->cascadeOnDelete();
$table->foreignId('assignee_id')->nullable()->constrained('users')->nullOnDelete();
$table->string('title');
$table->text('description')->nullable();
$table->string('status')->default('todo');
$table->unsignedInteger('position')->default(0); // order within a status column
$table->date('due_date')->nullable();
$table->timestamps();
// The board query filters by project and status, then sorts by position
$table->index(['project_id', 'status', 'position']);
});
Schema::create('comments', function (Blueprint $table) {
$table->id();
$table->foreignId('task_id')->constrained()->cascadeOnDelete();
$table->foreignId('user_id')->constrained()->cascadeOnDelete();
$table->text('body');
$table->timestamps();
});
Use a PHP enum for task status so invalid values never reach the database:
// app/Enums/TaskStatus.php
enum TaskStatus: string
{
case Todo = 'todo';
case InProgress = 'in_progress';
case Review = 'review';
case Done = 'done';
}
// app/Models/Task.php
class Task extends Model
{
protected $fillable = ['title', 'description', 'status', 'position', 'assignee_id', 'due_date'];
protected function casts(): array
{
return [
'status' => TaskStatus::class,
'due_date' => 'date',
];
}
public function project(): BelongsTo
{
return $this->belongsTo(Project::class);
}
public function assignee(): BelongsTo
{
return $this->belongsTo(User::class, 'assignee_id');
}
}
On the Project model, define members() as a belongsToMany(User::class)->withPivot('role')->withTimestamps() relationship, along with tasks() as a hasMany relationship. Note that project_id isn't in $fillable. Create tasks through $project->tasks()->create(...) so users can't move a task into a project they don't belong to.
When someone creates a project, add them as its owner in the same transaction. The policies below only check project_user, so skipping this step locks creators out of their own projects:
// ProjectController@store
$project = DB::transaction(function () use ($request, $data) {
$project = $request->user()->ownedProjects()->create($data);
$project->members()->attach($request->user()->id, ['role' => 'owner']);
return $project;
});
This assumes an ownedProjects() hasMany(Project::class, 'owner_id') relationship on User, and that $data comes from a validated request.
Want the schema, models, and admin panel generated for you? StackMivo builds a full-stack Laravel app from a plain-English description, including a Filament admin panel, so you can start at Step 3 instead of Step 1.
Step 2: Permissions with Policies
In a project management tool, the worst bug isn't a crash. It's a client seeing another client's tasks. Put every access rule in policies and base them on project membership.
// app/Policies/ProjectPolicy.php
class ProjectPolicy
{
// Any member, including viewers, can see the project
public function view(User $user, Project $project): bool
{
return $project->members()->whereKey($user->id)->exists();
}
// Only owners can invite or remove members
public function manageMembers(User $user, Project $project): bool
{
return $project->members()
->whereKey($user->id)
->wherePivot('role', 'owner')
->exists();
}
}
// app/Policies/TaskPolicy.php
class TaskPolicy
{
// Owners and members can edit tasks; viewers are read-only
public function update(User $user, Task $task): bool
{
return $task->project->members()
->whereKey($user->id)
->wherePivot('role', '!=', 'viewer')
->exists();
}
}
Nest task routes under projects and use scopeBindings() so /projects/1/tasks/99 returns a 404 if task 99 belongs to a different project:
// routes/web.php
Route::middleware('auth')->scopeBindings()->group(function () {
Route::get('/projects/{project}', [ProjectController::class, 'show'])
->name('projects.show')
->can('view', 'project');
Route::patch('/projects/{project}/tasks/{task}/move', [TaskController::class, 'move'])
->can('update', 'task');
});
Step 3: The Kanban Board Backend
The board shows one column per status, with tasks sorted by position. Load everything in one query and group the results in PHP:
// ProjectController@show
$columns = $project->tasks()
->with('assignee:id,name') // avoid an N+1 query per card
->orderBy('position')
->get()
->groupBy(fn (Task $task) => $task->status->value);
Drag and drop is where many Laravel task manager builds break. When a card moves, you need to close the gap in its old column, make room in the new one, and save the card, all at once. Do it in a transaction, and lock the project row so two people dragging cards simultaneously don't produce duplicate positions:
// app/Http/Controllers/TaskController.php
public function move(Request $request, Project $project, Task $task)
{
$data = $request->validate([
'status' => ['required', Rule::enum(TaskStatus::class)],
'position' => ['required', 'integer', 'min:0'],
]);
DB::transaction(function () use ($project, $task, $data) {
// Serialize concurrent moves within this project
Project::whereKey($project->id)->lockForUpdate()->first();
$task->refresh();
// Close the gap in the old column (excluding the moved task itself)
Task::where('project_id', $project->id)
->whereKeyNot($task->id)
->where('status', $task->status)
->where('position', '>', $task->position)
->decrement('position');
// Make room in the new column (excluding the moved task itself)
Task::where('project_id', $project->id)
->whereKeyNot($task->id)
->where('status', $data['status'])
->where('position', '>=', $data['position'])
->increment('position');
$task->update($data);
});
// Tell everyone else on the board (Step 4)
broadcast(new TaskMoved($task))->toOthers();
return response()->noContent();
}
Excluding the moved task from both shifts matters. Without it, dropping a card back where it started leaves a gap in the column. Note that lockForUpdate() has no effect on SQLite, so use MySQL or PostgreSQL in production.
The can('update', 'task') middleware on the route already checks permissions, and scopeBindings() guarantees that the task belongs to this project. Our guide to building a CRM in Laravel uses the same pattern for sales pipeline stages. On the front end, any drag-and-drop library works; SortableJS is a common choice with Livewire or Vue. Send the new status and index when a card is dropped.
Elena, a former construction site manager, built her punch-list tool this way in spring 2026. Her first version saved only the moved card's position. Within a week, two foremen dragging cards at the same time had created three cards at position 2. Adding the transaction and row lock fixed the issue in an afternoon, and the bug never came back.
Step 4: Real-Time Updates with Laravel Reverb
A board that needs a page refresh feels broken when five people use it. Laravel Reverb is Laravel's first-party WebSocket server. It's open source, so there are no per-message fees, though you still need to run and pay for the server yourself or use Laravel Cloud's managed Reverb. Our Laravel Reverb WebSockets guide covers setup and scaling in depth. Here's the part specific to a Kanban board.
Install broadcasting with the Reverb driver. This creates routes/channels.php and installs Reverb and Echo:
php artisan install:broadcasting --reverb
php artisan reverb:start
Create an event that broadcasts on a private channel for each project. Send only the fields the board needs, not the entire model:
// app/Events/TaskMoved.php
class TaskMoved implements ShouldBroadcast
{
use Dispatchable, InteractsWithSockets, SerializesModels;
public function __construct(public Task $task) {}
public function broadcastOn(): array
{
return [new PrivateChannel('projects.'.$this->task->project_id)];
}
// Limit the payload so private task data doesn't leak
public function broadcastWith(): array
{
return [
'id' => $this->task->id,
'status' => $this->task->status->value,
'position' => $this->task->position,
];
}
}
Authorize the channel using the same policy as the page:
// routes/channels.php
Broadcast::channel('projects.{project}', function (User $user, Project $project) {
return $user->can('view', $project);
});
Then listen in the browser with Echo:
// resources/js/board.js
window.Echo.private(`projects.${projectId}`)
.listen('TaskMoved', (e) => {
// moveCard is your own board function
moveCard(e.id, e.status, e.position);
});
Two details often trip people up. ShouldBroadcast events are queued, so a queue worker must be running. And toOthers() only skips the sender when the request includes the socket ID header, which Echo attaches automatically when you use the global Axios instance. Otherwise, for example with Livewire, send the X-Socket-ID header yourself using Echo.socketId().
Step 5: Notifications and Due-Date Reminders
People forget tasks they can't see. Send a notification when someone is assigned a task and a reminder the day before it's due. The Laravel notifications guide covers channels in detail.
// app/Notifications/TaskAssigned.php
class TaskAssigned extends Notification implements ShouldQueue
{
use Queueable;
public function __construct(public Task $task) {}
public function via(object $notifiable): array
{
return ['mail', 'database']; // email plus an in-app bell icon
}
public function toMail(object $notifiable): MailMessage
{
return (new MailMessage)
->subject('New task: '.$this->task->title)
->action('Open task', route('projects.show', $this->task->project_id));
}
public function toArray(object $notifiable): array
{
return ['task_id' => $this->task->id, 'title' => $this->task->title];
}
}
For reminders, write an Artisan command and schedule it in routes/console.php:
// app/Console/Commands/RemindDueTasks.php (inside handle())
Task::whereDate('due_date', today()->addDay())
->where('status', '!=', TaskStatus::Done)
->whereNotNull('assignee_id')
->with('assignee')
->chunkById(200, function ($tasks) {
$tasks->each(fn (Task $task) => $task->assignee->notify(new TaskDueSoon($task)));
});
// routes/console.php
use Illuminate\Support\Facades\Schedule;
Schedule::command('tasks:remind-due')->dailyAt('08:00')->onOneServer();
The database channel needs a notifications table. Run php artisan make:notifications-table and php artisan migrate. TaskDueSoon follows the same pattern as TaskAssigned, and onOneServer() requires the database, Memcached, DynamoDB, or Redis cache driver. The projects.show route is the one named in Step 2. If scheduling is new to you, see our guide to Laravel task scheduling and cron jobs.
Build It Faster with AI
Steps 1 to 5 are the foundation, not the product. What makes your tool worth paying for is the workflow on top: client approvals, punch-list photos, sprint reports, or whatever your customers do every day.
That's where an AI app builder saves the most time. With StackMivo, you describe the app in plain English and get real Laravel code. A prompt like this covers most of this guide:
"Build a project management tool. Users belong to projects with owner, member or viewer roles. Tasks have a title, description, status (todo, in progress, review, done), assignee, due date and position. Show a Kanban board per project. Add comments on tasks, email notifications when a task is assigned, and an admin panel for managing users and projects."
Then review the generated code like any pull request. Check that policies cover every route, the board query eager loads assignees, and task moves run in a transaction. Add Reverb and your custom workflow on top. For the bigger picture of how teams split work between AI and developers, read Laravel AI for teams.
Tom, who freelances for marketing agencies, used this approach for an editorial calendar tool in August 2026. He generated the projects, tasks, roles, and admin panel on day one. He spent the rest of the week on the part his client cared about: a "ready for review" column that emails the client a preview link and records their approval.
Ready to skip the boilerplate? Start building on StackMivo and get to your custom workflow on day one.
Common Mistakes When Building a Laravel Task Manager
Checking permissions only in the UI. Hiding a button isn't security. Every route and broadcast channel needs a policy check.
Saving only the moved card's position. Other cards in both columns must shift too, inside a transaction.
Broadcasting the whole model. Use
broadcastWith()so descriptions and private fields don't reach every subscriber.Skipping eager loading on the board. A 200-card board with lazy-loaded assignees runs 201 queries.
Forgetting the queue worker. Broadcasts and queued notifications wait until a worker runs.
Building Gantt charts before version one ships. Launch with a board, then add views your users ask for.
Frequently Asked Questions
Can Laravel be used to build a project management tool?
Yes. Laravel includes everything a project management tool needs: authentication, authorization policies, queues, notifications, task scheduling, and first-party WebSockets through Reverb. Each layer of a task manager maps to a built-in Laravel feature.
How long does it take to build a project management app in Laravel?
A version one with projects, roles, a Kanban board, comments, and notifications typically takes an experienced Laravel developer several weeks to build by hand, depending on the scope and front-end polish. Generating the base app with AI can reduce much of the CRUD, authentication, and admin work, leaving more time for custom features.
What's the best way to build a Kanban board in Laravel?
Store a status and an integer position on each task, load the board with one eager-loaded query, and reorder positions inside a database transaction with a row lock. Use Livewire or Vue with SortableJS on the front end, and broadcast moves with Reverb.
Do I need Laravel Reverb for a task manager?
Not for version one, but it makes a big difference once several people use the same board. Without it, users must refresh to see changes. Reverb is first-party and open source, with no per-message fees. You pay only for the server it runs on, so it's usually the simplest way to add real-time updates.
Should I build or buy a project management tool?
Buy if your team needs a general task tool. Build if you're selling a tool to a niche or if your workflow, such as client approvals, compliance checklists, or field work, doesn't fit general tools and you're paying per seat for workarounds.
Conclusion: Build the Workflow, Not the Boilerplate
A Laravel project management tool comes down to five parts: projects and roles, tasks with ordered positions, membership-based policies, real-time updates with Reverb, and notifications.
To recap:
Start with a focused version one and three roles: owner, member, and viewer.
Authorize every route and channel through project membership.
Reorder Kanban cards in a transaction with a row lock.
Broadcast small payloads on private channels, and keep a queue worker running.
Generate the boilerplate with AI, then spend your time on the workflow your customers will pay for.
Ravi's agency now runs every client project through its own portal, and clients approve work without ever opening a third-party tool. Yours can be next.
Want a working Laravel project management tool to build on today? Try StackMivo free → New accounts get 20 free credits, no card required.