Skip to content

Repository files navigation

Crewber

TypeScript Next.js Node.js Socket.IO PostgreSQL

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.


Overview

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.

Features

  • 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

Tech Stack

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

Architecture

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
Loading

Key design decisions:

  1. DB-first real-time — messages and shifts are always written to PostgreSQL first; Socket.IO only broadcasts the change. Chat history loads via REST.
  2. Rooms per crew — each crew gets a Socket.IO room (crew:{id}); membership is verified on join.
  3. Shared contractspackages/shared holds Zod schemas, TypeScript types, and socket event names used by both apps.
  4. Stateless auth — JWT means the API can scale horizontally (with a Redis adapter for Socket.IO later).

Database Schema

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])
}

API Reference

All routes prefixed with /api. Auth column: required access level.

Auth

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

Crews

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

Shifts

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

Messages

Method Endpoint Description Auth
GET /crews/:id/messages?cursor=&limit= Paginated chat history Member

Real-Time Events

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.


Project Structure

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

Getting Started

Prerequisites

  • Node.js 20+
  • Git
  • Docker (for local Postgres) — or a free Neon / Supabase account

Setup

# 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:4000

Environment Variables

apps/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"

Deployment (Free Tier)

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.


Scalability Notes

  • Cursor-based pagination on message history
  • DB indexes on crewId + createdAt / start
  • Stateless JWT auth → API can run multiple instances
  • @socket.io/redis-adapter ready to add for horizontal socket scaling
  • Rate limiting + helmet + Zod validation on all inputs

Roadmap

  • 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

License

MIT — free to use for learning and portfolios.

About

Real-time crew management & collaboration platform

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages