Task Management System

Trello-Style Board with GraphQL Subscriptions & Real-Time Updates

📋 Project Overview

A Task Management System inspired by Trello, built with GraphQL Subscriptions for real-time collaboration. This application allows teams to organize tasks using boards, lists, and cards with drag-and-drop functionality, real-time updates, and collaborative features.

🎯 Key Objectives

  • Real-Time Collaboration: Multiple users can work simultaneously with instant updates
  • Drag & Drop: Intuitive card movement between lists and boards
  • GraphQL Subscriptions: WebSocket-based live updates for all changes
  • Rich Task Features: Labels, due dates, attachments, comments, and checklists
  • Team Management: User roles, permissions, and workspace organization

🏗️ System Architecture

React Frontend

Apollo Client + Subscriptions

GraphQL Server

Apollo Server + WebSocket

Business Logic

Resolvers & Services

PubSub Engine

Redis PubSub

PostgreSQL

Main Database

Redis

Cache & PubSub

S3

File Storage

5
Main Entities
8
Subscriptions
25+
Mutations
100%
Real-Time

✨ Key Features

Comprehensive task management with real-time collaboration and rich functionality.

📊

Boards & Lists

Organize work with unlimited boards and lists. Each board can have custom backgrounds, visibility settings, and member permissions.

🎴

Smart Cards

Rich task cards with descriptions, labels, due dates, attachments, checklists, and activity history. Full markdown support for descriptions.

🔄

Drag & Drop

Smooth drag-and-drop interface for moving cards between lists, reordering items, and organizing boards with optimistic UI updates.

Real-Time Updates

GraphQL subscriptions provide instant synchronization across all connected clients. See changes as they happen without refreshing.

💬

Comments & Activity

Threaded comments on cards with @mentions, emoji reactions, and complete activity log showing all changes with timestamps.

👥

Team Collaboration

Invite team members, assign cards, set permissions (admin/member/viewer), and track who's working on what in real-time.

🏷️

Labels & Filters

Color-coded labels for categorization, advanced filtering by assignee, due date, labels, and full-text search across cards.

📎

File Attachments

Upload images, documents, and files to cards. Preview images inline, support for various file types with S3 storage integration.

Checklists

Break down tasks with nested checklists. Track progress with completion percentages and convert checklist items to cards.

📝 GraphQL Schema

Complete type definitions for the task management system with boards, lists, cards, and user management.

Core Types

type Board {
  id: ID!
  title: String!
  description: String
  background: String
  visibility: BoardVisibility!
  createdAt: DateTime!
  updatedAt: DateTime!
  owner: User!
  members: [BoardMember!]!
  lists: [List!]!
  labels: [Label!]!
  starred: Boolean!
}

enum BoardVisibility {
  PRIVATE
  WORKSPACE
  PUBLIC
}

type BoardMember {
  id: ID!
  user: User!
  role: MemberRole!
  joinedAt: DateTime!
}

enum MemberRole {
  ADMIN
  MEMBER
  VIEWER
}

type List {
  id: ID!
  title: String!
  position: Int!
  board: Board!
  cards: [Card!]!
  cardCount: Int!
  createdAt: DateTime!
}

type Card {
  id: ID!
  title: String!
  description: String
  position: Int!
  list: List!
  board: Board!
  labels: [Label!]!
  members: [User!]!
  dueDate: DateTime
  completed: Boolean!
  attachments: [Attachment!]!
  checklists: [Checklist!]!
  comments: [Comment!]!
  activity: [Activity!]!
  createdBy: User!
  createdAt: DateTime!
  updatedAt: DateTime!
}

type Label {
  id: ID!
  name: String!
  color: String!
  board: Board!
}

type Attachment {
  id: ID!
  filename: String!
  url: String!
  mimeType: String!
  size: Int!
  uploadedBy: User!
  uploadedAt: DateTime!
}

type Checklist {
  id: ID!
  title: String!
  position: Int!
  items: [ChecklistItem!]!
  completedCount: Int!
  totalCount: Int!
}

type ChecklistItem {
  id: ID!
  text: String!
  completed: Boolean!
  position: Int!
  assignee: User
  dueDate: DateTime
}

type Comment {
  id: ID!
  text: String!
  author: User!
  card: Card!
  createdAt: DateTime!
  updatedAt: DateTime
  reactions: [Reaction!]!
}

type Reaction {
  emoji: String!
  users: [User!]!
  count: Int!
}

