DEV Community

Cover image for Building a Scalable Support Ticket System with Node.js, Express & MongoDB
AbhiJeet Sachan
AbhiJeet Sachan

Posted on

Building a Scalable Support Ticket System with Node.js, Express & MongoDB

**

Part 1: Backend Foundation creating a Ticket raising platform

**

A practical guide to creating real-world backend systems using Mongoose, Express middleware, and secure authentication.

Introduction

Customer support systems are an essential part of any tech-driven business, and building one from scratch is a great way to learn full-stack development with real-world requirements.

In this series, I'm documenting the development of HelpMe, an AI-powered support ticket system built using the MERN stack (MongoDB, Express, React, Node.js). The goal is to simulate a production-grade ticketing platform with:

  • Role-based access (user, agent, admin)
  • JWT-based authentication
  • Ticket lifecycle management
  • Modular, scalable backend structure
  • AI features (coming soon)

This post will cover the backend foundation — ideal for anyone looking to build a professional-grade project using Express and MongoDB.

Github repo

Project Structure

Organizing files in a clean, modular way is key to scaling your backend efficiently. Here's how the backend is structured:

Project structure

Environment Setup

  1. Initialize the project
npm init -y
npm install express mongoose dotenv cookie-parser bcryptjs jsonwebtoken

Enter fullscreen mode Exit fullscreen mode
  1. Create the entry file

// server.js

import express from 'express';
import mongoose from 'mongoose';
import dotenv from 'dotenv';
import cookieParser from 'cookie-parser';

dotenv.config();
const app = express();

app.use(express.json());
app.use(cookieParser());

// Routes (example)
import authRoutes from './routes/auth.js';
app.use('/api/auth', authRoutes);

// Connect to MongoDB
mongoose.connect(process.env.MONGO_URI)
  .then(() => app.listen(5000, () => console.log('Server running')))
  .catch(err => console.error('MongoDB connection failed:', err));

Enter fullscreen mode Exit fullscreen mode

Defining the Schemas

  • User Schema // models/User.js
import mongoose from 'mongoose';

const userSchema = new mongoose.Schema({
  username: { type: String, required: true, trim: true, unique: true },
  email:    { type: String, required: true, trim: true, lowercase: true, unique: true },
  password: { type: String, required: true, minlength: 6 },
  role:     { type: String, enum: ['user', 'admin', 'agent'], default: 'user' }
});

export default mongoose.model('User', userSchema);

Enter fullscreen mode Exit fullscreen mode
  • Ticket Schema // models/Ticket.js
import mongoose from 'mongoose';

const ticketSchema = new mongoose.Schema({
  title:       { type: String, required: true },
  description: { type: String },
  priority:    { type: String, enum: ['low', 'medium', 'high'], default: 'low' },
  status:      { type: String, enum: ['open', 'in-progress', 'closed'], default: 'open' },
  createdBy:   { type: mongoose.Schema.Types.ObjectId, ref: 'User', required: true },
  assignedTo:  { type: mongoose.Schema.Types.ObjectId, ref: 'User', default: null }
});

export default mongoose.model('Ticket', ticketSchema);

Enter fullscreen mode Exit fullscreen mode

Why Schema Design Matters

Relationships like createdBy and assignedTo are handled using MongoDB references (ObjectId), enabling efficient population and querying.

Enumerations ensure valid values for priority, status, and role, enforcing business rules at the DB level.

Authentication System

Register, Login & Logout

  • Auth routes include: POST /register – Create a user account

POST /login – Authenticate and store JWT in an HTTP-only cookie

POST /logout – Clear the authentication cookie

// routes/auth.js
import express from 'express';
import bcrypt from 'bcryptjs';
import jwt from 'jsonwebtoken';
import User from '../models/User.js';

const router = express.Router();

router.post('/register', async (req, res) => {
  const { username, email, password } = req.body;
  const hashed = await bcrypt.hash(password, 10);
  const user = new User({ username, email, password: hashed });
  await user.save();
  res.status(201).json({ message: 'Registration successful' });
});

router.post('/login', async (req, res) => {
  const { email, password } = req.body;
  const user = await User.findOne({ email });
  if (!user || !(await bcrypt.compare(password, user.password))) {
    return res.status(401).json({ message: 'Invalid credentials' });
  }

  const token = jwt.sign({ id: user._id, role: user.role }, process.env.JWT_SECRET);
  res.cookie('token', token, { httpOnly: true });
  res.status(200).json({ message: 'Login successful' });
});

router.post('/logout', (req, res) => {
  res.clearCookie('token');
  res.status(200).json({ message: 'Logged out' });
});

export default router;

Enter fullscreen mode Exit fullscreen mode

Middleware: Secure Access

  • To protect routes and limit access based on roles, we add two middlewares:

Verify JWT

// middleware/verifyToken.js
import jwt from 'jsonwebtoken';

export const verifyToken = (req, res, next) => {
  const token = req.cookies.token;
  if (!token) return res.status(401).json({ message: 'Unauthorized' });

  jwt.verify(token, process.env.JWT_SECRET, (err, decoded) => {
    if (err) return res.status(403).json({ message: 'Invalid token' });
    req.user = decoded;
    next();
  });
};

Enter fullscreen mode Exit fullscreen mode

Role-Based Access

// middleware/roleMiddleware.js
export const roleMiddleware = (roles) => (req, res, next) => {
  if (!roles.includes(req.user.role)) {
    return res.status(403).json({ message: 'Access denied' });
  }
  next();
};

Enter fullscreen mode Exit fullscreen mode

This system allows you to protect endpoints like:

app.get('/tickets/all', verifyToken, roleMiddleware(['admin']), controller);

Enter fullscreen mode Exit fullscreen mode

What’s Covered So Far

Project structure & setup ✅ Done
User & Ticket schemas ✅ Done
Auth routes (/register, /login, /logout) ✅ Done
JWT & role-based middleware ✅ Done

Coming in Part 2

Next, I’ll cover:

  1. Ticket creation and viewing routes
  2. Admin: Assign ticket to agent
  3. Agent dashboard: View assigned tickets
  4. Threaded replies using a conversation schema
  5. Integrating AI models for tagging & sentiment

Final Notes

If you're looking to build a full-featured backend using modern Node.js practices — especially for multi-role apps — this architecture is production-ready and extendable.

Feel free to explore, clone, or contribute to the project below.

🔗 GitHub Repo: http://github.com/Abhijeet002/HelpMe-AI-Powered-Support-Ticket-System

Follow me on Dev.to or LinkedIn for the next parts of this series.
Got questions or feedback? Let me know in the comments — happy to help!

Top comments (0)