A real-time collaborative workspace platform where teams manage tasks, invite members, and see live updates across every connected browser — built with Next.js 16, PostgreSQL, and WebSocket pub/sub.
- Why This Project Exists
- Demo
- Key Features
- System Architecture
- Realtime Data Flow
- Database Schema
- Tech Stack
- Project Structure
- Getting Started
- Environment Variables
- Server Actions & Events
- Design Decisions
- Resilience & Fallbacks
- Scripts
- Roadmap
- Author
Most task boards are single-user CRUD apps. Real collaboration requires three hard problems solved together:
- Authoritative persistence — every change must survive refreshes and crashes
- Low-latency fan-out — User A's update must reach User B in under ~100ms
- Multi-tenant security — workspaces, roles, and membership must be enforced on every mutation
Syncro Flow is a production-shaped answer: PostgreSQL as the source of truth, Server Actions for validated mutations, and Pusher WebSockets for sub-second cross-client synchronization — with automatic polling fallback when the realtime channel drops.
Quick test (local):
- Open
http://localhost:3000in two browser windows - Sign in as two different users in the same workspace
- Create or update a task in one window — watch it appear instantly in the other
| Feature | Description |
|---|---|
| Multi-tenant workspaces | Isolated workspaces with owner-scoped onboarding |
| RBAC | owner · admin · member roles with enforced server-side checks |
| Kanban task board | Todo → In Progress → Done → Blocked columns with inline status & assignee updates |
| Live activity feed | Audit log of task, member, and workspace events |
| Member management | Invite by email, list members, remove (owner/admin) |
| Realtime sync | Pusher pub/sub per workspace channel (workspace-{id}) |
| Graceful degradation | Auto polling every 5s when WebSocket disconnects |
| Type-safe mutations | Zod validation + unified createAction wrapper on all server actions |
| Clerk webhooks | Automatic user provisioning into PostgreSQL on sign-up |
flowchart TB
subgraph Clients["Browser Clients"]
UA["User A<br/>(React 19)"]
UB["User B<br/>(React 19)"]
end
subgraph Vercel["Next.js 16 App (Vercel)"]
MW["Clerk Middleware<br/>Session Guard"]
SA["Server Actions<br/>Zod + Auth"]
WH["Clerk Webhook<br/>/api/webhooks/clerk"]
RSC["React Server Components<br/>Initial Data Load"]
WP["WorkspaceProvider<br/>Client State + Realtime"]
end
subgraph External["Managed Services"]
CLERK["Clerk<br/>Authentication"]
PUSHER["Pusher<br/>WebSocket Pub/Sub"]
PG[("PostgreSQL<br/>Source of Truth")]
REDIS[("Upstash Redis<br/>Ready for cache / pub-sub")]
end
UA -->|"HTTPS + Server Actions"| MW
UB -->|"HTTPS + Server Actions"| MW
MW --> SA
MW --> RSC
RSC -->|"Drizzle ORM"| PG
SA -->|"Write + Read"| PG
SA -->|"trigger()"| PUSHER
WH -->|"user.created"| PG
CLERK -->|"JWT / Webhooks"| MW
CLERK --> WH
PUSHER -->|"task-created<br/>task-status-updated<br/>task-assigned"| WP
WP --> UA
WP --> UB
SA -.->|"future: cache layer"| REDIS
style PG fill:#336791,color:#fff
style PUSHER fill:#300D4F,color:#fff
style CLERK fill:#6C47FF,color:#fff
When User A updates a task, this is the end-to-end path:
sequenceDiagram
autonumber
participant A as User A Browser
participant SA as Server Action
participant PG as PostgreSQL
participant PS as Pusher
participant B as User B Browser
participant WP as WorkspaceProvider
A->>SA: updateTaskStatus({ taskId, status })
SA->>SA: Clerk auth() + Zod validate
SA->>SA: Membership check (workspace_members)
SA->>PG: UPDATE tasks SET status = ...
PG-->>SA: Updated row
SA->>PG: INSERT activity_log
SA->>PS: trigger("workspace-{id}", "task-status-updated")
SA-->>A: { success, data }
PS-->>WP: WebSocket event
WP->>WP: Merge into local task state
WP->>WP: Append to activity feed
WP-->>B: UI re-renders (< 100ms target)
| Step | Action | Latency |
|---|---|---|
| 1 | User A → Server (HTTP / Server Action) | ~30–40ms |
| 2 | Auth + Zod validation + membership check | ~5ms |
| 3 | PostgreSQL write + activity log | ~10–20ms |
| 4 | Pusher trigger → subscriber fan-out | ~2–5ms |
| 5 | User B WebSocket receive → React render | ~20–40ms |
| Total (typical) | ~70–110ms |
erDiagram
users ||--o{ workspace_members : "belongs to"
users ||--o{ workspaces : "owns"
workspaces ||--o{ workspace_members : "has"
workspaces ||--o{ tasks : "contains"
workspaces ||--o{ activity_log : "logs"
users ||--o{ tasks : "assigned to"
users ||--o{ tasks : "created by"
users ||--o{ activity_log : "performed by"
users {
varchar id PK
varchar email UK
varchar name
varchar avatar
timestamp created_at
}
workspaces {
uuid id PK
varchar name
varchar owner_id FK
timestamp created_at
}
workspace_members {
varchar user_id PK,FK
uuid workspace_id PK,FK
enum role
timestamp joined_at
}
tasks {
uuid id PK
uuid workspace_id FK
varchar title
text description
enum status
enum priority
varchar assignee_id FK
varchar created_by FK
timestamp created_at
timestamp updated_at
}
activity_log {
uuid id PK
uuid workspace_id FK
varchar user_id FK
varchar action
varchar entity_type
uuid entity_id
jsonb metadata
timestamp created_at
}
Enums: roles (owner, admin, member) · task_status (todo, in_progress, done, blocked) · priority (low, medium, high)
| Layer | Technology | Why |
|---|---|---|
| Framework | Next.js 16 (App Router) | Server Components, Server Actions, parallel routes |
| Language | TypeScript 5 | End-to-end type safety |
| UI | React 19 + Tailwind CSS 4 + shadcn/ui | Accessible, composable components |
| Auth | Clerk | OAuth, sessions, webhooks, production-ready |
| Database | PostgreSQL + Drizzle ORM | ACID transactions, foreign keys, relational queries |
| Realtime | Pusher (WebSockets) | Managed pub/sub without operating socket servers |
| Cache (ready) | Upstash Redis + ioredis | Configured for future caching / Redis pub-sub |
| Validation | Zod 4 | Runtime input validation on every mutation |
collab-dashboard/
├── app/
│ ├── (auth)/ # Clerk sign-in / sign-up
│ ├── (dashboard)/
│ │ └── workspace/[id]/
│ │ ├── @tasks/ # Parallel route — assigned tasks panel
│ │ ├── @activity/ # Parallel route — live activity feed
│ │ ├── members/ # Member invite & management
│ │ ├── layout.tsx # Server fetch → WorkspaceProvider
│ │ └── page.tsx # Kanban board + stats
│ ├── actions/
│ │ ├── tasks.ts # createTask, updateTaskStatus, assignTask
│ │ ├── workspace.ts # inviteMember, removeMember
│ │ ├── onboarding.ts # Workspace creation flow
│ │ └── queries.ts # Read queries with membership guards
│ ├── api/webhooks/clerk/ # User sync webhook (Svix verified)
│ ├── hooks/useWorkspace.realtime.ts
│ ├── lib/
│ │ ├── db/schema.ts # Drizzle schema + relations
│ │ ├── pusher/server.ts # Server-only Pusher SDK
│ │ ├── pusher/client.ts # Browser-only pusher-js
│ │ └── safe.actions.ts # createAction auth + Zod wrapper
│ ├── providers/workspace-provider.tsx # State + realtime + polling fallback
│ └── onboarding/
├── components/
│ ├── tasks/ # TaskBoard, TaskCard, CreateTaskDialog
│ └── workspace/ # Sidebar, ActivityFeed, InviteMemberForm
└── drizzle.config.ts
- Node.js 20+
- PostgreSQL database (Neon, Supabase, or local)
- Clerk application
- Pusher Channels app
- (Optional) Upstash Redis for future cache layer
git clone https://github.com/Durga1534/collab-dashboard.git
cd collab-dashboard
npm install# Push schema to your PostgreSQL instance
npm run db:push
# Or generate + run migrations
npm run db:generate
npm run db:migrate
# Open Drizzle Studio (optional)
npm run db:studionpm run devOpen http://localhost:3000.
- In Clerk Dashboard → Webhooks → Add endpoint:
https://your-domain/api/webhooks/clerk - Subscribe to:
user.created,user.updated,user.deleted - Copy the signing secret to
CLERK_WEBHOOK_SECRET
For local development, use ngrok or Clerk's dev tooling to forward webhooks.
Create .env.local:
# ─── Database ───────────────────────────────────────────
DATABASE_URL=postgresql://user:password@host:5432/dbname?sslmode=require
# ─── Clerk ──────────────────────────────────────────────
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=pk_test_...
CLERK_SECRET_KEY=sk_test_...
CLERK_WEBHOOK_SECRET=whsec_...
# ─── Pusher (Realtime) ──────────────────────────────────
NEXT_PUBLIC_PUSHER_APP_ID=your_app_id
NEXT_PUBLIC_PUSHER_KEY=your_key
NEXT_PUBLIC_PUSHER_CLUSTER=ap2
PUSHER_SECRET=your_secret
# ─── Upstash Redis (optional — future cache layer) ──────
UPSTASH_REDIS_URL=rediss://...
UPSTASH_REDIS_REST_URL=https://...
UPSTASH_REDIS_REST_TOKEN=...| Action | File | Auth Check |
|---|---|---|
createTask |
actions/tasks.ts |
Workspace membership |
updateTaskStatus |
actions/tasks.ts |
Workspace membership |
assignTask |
actions/tasks.ts |
Membership + assignee in workspace |
createWorkspace |
actions/onboarding.ts |
Authenticated user |
inviteMember |
actions/workspace.ts |
Workspace owner only |
removeMember |
actions/workspace.ts |
Owner or admin |
| Channel | Event | Payload |
|---|---|---|
workspace-{id} |
task-created |
{ taskId, title, priority, assigneeId, ... } |
workspace-{id} |
task-status-updated |
{ taskId, status, updatedBy } |
workspace-{id} |
task-assigned |
{ taskId, assigneeId, updatedBy } |
Every mutation follows the same pattern:
Validate (Zod) → Authorize (membership) → Persist (PostgreSQL) → Broadcast (Pusher) → Audit (activity_log)
Tasks don't exist in isolation — they belong to users, workspaces, and permission boundaries. PostgreSQL gives:
- Foreign key integrity — no orphaned tasks or dangling assignees
- ACID transactions — task update + activity log succeed or fail together
- Relational queries — Drizzle
with: { assignee: true }in a single round-trip
- Colocated with the UI, no separate API contract to maintain
- Clerk
auth()available without manual token parsing - Zod validation at the boundary via
createActionhelper - Type-safe return shape:
{ success: true, data } | { success: false, error }
- No socket server to deploy, scale, or monitor
- Channel-based pub/sub maps directly to
workspace-{id} - Server SDK (
pusher) and client SDK (pusher-js) are split into separate files to avoid SSR bundling issues
pusher-js is browser-only. Importing it from server action files causes default is not a constructor at runtime. Fix:
app/lib/pusher/server.ts → used by Server Actions
app/lib/pusher/client.ts → "use client", used by hooks only
| Failure | Behavior |
|---|---|
| Pusher disconnects | Sidebar shows "Polling" badge; client refetches every 5s |
| Pusher reconnects | Sidebar shows "Live" badge; polling stops |
| User not in workspace | Server action returns error; layout redirects to /onboarding |
| Invalid input | Zod rejects before any DB write |
| Redis unavailable | App continues — Redis is not on the critical path today |
See SYSTEM_DESIGN.md for deeper notes on Redis Sentinel, exponential backoff, and HA patterns.
| Command | Description |
|---|---|
npm run dev |
Start development server |
npm run build |
Production build |
npm run start |
Start production server |
npm run lint |
Run ESLint |
npm run db:push |
Push schema to database |
npm run db:generate |
Generate Drizzle migrations |
npm run db:migrate |
Run migrations |
npm run db:studio |
Open Drizzle Studio GUI |
- Drag-and-drop Kanban columns
- Task edit / delete
- Redis caching for read-heavy queries
- Redis pub/sub as Pusher alternative (self-hosted path)
- Optimistic UI with rollback on mutation failure
- E2E tests (Playwright) for realtime two-tab sync
- Workspace settings & rename
- Deploy to Vercel with CI (GitHub Actions)
Konduru Durga Prasad
Full Stack Developer · Bangalore, India
Built with Next.js, PostgreSQL, and a focus on real-time collaboration at scale.