type Activity {
  id: ID!
  type: ActivityType!
  user: User!
  card: Card!
  description: String!
  metadata: JSON
  createdAt: DateTime!
}

enum ActivityType {
  CARD_CREATED
  CARD_UPDATED
  CARD_MOVED
  CARD_ARCHIVED
  COMMENT_ADDED
  MEMBER_ADDED
  MEMBER_REMOVED
  LABEL_ADDED
  LABEL_REMOVED
  DUE_DATE_SET
  DUE_DATE_CHANGED
  ATTACHMENT_ADDED
  CHECKLIST_ADDED
  CHECKLIST_ITEM_COMPLETED
}

type User {
  id: ID!
  email: String!
  username: String!
  fullName: String!
  avatar: String
  boards: [Board!]!
  createdAt: DateTime!
}

scalar DateTime
scalar JSON

Queries

type Query {
  # User queries
  me: User!
  user(id: ID!): User
  
  # Board queries
  board(id: ID!): Board
  boards(
    filter: BoardFilter
    sort: BoardSort
    limit: Int
    offset: Int
  ): BoardConnection!
  starredBoards: [Board!]!
  recentBoards(limit: Int): [Board!]!
  
  # List queries
  list(id: ID!): List
  
  # Card queries
  card(id: ID!): Card
  searchCards(
    boardId: ID!
    query: String!
    filters: CardFilters
  ): [Card!]!
  myCards(
    status: CardStatus
    sortBy: CardSort
  ): [Card!]!
}

input BoardFilter {
  visibility: BoardVisibility
  memberId: ID
  starred: Boolean
}

enum BoardSort {
  CREATED_DESC
  CREATED_ASC
  UPDATED_DESC
  TITLE_ASC
  TITLE_DESC
}

type BoardConnection {
  edges: [BoardEdge!]!
  pageInfo: PageInfo!
  totalCount: Int!
}

type BoardEdge {
  node: Board!
  cursor: String!
}

type PageInfo {
  hasNextPage: Boolean!
  hasPreviousPage: Boolean!
  startCursor: String
  endCursor: String
}

input CardFilters {
  labelIds: [ID!]
  memberIds: [ID!]
  dueDateRange: DateRange
  completed: Boolean
}

input DateRange {
  start: DateTime
  end: DateTime
}

enum CardStatus {
  ALL
  ASSIGNED_TO_ME
  DUE_SOON
  OVERDUE
  COMPLETED
}

enum CardSort {
  DUE_DATE_ASC
  DUE_DATE_DESC
  CREATED_ASC
  CREATED_DESC
  UPDATED_DESC
  TITLE_ASC
}

Mutations

type Mutation {
  # Board mutations
  createBoard(input: CreateBoardInput!): Board!
  updateBoard(id: ID!, input: UpdateBoardInput!): Board!
  deleteBoard(id: ID!): Boolean!
  starBoard(id: ID!): Board!
  unstarBoard(id: ID!): Board!
  addBoardMember(boardId: ID!, userId: ID!, role: MemberRole!): BoardMember!
  removeBoardMember(boardId: ID!, userId: ID!): Boolean!
  updateBoardMemberRole(boardId: ID!, userId: ID!, role: MemberRole!): BoardMember!
  
  # List mutations
  createList(input: CreateListInput!): List!
  updateList(id: ID!, input: UpdateListInput!): List!
  deleteList(id: ID!): Boolean!
  moveList(id: ID!, position: Int!): List!
  
  # Card mutations
  createCard(input: CreateCardInput!): Card!
  updateCard(id: ID!, input: UpdateCardInput!): Card!
  deleteCard(id: ID!): Boolean!
  moveCard(id: ID!, listId: ID!, position: Int!): Card!
  archiveCard(id: ID!): Card!
  unarchiveCard(id: ID!): Card!
  
  # Card details
  addCardMember(cardId: ID!, userId: ID!): Card!
  removeCardMember(cardId: ID!, userId: ID!): Card!
  addCardLabel(cardId: ID!, labelId: ID!): Card!
  removeCardLabel(cardId: ID!, labelId: ID!): Card!
  setCardDueDate(cardId: ID!, dueDate: DateTime!): Card!
  removeCardDueDate(cardId: ID!): Card!
  toggleCardComplete(cardId: ID!): Card!
  
  # Label mutations
  createLabel(input: CreateLabelInput!): Label!
  updateLabel(id: ID!, input: UpdateLabelInput!): Label!
  deleteLabel(id: ID!): Boolean!
  
  # Attachment mutations
  addAttachment(cardId: ID!, file: Upload!): Attachment!
  deleteAttachment(id: ID!): Boolean!
  
  # Checklist mutations
  createChecklist(cardId: ID!, input: CreateChecklistInput!): Checklist!
  updateChecklist(id: ID!, input: UpdateChecklistInput!): Checklist!
  deleteChecklist(id: ID!): Boolean!
  addChecklistItem(checklistId: ID!, input: CreateChecklistItemInput!): ChecklistItem!
  updateChecklistItem(id: ID!, input: UpdateChecklistItemInput!): ChecklistItem!
  deleteChecklistItem(id: ID!): Boolean!
  toggleChecklistItem(id: ID!): ChecklistItem!
  
  # Comment mutations
  addComment(cardId: ID!, text: String!): Comment!
  updateComment(id: ID!, text: String!): Comment!
  deleteComment(id: ID!): Boolean!
  addReaction(commentId: ID!, emoji: String!): Comment!
  removeReaction(commentId: ID!, emoji: String!): Comment!
}

