A modern, full-featured project management and task tracking application inspired by Trello. Built with Next.js 15, TypeScript, Supabase, and Clerk authentication.
- Secure authentication with Clerk (Email, Google, GitHub OAuth)
- Multi-organization support with role-based access control
- Create and manage unlimited boards
- Drag-and-drop task management with smooth animations
- Real-time collaboration and updates
- Global search across boards, tasks, and members
- Mobile-first responsive design
- Rich text descriptions with formatting toolbar
- Custom cover images and colors
- Labels and tags for organization
- Due dates and priority levels
- Multiple member assignment to tasks
- Checklists with progress tracking
- File attachments and links
- Comments and activity tracking
- Task watchers for notifications
- Task completion status
- Organization workspaces
- Team member invitations via email
- Role management (Owner, Admin, Member, Guest)
- Shared boards and tasks
- Activity feed and notifications
- Member profiles and avatars
- Intuitive board navigation widget
- Quick task creation and editing
- Keyboard shortcuts support
- Optimized loading and animations
- Dark mode compatible UI
- Toast notifications for actions
- Next.js 15 (App Router)
- React 19
- TypeScript
- Tailwind CSS
- Radix UI Components
- dnd-kit (Drag and Drop)
- Lucide Icons
- Next.js API Routes
- Supabase PostgreSQL
- Row Level Security (RLS)
- Server-side rendering
- Clerk Authentication
- OAuth providers (Google, GitHub)
- Protected routes and middleware
- Role-based access control
- Node.js 18 or higher
- npm or yarn package manager
- Supabase account (free tier available)
- Clerk account (free tier available)
- Clone the repository
git clone https://github.com/yourusername/tasko.git
cd tasko- Install dependencies
npm install- Set up environment variables
Create a .env.local file in the root directory with the following variables:
# Clerk Authentication
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=your_clerk_publishable_key
CLERK_SECRET_KEY=your_clerk_secret_key
NEXT_PUBLIC_CLERK_SIGN_IN_URL=/login
NEXT_PUBLIC_CLERK_SIGN_UP_URL=/sign-up
NEXT_PUBLIC_CLERK_AFTER_SIGN_IN_URL=/dashboard
NEXT_PUBLIC_CLERK_AFTER_SIGN_UP_URL=/dashboard
# Supabase
NEXT_PUBLIC_SUPABASE_URL=your_supabase_url
NEXT_PUBLIC_SUPABASE_ANON_KEY=your_supabase_anon_key
SUPABASE_SERVICE_ROLE_KEY=your_supabase_service_role_key
# App URL
NEXT_PUBLIC_APP_URL=http://localhost:3000
# Brevo Email Service (for organization invites)
BREVO_API_KEY=your_brevo_api_key
BREVO_SENDER_EMAIL=your_email@example.com
BREVO_SENDER_NAME=Your App Name- Set up Supabase database
Go to your Supabase dashboard, open the SQL Editor, and run the following schema:
-- Enable UUID extension
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
-- Organizations & memberships
CREATE TABLE IF NOT EXISTS organizations (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
name TEXT NOT NULL,
slug TEXT NOT NULL UNIQUE,
plan TEXT NOT NULL DEFAULT 'free',
created_by TEXT NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE TABLE IF NOT EXISTS organization_members (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
org_id UUID NOT NULL REFERENCES organizations(id) ON DELETE CASCADE,
user_id TEXT NOT NULL,
role TEXT NOT NULL DEFAULT 'member' CHECK (role IN ('owner','admin','member','guest')),
status TEXT NOT NULL DEFAULT 'active' CHECK (status IN ('active','invited','suspended')),
joined_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
UNIQUE(org_id, user_id)
);
CREATE TABLE IF NOT EXISTS organization_invites (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
org_id UUID NOT NULL REFERENCES organizations(id) ON DELETE CASCADE,
invited_email TEXT NOT NULL,
invited_by TEXT NOT NULL,
token TEXT NOT NULL UNIQUE,
status TEXT NOT NULL DEFAULT 'pending' CHECK (status IN ('pending','accepted','expired')),
expires_at TIMESTAMPTZ NOT NULL,
redirect_url TEXT,
metadata JSONB,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
-- Boards, lists, cards, activity
CREATE TABLE IF NOT EXISTS boards (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
title TEXT NOT NULL,
description TEXT,
color TEXT DEFAULT '#0079BF',
thumbnail_url TEXT,
background_image TEXT,
user_id TEXT,
org_id UUID REFERENCES organizations(id) ON DELETE CASCADE,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE TABLE IF NOT EXISTS columns (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
board_id UUID NOT NULL REFERENCES boards(id) ON DELETE CASCADE,
title TEXT NOT NULL,
sort_order INTEGER NOT NULL DEFAULT 0,
user_id TEXT,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE TABLE IF NOT EXISTS tasks (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
column_id UUID NOT NULL REFERENCES columns(id) ON DELETE CASCADE,
title TEXT NOT NULL,
description TEXT,
assignee TEXT,
due_date TIMESTAMPTZ,
priority TEXT DEFAULT 'medium' CHECK (priority IN ('low','medium','high')),
sort_order INTEGER NOT NULL DEFAULT 0,
is_completed BOOLEAN DEFAULT FALSE,
cover_image TEXT,
cover_color TEXT,
org_id UUID,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
-- Task members (for assigning multiple members to a task)
CREATE TABLE IF NOT EXISTS task_members (
task_id UUID NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,
user_id TEXT NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
PRIMARY KEY (task_id, user_id)
);
-- Labels
CREATE TABLE IF NOT EXISTS labels (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
board_id UUID NOT NULL REFERENCES boards(id) ON DELETE CASCADE,
name TEXT NOT NULL,
color TEXT NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE TABLE IF NOT EXISTS task_labels (
task_id UUID NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,
label_id UUID NOT NULL REFERENCES labels(id) ON DELETE CASCADE,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
PRIMARY KEY (task_id, label_id)
);
-- Checklists
CREATE TABLE IF NOT EXISTS checklists (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
task_id UUID NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,
title TEXT NOT NULL,
sort_order INTEGER NOT NULL DEFAULT 0,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE TABLE IF NOT EXISTS checklist_items (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
checklist_id UUID NOT NULL REFERENCES checklists(id) ON DELETE CASCADE,
title TEXT NOT NULL,
is_completed BOOLEAN DEFAULT FALSE,
sort_order INTEGER NOT NULL DEFAULT 0,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
-- Attachments
CREATE TABLE IF NOT EXISTS attachments (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
task_id UUID NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,
name TEXT NOT NULL,
url TEXT NOT NULL,
type TEXT NOT NULL CHECK (type IN ('file', 'link')),
size INTEGER,
created_by TEXT NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
-- Comments
CREATE TABLE IF NOT EXISTS comments (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
task_id UUID NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,
user_id TEXT NOT NULL,
content TEXT NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE TABLE IF NOT EXISTS comment_reactions (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
comment_id UUID NOT NULL REFERENCES comments(id) ON DELETE CASCADE,
user_id TEXT NOT NULL,
emoji TEXT NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
UNIQUE(comment_id, user_id, emoji)
);
-- Task watchers
CREATE TABLE IF NOT EXISTS task_watchers (
task_id UUID NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,
user_id TEXT NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
PRIMARY KEY (task_id, user_id)
);
-- Card activity
CREATE TABLE IF NOT EXISTS card_activity (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
card_id UUID NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,
performed_by TEXT NOT NULL,
action_type TEXT NOT NULL,
payload JSONB,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
-- Helper functions
CREATE OR REPLACE FUNCTION requesting_user_id()
RETURNS text AS $$
SELECT NULLIF(
current_setting('request.jwt.claims', true)::json->>'sub',
''
)::text;
$$ LANGUAGE SQL STABLE;
CREATE OR REPLACE FUNCTION is_org_member(target_org uuid)
RETURNS boolean AS $$
SELECT EXISTS (
SELECT 1
FROM organization_members
WHERE org_id = target_org
AND user_id = requesting_user_id()
AND status = 'active'
);
$$ LANGUAGE SQL STABLE;
CREATE OR REPLACE FUNCTION is_org_admin(target_org uuid)
RETURNS boolean AS $$
SELECT EXISTS (
SELECT 1
FROM organization_members
WHERE org_id = target_org
AND user_id = requesting_user_id()
AND status = 'active'
AND role IN ('owner', 'admin')
);
$$ LANGUAGE SQL STABLE;
-- Indexes
CREATE INDEX IF NOT EXISTS idx_boards_org_id ON boards(org_id);
CREATE INDEX IF NOT EXISTS idx_boards_user_id ON boards(user_id);
CREATE INDEX IF NOT EXISTS idx_columns_board_id ON columns(board_id);
CREATE INDEX IF NOT EXISTS idx_tasks_column_id ON tasks(column_id);
CREATE INDEX IF NOT EXISTS idx_tasks_sort_order ON tasks(sort_order);
CREATE INDEX IF NOT EXISTS idx_task_members_task_id ON task_members(task_id);
CREATE INDEX IF NOT EXISTS idx_task_members_user_id ON task_members(user_id);
CREATE INDEX IF NOT EXISTS idx_labels_board_id ON labels(board_id);
CREATE INDEX IF NOT EXISTS idx_task_labels_task_id ON task_labels(task_id);
CREATE INDEX IF NOT EXISTS idx_task_labels_label_id ON task_labels(label_id);
CREATE INDEX IF NOT EXISTS idx_checklists_task_id ON checklists(task_id);
CREATE INDEX IF NOT EXISTS idx_checklist_items_checklist_id ON checklist_items(checklist_id);
CREATE INDEX IF NOT EXISTS idx_attachments_task_id ON attachments(task_id);
CREATE INDEX IF NOT EXISTS idx_comments_task_id ON comments(task_id);
CREATE INDEX IF NOT EXISTS idx_comment_reactions_comment_id ON comment_reactions(comment_id);
CREATE INDEX IF NOT EXISTS idx_task_watchers_task_id ON task_watchers(task_id);
CREATE INDEX IF NOT EXISTS idx_task_watchers_user_id ON task_watchers(user_id);
CREATE INDEX IF NOT EXISTS idx_card_activity_card_id ON card_activity(card_id);
CREATE INDEX IF NOT EXISTS idx_organization_invites_email ON organization_invites(invited_email);
CREATE INDEX IF NOT EXISTS idx_organization_invites_token ON organization_invites(token);
CREATE INDEX IF NOT EXISTS idx_organization_members_org_id ON organization_members(org_id);
CREATE INDEX IF NOT EXISTS idx_organization_members_user_id ON organization_members(user_id);
-- Updated_at trigger
CREATE OR REPLACE FUNCTION update_updated_at_column()
RETURNS TRIGGER AS $$
BEGIN
NEW.updated_at = NOW();
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER update_boards_updated_at
BEFORE UPDATE ON boards
FOR EACH ROW
EXECUTE FUNCTION update_updated_at_column();
CREATE TRIGGER update_organizations_updated_at
BEFORE UPDATE ON organizations
FOR EACH ROW
EXECUTE FUNCTION update_updated_at_column();
-- Row Level Security
ALTER TABLE organizations ENABLE ROW LEVEL SECURITY;
ALTER TABLE organization_members ENABLE ROW LEVEL SECURITY;
ALTER TABLE organization_invites ENABLE ROW LEVEL SECURITY;
ALTER TABLE boards ENABLE ROW LEVEL SECURITY;
ALTER TABLE columns ENABLE ROW LEVEL SECURITY;
ALTER TABLE tasks ENABLE ROW LEVEL SECURITY;
ALTER TABLE card_activity ENABLE ROW LEVEL SECURITY;
-- Auto-create owner membership when org is created
CREATE OR REPLACE FUNCTION create_org_owner_membership()
RETURNS TRIGGER AS $$
BEGIN
INSERT INTO organization_members (org_id, user_id, role, status)
VALUES (NEW.id, NEW.created_by, 'owner', 'active');
RETURN NEW;
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;
CREATE TRIGGER auto_create_org_owner
AFTER INSERT ON organizations
FOR EACH ROW
EXECUTE FUNCTION create_org_owner_membership();
-- RLS Policies
CREATE POLICY "Members can view org" ON organizations
FOR SELECT USING (created_by = requesting_user_id());
CREATE POLICY "Owners manage org" ON organizations
FOR UPDATE USING (is_org_admin(id));
CREATE POLICY "Owners delete org" ON organizations
FOR DELETE USING (is_org_admin(id));
CREATE POLICY "Users create their org" ON organizations
FOR INSERT WITH CHECK (created_by = requesting_user_id());
CREATE POLICY "Members can view membership" ON organization_members
FOR SELECT USING (user_id = requesting_user_id());
CREATE POLICY "Admins invite members" ON organization_members
FOR INSERT WITH CHECK (true);
CREATE POLICY "Admins update member roles" ON organization_members
FOR UPDATE USING (is_org_admin(org_id));
CREATE POLICY "Admins remove members" ON organization_members
FOR DELETE USING (is_org_admin(org_id));
-- Organization invites RLS policy
-- Service role has full access (used by backend API routes)
CREATE POLICY "Service role full access" ON organization_invites
FOR ALL
USING (true)
WITH CHECK (true);
CREATE POLICY "Org members view boards" ON boards
FOR SELECT USING (
(org_id IS NOT NULL AND is_org_member(org_id))
OR (org_id IS NULL AND user_id = requesting_user_id())
);
CREATE POLICY "Org members insert boards" ON boards
FOR INSERT WITH CHECK (
(org_id IS NOT NULL AND is_org_member(org_id))
OR (org_id IS NULL AND user_id = requesting_user_id())
);
CREATE POLICY "Org members update boards" ON boards
FOR UPDATE USING (
(org_id IS NOT NULL AND is_org_member(org_id))
OR (org_id IS NULL AND user_id = requesting_user_id())
);
CREATE POLICY "Org members delete boards" ON boards
FOR DELETE USING (
(org_id IS NOT NULL AND is_org_member(org_id))
OR (org_id IS NULL AND user_id = requesting_user_id())
);- Run the development server
npm run dev- Open your browser and navigate to http://localhost:3000
npm run dev # Start development server on port 3000
npm run build # Build production bundle
npm start # Start production server
npm run lint # Run ESLint for code quality- Push your code to GitHub
git init
git add .
git commit -m "Initial commit"
git branch -M main
git remote add origin https://github.com/yourusername/tasko.git
git push -u origin main-
Import your project in Vercel
- Go to https://vercel.com
- Click "New Project"
- Import your GitHub repository
- Vercel will auto-detect Next.js
-
Configure environment variables
- Add all variables from your
.env.localfile - Update
NEXT_PUBLIC_APP_URLto your Vercel URL
- Add all variables from your
-
Deploy
- Click "Deploy"
- Wait for build to complete
IMPORTANT: After deployment, update your Clerk dashboard settings:
- Go to https://dashboard.clerk.com
- Select your application
- Navigate to "Paths" or "URLs" settings
- Add your production URLs:
- Authorized redirect URLs:
https://your-app.vercel.app/sso-callback - Sign-in URL:
https://your-app.vercel.app/login - Sign-up URL:
https://your-app.vercel.app/sign-up - After sign-in URL:
https://your-app.vercel.app/dashboard - After sign-up URL:
https://your-app.vercel.app/dashboard
- Authorized redirect URLs:
Without these settings, OAuth authentication will fail.
Tasko uses Brevo (formerly Sendinblue) for sending organization invitation emails.
-
Sign up for Brevo
- Go to https://app.brevo.com
- Create a free account (300 emails/day, no domain verification required)
-
Get your API Key
- Navigate to SMTP & API → API Keys
- Click Create a new API key
- Copy the API key (starts with
xkeysib-)
-
Add to environment variables
BREVO_API_KEY=xkeysib-your_api_key_here BREVO_SENDER_EMAIL=your_email@example.com BREVO_SENDER_NAME=Tasko
-
For production (Vercel)
- Add the same three variables to Vercel environment variables
- Redeploy your application
Note: The free tier allows 300 emails/day without domain verification, perfect for development and small teams.
tasko/
├── app/ # Next.js 15 App Router
│ ├── api/ # API routes (REST endpoints)
│ │ ├── boards/ # Board CRUD operations
│ │ ├── tasks/ # Task management
│ │ ├── comments/ # Comments system
│ │ ├── labels/ # Labels and tags
│ │ ├── organizations/ # Organization management
│ │ └── ...
│ ├── boards/[id]/ # Dynamic board pages
│ ├── dashboard/ # User dashboard
│ ├── login/ # Authentication pages
│ ├── sign-up/ # Registration pages
│ └── ...
├── components/
│ ├── features/ # Feature-specific components
│ │ ├── tasks/ # Task cards, modals
│ │ ├── boards/ # Board components
│ │ └── organizations/ # Org management
│ ├── layout/ # Layout components
│ │ ├── navbar/ # Navigation
│ │ └── dashboard/ # Dashboard layout
│ ├── ui/ # Reusable UI components
│ └── shared/ # Shared utilities
├── lib/
│ ├── contexts/ # React contexts
│ ├── hooks/ # Custom React hooks
│ ├── supabase/ # Supabase client & models
│ └── utils/ # Utility functions
├── public/ # Static assets
└── middleware.ts # Next.js middleware (auth)
GET /api/boards- List all boardsPOST /api/boards- Create new boardGET /api/boards/[id]- Get board detailsPATCH /api/boards/[id]- Update boardDELETE /api/boards/[id]- Delete board
GET /api/tasks- List tasksPOST /api/tasks- Create taskGET /api/tasks/[id]- Get task detailsPATCH /api/tasks/[id]- Update taskDELETE /api/tasks/[id]- Delete taskGET /api/tasks/[id]/activity- Get task activityGET /api/tasks/[id]/details- Get full task details
GET /api/organizations- List user organizationsPOST /api/organizations- Create organizationGET /api/organizations/[orgId]/members- List membersPOST /api/organizations/[orgId]/invites- Send invitationGET /api/organizations/[orgId]/invites- List invitations
POST /api/tasks/[id]/members- Assign member to task- Body:
{ "user_id": "user_clerk_id" } - Validates user is organization member
- Prevents duplicate assignments
- Body:
DELETE /api/tasks/[id]/members?user_id=xxx- Remove member from task- Query param:
user_id(required) - Logs activity for audit trail
- Query param:
Contributions are welcome! Please follow these steps:
- Fork the repository
- Create a feature branch (
git checkout -b feature/amazing-feature) - Commit your changes (
git commit -m 'Add amazing feature') - Push to the branch (
git push origin feature/amazing-feature) - Open a Pull Request
Please ensure your code:
- Follows the existing code style
- Includes appropriate comments
- Passes all linting checks (
npm run lint) - Builds successfully (
npm run build)
This project is licensed under the MIT License.
- Inspired by Trello's intuitive project management interface
- Built with modern web technologies and best practices
- Community contributions and feedback
For issues, questions, or feature requests:
- Open an issue on GitHub
- Check existing issues for solutions
- Provide detailed information for bug reports
Future enhancements planned:
- Real-time collaborative editing
- Advanced filtering and sorting
- Custom fields and templates
- Time tracking integration
- Mobile native applications
- Advanced analytics and reporting
- Automation and webhooks
- Third-party integrations
Built with care by the Tasko team


