Trello-Style Board with GraphQL Subscriptions & Real-Time Updates
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.
Apollo Client + Subscriptions
Apollo Server + WebSocket
Resolvers & Services
Redis PubSub
Main Database
Cache & PubSub
File Storage
Comprehensive task management with real-time collaboration and rich functionality.
Organize work with unlimited boards and lists. Each board can have custom backgrounds, visibility settings, and member permissions.
Rich task cards with descriptions, labels, due dates, attachments, checklists, and activity history. Full markdown support for descriptions.
Smooth drag-and-drop interface for moving cards between lists, reordering items, and organizing boards with optimistic UI updates.
GraphQL subscriptions provide instant synchronization across all connected clients. See changes as they happen without refreshing.
Threaded comments on cards with @mentions, emoji reactions, and complete activity log showing all changes with timestamps.
Invite team members, assign cards, set permissions (admin/member/viewer), and track who's working on what in real-time.
Color-coded labels for categorization, advanced filtering by assignee, due date, labels, and full-text search across cards.
Upload images, documents, and files to cards. Preview images inline, support for various file types with S3 storage integration.
Break down tasks with nested checklists. Track progress with completion percentages and convert checklist items to cards.
Complete type definitions for the task management system with boards, lists, cards, and user management.
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
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
}
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
GraphQL subscriptions enable real-time updates across all connected clients using WebSocket connections and Redis PubSub.
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.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();
// 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 => (
))}
);
}
Key resolver implementations for mutation operations with real-time broadcasting.
// 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;
// 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>
);
}
Try out the task management system with this interactive demo board.
💡 Try it: Drag cards between lists to see how the real-time updates would work in the actual application!
Techniques for optimizing GraphQL queries, reducing latency, and handling high concurrent connections.
Batch database queries to prevent N+1 problems when loading nested relationships like board members and card labels.
Cache frequently accessed data like board configurations and user profiles with smart invalidation strategies.
Limit query depth and complexity to prevent expensive operations that could impact server performance.
// 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;
// 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 };
Production deployment configuration with Docker, monitoring, and CI/CD pipeline.
# 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:
# .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
Proper connection handling, heartbeats, and reconnection logic are crucial for reliable real-time features in production.
Optimistic UI makes the app feel instant, but proper error handling and rollback strategies are essential for good UX.
In real-time systems, event ordering matters. Use timestamps and sequence numbers to handle out-of-order updates.
Apollo Cache works great with subscriptions, but complex drag-drop states sometimes need local state management.
DataLoader is mandatory for nested queries. N+1 problems become severe with real-time updates hitting multiple clients.
Verify permissions on every subscription event. Don't rely solely on initial connection auth - validate each message.
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.