input CreateBoardInput {
  title: String!
  description: String
  background: String
  visibility: BoardVisibility
}

input UpdateBoardInput {
  title: String
  description: String
  background: String
  visibility: BoardVisibility
}

input CreateListInput {
  boardId: ID!
  title: String!
  position: Int
}

input UpdateListInput {
  title: String
}

input CreateCardInput {
  listId: ID!
  title: String!
  description: String
  position: Int
}

input UpdateCardInput {
  title: String
  description: String
  dueDate: DateTime
  completed: Boolean
}

input CreateLabelInput {
  boardId: ID!
  name: String!
  color: String!
}

input UpdateLabelInput {
  name: String
  color: String
}

input CreateChecklistInput {
  title: String!
  position: Int
}

input UpdateChecklistInput {
  title: String
  position: Int
}

input CreateChecklistItemInput {
  text: String!
  position: Int
}

input UpdateChecklistItemInput {
  text: String
  completed: Boolean
  position: Int
}

scalar Upload

⚡ Real-Time Subscriptions

GraphQL subscriptions enable real-time updates across all connected clients using WebSocket connections and Redis PubSub.

Subscription Types

type Subscription {
  # Board subscriptions
  boardUpdated(boardId: ID!): BoardUpdatePayload!
  boardDeleted(boardId: ID!): ID!
  
  # List subscriptions
  listAdded(boardId: ID!): List!
  listUpdated(boardId: ID!): List!
  listDeleted(boardId: ID!): ID!
  listMoved(boardId: ID!): ListMovedPayload!
  
  # Card subscriptions
  cardAdded(boardId: ID!): Card!
  cardUpdated(boardId: ID!): Card!
  cardDeleted(boardId: ID!): ID!
  cardMoved(boardId: ID!): CardMovedPayload!
  
  # Comment subscriptions
  commentAdded(cardId: ID!): Comment!
  commentUpdated(cardId: ID!): Comment!
  commentDeleted(cardId: ID!): ID!
  
  # Activity subscriptions
  activityAdded(boardId: ID!): Activity!
}

type BoardUpdatePayload {
  board: Board!
  updatedFields: [String!]!
}

type ListMovedPayload {
  listId: ID!
  oldPosition: Int!
  newPosition: Int!
}

type CardMovedPayload {
  cardId: ID!
  oldListId: ID!
  newListId: ID!
  oldPosition: Int!
  newPosition: Int!
}

Server Implementation

// server.js
const { ApolloServer } = require('apollo-server-express');
const { createServer } = require('http');
const { WebSocketServer } = require('ws');
const { useServer } = require('graphql-ws/lib/use/ws');
const { makeExecutableSchema } = require('@graphql-tools/schema');
const { RedisPubSub } = require('graphql-redis-subscriptions');
const Redis = require('ioredis');
const express = require('express');

// Redis PubSub configuration
const redisOptions = {
  host: process.env.REDIS_HOST || 'localhost',
  port: process.env.REDIS_PORT || 6379,
  retryStrategy: (times) => Math.min(times * 50, 2000)
};

const pubsub = new RedisPubSub({
  publisher: new Redis(redisOptions),
  subscriber: new Redis(redisOptions)
});

