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