Most project management tools are either too heavy or too simple. This project explores what a focused, real-time collaboration tool looks like when built from scratch — with a modern stack, database-level security, and no unnecessary complexity.
The Problem
Teams working on multiple projects need a shared space to track tasks, assign work, and see progress at a glance. The challenge is not just building the UI — it is handling multi-tenant data isolation, real-time state synchronization across clients, and a responsive drag-and-drop experience that feels instant even before the server confirms the update.
What It Does
The app covers the full lifecycle of project work within a workspace:
A user signs in with Google OAuth and creates a workspace. Projects are organized within the workspace. Each project has a Kanban board with four status columns — Todo, In Progress, In Review, Done. Issues can be assigned to multiple members, tagged with priority levels, and given due dates. Moving a card between columns updates the status instantly on all connected clients via Supabase Realtime. Workspace members can be invited via a shareable link — no email required.
Architecture
Built as a Next.js 16 App Router application with Supabase as the backend. Server Components handle data fetching. Server Actions handle all mutations — no separate API layer. The Kanban board is a Client Component that subscribes to real-time database changes via Supabase Realtime WebSockets.
Key Technical Decisions
Row Level Security is the core of the multi-tenant data model. Every table has RLS enabled — users can only read and write data within workspaces they belong to. Helper functions (is_workspace_member, is_workspace_admin_or_owner) are defined as SECURITY DEFINER to prevent recursive RLS evaluation when the helper itself queries a table that has RLS enabled.
Optimistic UI on the Kanban board — dragging a card between columns updates local state immediately via @dnd-kit, then calls updateIssueStatus in the background. If the server returns an error, the board rolls back to a pre-drag snapshot. The result is a drag experience that feels instant regardless of network latency.
Real-time subscriptions — the Kanban board subscribes to postgres_changes on the issues table filtered by project_id. INSERT, UPDATE, and DELETE events update local board state directly without a full page reload. Assignee changes trigger router.refresh() to rehydrate server component data with fresh join results.
Invite flow with auth redirect preservation — when an unauthenticated user opens an invite link, they are redirected to /login?next=/invite/[token]. After Google OAuth completes, the callback route reads the next param and redirects back to the invite page before falling through to the standard post-login routing logic.
Cookie-based Supabase client in Server Actions — getSession() does not forward the user JWT to PostgreSQL RLS in Server Action context. The correct pattern uses getClaims() with a createServerClient instance built from cookies() on every request, ensuring auth.uid() resolves correctly at the database level.