// PubSub topics
const TOPICS = {
  BOARD_UPDATED: 'BOARD_UPDATED',
  BOARD_DELETED: 'BOARD_DELETED',
  LIST_ADDED: 'LIST_ADDED',
  LIST_UPDATED: 'LIST_UPDATED',
  LIST_DELETED: 'LIST_DELETED',
  LIST_MOVED: 'LIST_MOVED',
  CARD_ADDED: 'CARD_ADDED',
  CARD_UPDATED: 'CARD_UPDATED',
  CARD_DELETED: 'CARD_DELETED',
  CARD_MOVED: 'CARD_MOVED',
  COMMENT_ADDED: 'COMMENT_ADDED',
  ACTIVITY_ADDED: 'ACTIVITY_ADDED'
};

// Create executable schema
const schema = makeExecutableSchema({
  typeDefs,
  resolvers: {
    Query: { /* query resolvers */ },
    Mutation: { /* mutation resolvers */ },
    Subscription: {
      boardUpdated: {
        subscribe: withFilter(
          () => pubsub.asyncIterator(TOPICS.BOARD_UPDATED),
          (payload, variables) => {
            return payload.boardId === variables.boardId;
          }
        )
      },
      
      cardAdded: {
        subscribe: withFilter(
          () => pubsub.asyncIterator(TOPICS.CARD_ADDED),
          (payload, variables) => {
            return payload.card.board.id === variables.boardId;
          }
        )
      },
      
      cardMoved: {
        subscribe: withFilter(
          () => pubsub.asyncIterator(TOPICS.CARD_MOVED),
          (payload, variables) => {
            return payload.boardId === variables.boardId;
          }
        )
      },
      
      commentAdded: {
        subscribe: withFilter(
          () => pubsub.asyncIterator(TOPICS.COMMENT_ADDED),
          (payload, variables) => {
            return payload.comment.card.id === variables.cardId;
          }
        )
      }
    }
  }
});

// Express setup
const app = express();
const httpServer = createServer(app);

// WebSocket server for subscriptions
const wsServer = new WebSocketServer({
  server: httpServer,
  path: '/graphql'
});

// GraphQL WS server
const serverCleanup = useServer({
  schema,
  context: async (ctx) => {
    // Get token from connection params
    const token = ctx.connectionParams?.authorization;
    const user = await verifyToken(token);
    
    return {
      user,
      pubsub
    };
  },
  onConnect: async (ctx) => {
    console.log('Client connected:', ctx.connectionParams);
  },
  onDisconnect: () => {
    console.log('Client disconnected');
  }
}, wsServer);

// Apollo Server
const server = new ApolloServer({
  schema,
  context: ({ req }) => ({
    user: req.user,
    pubsub
  }),
  plugins: [
    {
      async serverWillStart() {
        return {
          async drainServer() {
            await serverCleanup.dispose();
          }
        };
      }
    }
  ]
});

// Start server
async function startServer() {
  await server.start();
  server.applyMiddleware({ app });
  
  const PORT = process.env.PORT || 4000;
  httpServer.listen(PORT, () => {
    console.log(`🚀 Server ready at http://localhost:${PORT}${server.graphqlPath}`);
    console.log(`🔌 Subscriptions ready at ws://localhost:${PORT}/graphql`);
  });
}

startServer();

Client Implementation (React + Apollo)

// apollo-client.js
import { ApolloClient, InMemoryCache, split, HttpLink } from '@apollo/client';
import { GraphQLWsLink } from '@apollo/client/link/subscriptions';
import { getMainDefinition } from '@apollo/client/utilities';
import { createClient } from 'graphql-ws';

const httpLink = new HttpLink({
  uri: 'http://localhost:4000/graphql',
  headers: {
    authorization: `Bearer ${localStorage.getItem('token')}`
  }
});

const wsLink = new GraphQLWsLink(
  createClient({
    url: 'ws://localhost:4000/graphql',
    connectionParams: {
      authorization: `Bearer ${localStorage.getItem('token')}`
    },
    retryAttempts: 5,
    shouldRetry: () => true
  })
);

// Split based on operation type
const splitLink = split(
  ({ query }) => {
    const definition = getMainDefinition(query);
    return (
      definition.kind === 'OperationDefinition' &&
      definition.operation === 'subscription'
    );
  },
  wsLink,
  httpLink
);

const client = new ApolloClient({
  link: splitLink,
  cache: new InMemoryCache()
});

export default client;

// Board component with subscriptions
import { useQuery, useSubscription, gql } from '@apollo/client';

