Real-time crew management & collaboration platform.
Crewber brings team scheduling, member management, and communication into one place. Create a crew, assign shifts, and chat — everything syncs live across all connected members.
In active development — see Roadmap.
Coordinating a team ("crew") usually happens across spreadsheets, group chats, and phone calls. Crewber replaces that with a single platform:
- Crews — private team spaces with role-based membership (Owner / Admin / Member).
- Shifts — create, assign, and track scheduled work.
- Chat — persistent, real-time messaging per crew with paginated history.
- Presence — see who is online right now.
- Authentication — JWT in httpOnly cookie, bcrypt-hashed passwords
- Crew management — create, invite, remove members, role-based permissions
- Shift scheduling — create, assign, update, and complete shifts
- Real-time chat — instant delivery + persistent, paginated history
- Live updates — schedule and membership changes sync to all clients instantly
- Online presence — live online/offline indicators per crew
- Responsive UI — mobile-first design
| Layer | Technology |
|---|---|
| Frontend | Next.js (App Router), React, TypeScript, Tailwind CSS |
| State | TanStack Query (server state), Zustand (client state) |
| Real-time | Socket.IO |
| Backend | Node.js, Express, TypeScript |
| Database | PostgreSQL + Prisma ORM |
| Auth | JWT (httpOnly cookie) + bcrypt |
| Validation | Zod (shared schemas, client + server) |
| Tooling | Turborepo monorepo, ESLint, Prettier |
Turborepo monorepo with a separate stateful API server (required for WebSockets — serverless platforms like Vercel cannot host Socket.IO).
flowchart LR
client["Browser · Next.js + React"]
api["Node.js API · Express + Socket.IO"]
db[("PostgreSQL · Prisma")]
client -- "REST (HTTP)" --> api
client -- "WebSocket" --> api
api --> db
Key design decisions:
- DB-first real-time — messages and shifts are always written to PostgreSQL first; Socket.IO only broadcasts the change. Chat history loads via REST.
- Rooms per crew — each crew gets a Socket.IO room (
crew:{id}); membership is verified on join. - Shared contracts —
packages/sharedholds Zod schemas, TypeScript types, and socket event names used by both apps. - Stateless auth — JWT means the API can scale horizontally (with a Redis adapter for Socket.IO later).
enum Role {
OWNER
ADMIN
MEMBER
}
enum Status {
OPEN
ASSIGNED
DONE
}
model User {
id String @id @default(cuid())
name String
email String @unique
passwordHash String
createdAt DateTime @default(now())
ownedCrews Crew[] @relation("OwnedCrews")
memberships CrewMember[]
shifts Shift[]
messages Message[]
}
model Crew {
id String @id @default(cuid())
name String
ownerId String
owner User @relation("OwnedCrews", fields: [ownerId], references: [id])
members CrewMember[]
shifts Shift[]
messages Message[]
createdAt DateTime @default(now())
}
model CrewMember {
crewId String
userId String
role Role @default(MEMBER)
joinedAt DateTime @default(now())
crew Crew @relation(fields: [crewId], references: [id], onDelete: Cascade)
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
@@id([crewId, userId])
}
model Shift {
id String @id @default(cuid())
crewId String
title String
start DateTime
end DateTime
status Status @default(OPEN)
assigneeId String?
crew Crew @relation(fields: [crewId], references: [id], onDelete: Cascade)
assignee User? @relation(fields: [assigneeId], references: [id], onDelete: SetNull)
@@index([crewId, start])
}
model Message {
id String @id @default(cuid())
crewId String
senderId String
content String
createdAt DateTime @default(now())
crew Crew @relation(fields: [crewId], references: [id], onDelete: Cascade)
sender User @relation(fields: [senderId], references: [id], onDelete: Cascade)
@@index([crewId, createdAt])
}All routes prefixed with /api. Auth column: required access level.
| Method | Endpoint | Description | Auth |
|---|---|---|---|
| POST | /auth/register |
Create account | Public |
| POST | /auth/login |
Login, sets JWT cookie | Public |
| POST | /auth/logout |
Clears cookie | Auth |
| GET | /auth/me |
Current user profile | Auth |
| Method | Endpoint | Description | Auth |
|---|---|---|---|
| GET | /crews |
List my crews | Auth |
| POST | /crews |
Create crew (creator = OWNER) | Auth |
| GET | /crews/:id |
Crew details + members | Member |
| PATCH | /crews/:id |
Update crew name/description | Admin |
| DELETE | /crews/:id |
Delete crew | Owner |
| POST | /crews/:id/members |
Add / invite member | Admin |
| PATCH | /crews/:id/members/:userId |
Change member role | Owner |
| DELETE | /crews/:id/members/:userId |
Remove member | Admin |
| Method | Endpoint | Description | Auth |
|---|---|---|---|
| GET | /crews/:id/shifts |
List shifts (date-range filter) | Member |
| POST | /crews/:id/shifts |
Create shift | Admin |
| PATCH | /shifts/:id |
Update / assign shift | Admin |
| DELETE | /shifts/:id |
Delete shift | Admin |
| Method | Endpoint | Description | Auth |
|---|---|---|---|
| GET | /crews/:id/messages?cursor=&limit= |
Paginated chat history | Member |
| Event | Direction | Payload | Description |
|---|---|---|---|
crew:join |
C → S | { crewId } |
Server verifies membership, joins room |
message:send |
C → S | { crewId, content } |
Persist message, then broadcast |
message:new |
S → C | Message |
New chat message |
shift:created |
S → C | Shift |
Live schedule update |
shift:updated |
S → C | Shift |
Live schedule update |
shift:deleted |
S → C | { shiftId } |
Live schedule update |
member:joined |
S → C | CrewMember |
Live member list |
member:left |
S → C | { userId } |
Live member list |
presence:update |
S → C | { userId, online } |
Online/offline indicator |
Socket authentication: JWT passed in the handshake (io(url, { auth: { token } })), verified server-side before any room join.
crewber/
├─ apps/
│ ├─ web/ # Next.js frontend (Vercel)
│ │ ├─ app/
│ │ │ ├─ (auth)/
│ │ │ │ ├─ login/page.tsx
│ │ │ │ └─ register/page.tsx
│ │ │ ├─ dashboard/page.tsx
│ │ │ ├─ crew/[crewId]/page.tsx
│ │ │ └─ layout.tsx
│ │ ├─ components/ # UI components
│ │ ├─ hooks/ # useSocket, usePresence, ...
│ │ └─ lib/ # api client, socket client
│ └─ api/ # Express + Socket.IO backend (Render)
│ ├─ prisma/
│ │ ├─ schema.prisma
│ │ └─ seed.ts
│ └─ src/
│ ├─ routes/ # auth, crews, shifts, messages
│ ├─ sockets/ # socket server + event handlers
│ ├─ middleware/ # authGuard, roleGuard, rateLimit
│ ├─ lib/ # prisma client, jwt utils
│ └─ server.ts
├─ packages/
│ └─ shared/ # zod schemas, types, socket event names
├─ docker-compose.yml # local PostgreSQL
├─ turbo.json
└─ package.json
# 1. Clone
git clone https://github.com/<your-username>/crewber.git
cd crewber
# 2. Install dependencies
npm install
# 3. Environment variables
cp apps/api/.env.example apps/api/.env
cp apps/web/.env.example apps/web/.env
# 4. Start local database (skip if using Neon/Supabase)
docker compose up -d db
# 5. Migrate + seed (from `apps/api/`)
cd apps/api
npm run db:migrate
npm run db:seed
cd ../..
# 6. Run both apps
npm run dev
# web → http://localhost:3000
# api → http://localhost:4000apps/api/.env
DATABASE_URL="postgresql://user:password@localhost:5432/crewber"
JWT_SECRET="change-me-in-production"
COOKIE_NAME="crewber_token"
PORT=4000
CLIENT_ORIGIN="http://localhost:3000"apps/web/.env
NEXT_PUBLIC_API_URL="http://localhost:4000"
NEXT_PUBLIC_SOCKET_URL="http://localhost:4000"| Piece | Host | Notes |
|---|---|---|
| Frontend | Vercel (Hobby) | Native Next.js support |
| Backend | Render / Koyeb | Must be a stateful server — WebSockets don't run on serverless |
| Database | Neon / Supabase | Free Postgres, works with Prisma |
Free backend tiers sleep after inactivity, which drops WebSocket connections. For a portfolio project this is fine — use UptimeRobot (free) to ping the API every 5 minutes and keep it awake.
- Cursor-based pagination on message history
- DB indexes on
crewId+createdAt/start - Stateless JWT auth → API can run multiple instances
@socket.io/redis-adapterready to add for horizontal socket scaling- Rate limiting + helmet + Zod validation on all inputs
- Project documentation & architecture (this file)
- Monorepo + tooling setup (Turborepo, TS, ESLint, Prettier)
- Prisma schema, migrations, seed script
- Auth API (register / login / me)
- Crews API + role guards
- Shifts API
- Messages API with pagination
- Socket.IO layer (chat, live shifts, presence)
- Frontend: auth pages
- Frontend: dashboard + crew space (schedule board, chat, members)
- Responsive polish, loading / empty / error states
- Deploy: Vercel + Render + Neon
MIT — free to use for learning and portfolios.