const GET_BOARD = gql`
  query GetBoard($id: ID!) {
    board(id: $id) {
      id
      title
      lists {
        id
        title
        position
        cards {
          id
          title
          position
        }
      }
    }
  }
`;

const CARD_ADDED_SUBSCRIPTION = gql`
  subscription OnCardAdded($boardId: ID!) {
    cardAdded(boardId: $boardId) {
      id
      title
      position
      list {
        id
      }
    }
  }
`;

const CARD_MOVED_SUBSCRIPTION = gql`
  subscription OnCardMoved($boardId: ID!) {
    cardMoved(boardId: $boardId) {
      cardId
      oldListId
      newListId
      oldPosition
      newPosition
    }
  }
`;

function Board({ boardId }) {
  const { data, loading } = useQuery(GET_BOARD, {
    variables: { id: boardId }
  });

  // Subscribe to card additions
  useSubscription(CARD_ADDED_SUBSCRIPTION, {
    variables: { boardId },
    onData: ({ data: { data } }) => {
      console.log('Card added:', data.cardAdded);
      // Cache automatically updates!
    }
  });

  // Subscribe to card movements
  useSubscription(CARD_MOVED_SUBSCRIPTION, {
    variables: { boardId },
    onData: ({ data: { data } }) => {
      console.log('Card moved:', data.cardMoved);
      // Update UI in real-time
    }
  });

  if (loading) return 
Loading...
; return (

{data.board.title}

{data.board.lists.map(list => ( ))}
); }

💻 Implementation Details

Key resolver implementations for mutation operations with real-time broadcasting.

Move Card Mutation

// resolvers/card.js
const { UserInputError } = require('apollo-server');

const cardResolvers = {
  Mutation: {
    moveCard: async (_, { id, listId, position }, { user, pubsub, db }) => {
      // Verify user has permission
      const card = await db.card.findUnique({
        where: { id },
        include: { list: { include: { board: true } } }
      });

      if (!card) {
        throw new UserInputError('Card not found');
      }

      const hasPermission = await checkBoardPermission(
        user.id,
        card.list.board.id,
        'MEMBER'
      );

      if (!hasPermission) {
        throw new ForbiddenError('No permission to move card');
      }

      const oldListId = card.listId;
      const oldPosition = card.position;

      // Transaction to update positions atomically
      const updatedCard = await db.$transaction(async (tx) => {
        // If moving to different list
        if (listId !== oldListId) {
          // Decrement positions in old list
          await tx.card.updateMany({
            where: {
              listId: oldListId,
              position: { gt: oldPosition }
            },
            data: {
              position: { decrement: 1 }
            }
          });

          // Increment positions in new list
          await tx.card.updateMany({
            where: {
              listId,
              position: { gte: position }
            },
            data: {
              position: { increment: 1 }
            }
          });
        } else {
          // Moving within same list
          if (position > oldPosition) {
            // Moving down
            await tx.card.updateMany({
              where: {
                listId,
                position: {
                  gt: oldPosition,
                  lte: position
                }
              },
              data: {
                position: { decrement: 1 }
              }
            });
          } else if (position < oldPosition) {
            // Moving up
            await tx.card.updateMany({
              where: {
                listId,
                position: {
                  gte: position,
                  lt: oldPosition
                }
              },
              data: {
                position: { increment: 1 }
              }
            });
          }
        }

        // Update the card itself
        return await tx.card.update({
          where: { id },
          data: {
            listId,
            position
          },
          include: {
            list: true,
            board: true,
            labels: true,
            members: true
          }
        });
      });

      // Create activity log
      await db.activity.create({
        data: {
          type: 'CARD_MOVED',
          userId: user.id,
          cardId: id,
          description: `moved card from ${card.list.title} to ${updatedCard.list.title}`,
          metadata: { oldListId, newListId: listId, oldPosition, newPosition: position }
        }
      });

      // Publish subscription event
      await pubsub.publish('CARD_MOVED', {
        cardMoved: {
          cardId: id,
          boardId: updatedCard.board.id,
          oldListId,
          newListId: listId,
          oldPosition,
          newPosition: position
        }
      });

      return updatedCard;
    },

    createCard: async (_, { input }, { user, pubsub, db }) => {
      const { listId, title, description, position } = input;

      // Get list and verify permissions
      const list = await db.list.findUnique({
        where: { id: listId },
        include: { board: true }
      });

      if (!list) {
        throw new UserInputError('List not found');
      }

      const hasPermission = await checkBoardPermission(
        user.id,
        list.board.id,
        'MEMBER'
      );

      if (!hasPermission) {
        throw new ForbiddenError('No permission to create card');
      }

      // Calculate position
      const cardPosition = position !== undefined
        ? position
        : await db.card.count({ where: { listId } });

      // Shift existing cards if needed
      if (position !== undefined) {
        await db.card.updateMany({
          where: {
            listId,
            position: { gte: position }
          },
          data: {
            position: { increment: 1 }
          }
        });
      }

      // Create card
      const card = await db.card.create({
        data: {
          title,
          description,
          position: cardPosition,
          listId,
          createdById: user.id
        },
        include: {
          list: true,
          board: true,
          createdBy: true
        }
      });

      // Create activity
      await db.activity.create({
        data: {
          type: 'CARD_CREATED',
          userId: user.id,
          cardId: card.id,
          description: `created card "${title}"`
        }
      });

      // Publish subscription
      await pubsub.publish('CARD_ADDED', {
        cardAdded: card
      });

      return card;
    }
  }
};

// Helper function
async function checkBoardPermission(userId, boardId, minimumRole) {
  const member = await db.boardMember.findFirst({
    where: { userId, boardId }
  });

  if (!member) return false;

  const roles = ['VIEWER', 'MEMBER', 'ADMIN'];
  const userRoleIndex = roles.indexOf(member.role);
  const requiredRoleIndex = roles.indexOf(minimumRole);

  return userRoleIndex >= requiredRoleIndex;
}

module.exports = cardResolvers;

Optimistic UI Updates

// components/Card.jsx
import { useMutation, gql } from '@apollo/client';
import { useDrag, useDrop } from 'react-dnd';

const MOVE_CARD = gql`
  mutation MoveCard($id: ID!, $listId: ID!, $position: Int!) {
    moveCard(id: $id, listId: $listId, position: $position) {
      id
      position
      list {
        id
      }
    }
  }
`;

function Card({ card, listId, index }) {
  const [moveCard] = useMutation(MOVE_CARD, {
    // Optimistic response for instant UI update
    optimisticResponse: {
      moveCard: {
        __typename: 'Card',
        id: card.id,
        position: index,
        list: {
          __typename: 'List',
          id: listId
        }
      }
    },
    // Update cache after successful mutation
    update: (cache, { data: { moveCard } }) => {
      // Cache automatically updated by subscription
    },
    // Handle errors
    onError: (error) => {
      console.error('Failed to move card:', error);
      // Revert optimistic update
    }
  });

  const [{ isDragging }, drag] = useDrag({
    type: 'CARD',
    item: { id: card.id, listId, index },
    collect: (monitor) => ({
      isDragging: monitor.isDragging()
    })
  });

  const [, drop] = useDrop({
    accept: 'CARD',
    hover: (draggedItem, monitor) => {
      if (draggedItem.id === card.id) return;
      
      const dragIndex = draggedItem.index;
      const hoverIndex = index;

      // Only move when hovering over a different card
      if (dragIndex === hoverIndex) return;

      // Execute mutation with optimistic UI
      moveCard({
        variables: {
          id: draggedItem.id,
          listId,
          position: hoverIndex
        }
      });

      // Update dragged item's index for proper hover behavior
      draggedItem.index = hoverIndex;
      draggedItem.listId = listId;
    }
  });

  return (
    <div
      ref={(node) => drag(drop(node))}
      className={`card ${isDragging ? 'dragging' : ''}`}
    >
      <h3>{card.title}</h3>
      {card.description && <p>{card.description}</p>}
      {card.labels.length > 0 && (
        <div className="labels">
          {card.labels.map(label => (
            <span
              key={label.id}
              className="label"
              style={{{ backgroundColor: label.color }}}
            >
              {label.name}
            </span>
          ))}
        </div>
      )}
    </div>
  );
}

🎮 Interactive Demo

Try out the task management system with this interactive demo board.

📋 Project Tasks

JD
JS
YO

📝 To Do

3
Setup GraphQL Server
Backend High Priority
Design Database Schema
Database
Create React Components
Frontend

🔄 In Progress

2
Implement Subscriptions
Backend Real-Time
Add Drag & Drop
Frontend

✅ Done

4
Setup Project Structure
Setup
Configure Apollo Server
Backend
User Authentication
Security
Database Setup
Database

💡 Try it: Drag cards between lists to see how the real-time updates would work in the actual application!

⚡ Performance Optimization

Techniques for optimizing GraphQL queries, reducing latency, and handling high concurrent connections.

🔄

DataLoader Batching

Batch database queries to prevent N+1 problems when loading nested relationships like board members and card labels.

💾

Redis Caching

Cache frequently accessed data like board configurations and user profiles with smart invalidation strategies.

📊

Query Complexity

Limit query depth and complexity to prevent expensive operations that could impact server performance.

DataLoader Implementation

// dataloaders/index.js
const DataLoader = require('dataloader');

function createLoaders(db) {
  return {
    // User loader
    userLoader: new DataLoader(async (userIds) => {
      const users = await db.user.findMany({
        where: { id: { in: userIds } }
      });
      
      return userIds.map(id => 
        users.find(user => user.id === id)
      );
    }),

    // Board loader
    boardLoader: new DataLoader(async (boardIds) => {
      const boards = await db.board.findMany({
        where: { id: { in: boardIds } },
        include: {
          owner: true,
          members: { include: { user: true } }
        }
      });
      
      return boardIds.map(id =>
        boards.find(board => board.id === id)
      );
    }),

    // Card members loader (by card ID)
    cardMembersLoader: new DataLoader(async (cardIds) => {
      const cardMembers = await db.cardMember.findMany({
        where: { cardId: { in: cardIds } },
        include: { user: true }
      });
      
      const membersByCard = {};
      cardMembers.forEach(cm => {
        if (!membersByCard[cm.cardId]) {
          membersByCard[cm.cardId] = [];
        }
        membersByCard[cm.cardId].push(cm.user);
      });
      
      return cardIds.map(id => membersByCard[id] || []);
    }),

    // Card labels loader
    cardLabelsLoader: new DataLoader(async (cardIds) => {
      const cardLabels = await db.cardLabel.findMany({
        where: { cardId: { in: cardIds } },
        include: { label: true }
      });
      
      const labelsByCard = {};
      cardLabels.forEach(cl => {
        if (!labelsByCard[cl.cardId]) {
          labelsByCard[cl.cardId] = [];
        }
        labelsByCard[cl.cardId].push(cl.label);
      });
      
      return cardIds.map(id => labelsByCard[id] || []);
    }),

    // Comments count loader
    commentsCountLoader: new DataLoader(async (cardIds) => {
      const counts = await db.comment.groupBy({
        by: ['cardId'],
        where: { cardId: { in: cardIds } },
        _count: { id: true }
      });
      
      const countsByCard = {};
      counts.forEach(c => {
        countsByCard[c.cardId] = c._count.id;
      });
      
      return cardIds.map(id => countsByCard[id] || 0);
    })
  };
}

// Usage in resolvers
const resolvers = {
  Card: {
    members: (card, _, { loaders }) => {
      return loaders.cardMembersLoader.load(card.id);
    },
    labels: (card, _, { loaders }) => {
      return loaders.cardLabelsLoader.load(card.id);
    },
    commentCount: (card, _, { loaders }) => {
      return loaders.commentsCountLoader.load(card.id);
    }
  }
};

module.exports = createLoaders;

Connection Pooling & Scaling

// config/database.js
const { PrismaClient } = require('@prisma/client');

// Configure connection pooling
const prisma = new PrismaClient({
  datasources: {
    db: {
      url: process.env.DATABASE_URL
    }
  },
  log: ['query', 'error', 'warn'],
  // Connection pool settings
  pool: {
    min: 2,
    max: 10,
    acquireTimeoutMillis: 30000,
    idleTimeoutMillis: 30000
  }
});

// Graceful shutdown
process.on('beforeExit', async () => {
  await prisma.$disconnect();
});

// Horizontal scaling with Redis PubSub
// Multiple server instances can share subscriptions
const { RedisPubSub } = require('graphql-redis-subscriptions');
const Redis = require('ioredis');
const cluster = require('cluster');
const numCPUs = require('os').cpus().length;

if (cluster.isMaster) {
  console.log(`Master ${process.pid} is running`);
  
  // Fork workers
  for (let i = 0; i < numCPUs; i++) {
    cluster.fork();
  }
  
  cluster.on('exit', (worker, code, signal) => {
    console.log(`Worker ${worker.process.pid} died`);
    cluster.fork(); // Restart worker
  });
} else {
  // Workers share Redis PubSub
  const pubsub = new RedisPubSub({
    publisher: new Redis(process.env.REDIS_URL),
    subscriber: new Redis(process.env.REDIS_URL)
  });
  
  startServer(pubsub);
  console.log(`Worker ${process.pid} started`);
}

module.exports = { prisma };

🚀 Deployment

Production deployment configuration with Docker, monitoring, and CI/CD pipeline.

Docker Configuration

# docker-compose.yml
version: '3.8'

services:
  # GraphQL Server
  api:
    build:
      context: .
      dockerfile: Dockerfile
    ports:
      - "4000:4000"
    environment:
      - NODE_ENV=production
      - DATABASE_URL=postgresql://postgres:password@postgres:5432/taskmanager
      - REDIS_URL=redis://redis:6379
      - JWT_SECRET=${JWT_SECRET}
      - AWS_S3_BUCKET=${AWS_S3_BUCKET}
    depends_on:
      - postgres
      - redis
    restart: unless-stopped
    healthcheck:
      test: ["CMD", "wget", "--quiet", "--tries=1", "--spider", "http://localhost:4000/.well-known/apollo/server-health"]
      interval: 30s
      timeout: 10s
      retries: 3

  # PostgreSQL Database
  postgres:
    image: postgres:15-alpine
    environment:
      - POSTGRES_USER=postgres
      - POSTGRES_PASSWORD=password
      - POSTGRES_DB=taskmanager
    volumes:
      - postgres_data:/var/lib/postgresql/data
    ports:
      - "5432:5432"
    restart: unless-stopped

  # Redis for caching and PubSub
  redis:
    image: redis:7-alpine
    command: redis-server --appendonly yes
    volumes:
      - redis_data:/data
    ports:
      - "6379:6379"
    restart: unless-stopped

  # React Frontend
  web:
    build:
      context: ./client
      dockerfile: Dockerfile
    ports:
      - "3000:80"
    environment:
      - REACT_APP_API_URL=http://localhost:4000/graphql
      - REACT_APP_WS_URL=ws://localhost:4000/graphql
    depends_on:
      - api
    restart: unless-stopped

volumes:
  postgres_data:
  redis_data:

Environment Variables

# .env.production
NODE_ENV=production
PORT=4000

# Database
DATABASE_URL=postgresql://user:password@host:5432/taskmanager

# Redis
REDIS_URL=redis://host:6379
REDIS_HOST=host
REDIS_PORT=6379

# Authentication
JWT_SECRET=your-secret-key-change-in-production
JWT_EXPIRES_IN=7d

# AWS S3 (for file uploads)
AWS_ACCESS_KEY_ID=your-access-key
AWS_SECRET_ACCESS_KEY=your-secret-key
AWS_S3_BUCKET=taskmanager-uploads
AWS_REGION=us-east-1

# Email (for notifications)
SMTP_HOST=smtp.gmail.com
SMTP_PORT=587
SMTP_USER=your-email@gmail.com
SMTP_PASS=your-app-password

# Monitoring
SENTRY_DSN=your-sentry-dsn
LOG_LEVEL=info

# CORS
CORS_ORIGIN=https://yourdomain.com

# Rate Limiting
RATE_LIMIT_MAX=100
RATE_LIMIT_WINDOW_MS=900000

Key Learnings

🔌 WebSocket Management

Proper connection handling, heartbeats, and reconnection logic are crucial for reliable real-time features in production.

⚡ Optimistic Updates

Optimistic UI makes the app feel instant, but proper error handling and rollback strategies are essential for good UX.

🔄 Event Ordering

In real-time systems, event ordering matters. Use timestamps and sequence numbers to handle out-of-order updates.

💾 State Management

Apollo Cache works great with subscriptions, but complex drag-drop states sometimes need local state management.

📊 Performance

DataLoader is mandatory for nested queries. N+1 problems become severe with real-time updates hitting multiple clients.

🔒 Authorization

Verify permissions on every subscription event. Don't rely solely on initial connection auth - validate each message.

🎓 Conclusion

This Task Management System demonstrates a production-ready Trello clone with real-time collaboration using GraphQL Subscriptions. The architecture supports multiple concurrent users, instant updates, and rich task management features.

Key achievements include WebSocket-based subscriptions, optimistic UI updates, drag-and-drop functionality, and scalable Redis PubSub for horizontal scaling across multiple server instances.

GraphQL Apollo Server WebSocket Redis PubSub React Apollo Client PostgreSQL Prisma Docker DataLoader