Bubbly – Full stack chat application

This is a Full Stack chat application created w/ Next.js, Socket.IO, Express, React and TypeScript.
Live demo deployed on Heroku is here.

Bubbly Logo


left_speech_bubble Creating chat rooms for real-time chat
white_check_mark Joining conversations by using themed rooms
id JWT based anonymous authentication
sparkles Users can set their username and conversation bubble color
framed_picture Sending images/gifs
sunglasses Emoji picker
keyboard "User is typing" notifications
black_circle Dark theme
envelope Automatically linkifying urls, emails etc
link Invite/share buttons
sound Sound notification when the window is not focused
arrow_double_down "Back to bottom" button to scroll down automatically



To run it in development mode:

Step 1: npm install

Step 2: npm run dev:custom


To create a production build ready to deploy, you need to set a JWT_SECRET_KEY in .env file first.

After that, run:

Step 1: npm run build

Step 2: npm start

Sample Code


import 'dotenv/config';
import express, { Request, Response } from 'express';
import next from 'next';
import http from 'http';
import { CustomSocketIoServer } from './socket/CustomSocketIoServer';
import { convertMBToByte } from './shared/shared.utils';
import { SocketUser } from './users/SocketUser';
import socketHandler from './socket/socket.handler';
import usersHandler from './users/users.handler';
import roomsHandler from './rooms/rooms.handler';
import chatHandler from './chat/chat.handler';
import { registerHelmet } from './security/security.utils';

const dev = process.env.NODE_ENV !== 'production';
const nextApp = next({ dev });
const nextHandler = nextApp.getRequestHandler();

nextApp.prepare().then(() => {
  const expressApp = express();


  const maxMessageSizeInMB = 2;
  const httpServer = http.createServer(expressApp);
  const io = new CustomSocketIoServer(httpServer, {
    // Max size for a message
    // TODO: We need a way to handle exceptions
    // those caused by this option on the client side.
    maxHttpBufferSize: convertMBToByte(maxMessageSizeInMB),

  io.use((socket, nextFn) => {
    const user = SocketUser.findOrCreateSocketUser(io, socket);
    // eslint-disable-next-line no-param-reassign
    socket.user = user;

  io.on('connection', (socket) => {
    socketHandler(io, socket);
    usersHandler(io, socket);
    roomsHandler(io, socket);
    chatHandler(io, socket);

  expressApp.all('*', (req: Request, res: Response) => nextHandler(req, res));

  const port = process.env.PORT ?? 3000;

  httpServer.listen(port, () => {
    // eslint-disable-next-line no-console
    console.log(<code>> Ready on http://localhost:${port}</code>);


import { nanoid } from 'nanoid';
import { ID, Maybe } from '@shared/SharedTypes';
import socketIo from 'socket.io';
import { SocketUser } from '../users/SocketUser';
import { isImageFile } from './chat.utils';
import { trimSpaces } from '../shared/shared.utils';

export interface ChatMessageInput {
  body: Maybe<string>;
  file: Maybe<Buffer | string>;

export class ChatMessage {
  id: ID;
  author: SocketUser;
  body: Maybe<string>;
  timestamp: number;
  file: Maybe<Buffer>;

  // We made this "private" to prevent this to be called from outside.
  // We shouldn't use this constructor directly.
  // We should use "createChatMessage" static method to create new messages.
  private constructor(args: {
    socket: socketIo.Socket;
    body: Maybe<string>;
    file: Maybe<Buffer>;
  }) {
    const { body, file, socket } = args;
    this.id = nanoid();
    this.author = socket.user;
    this.body = body;
    this.timestamp = Date.now();
    this.file = file;

  // Because that we can't make the constructor async,
  // we create ChatMessage instances with this static method.
  static createChatMessage = async (
    args: ChatMessageInput & { socket: socketIo.Socket },
  ): Promise<ChatMessage> => {
    const { socket, body, file } = args;
    const trimmedBody = trimSpaces(body ?? '');
    if (!trimmedBody && !file) {
      throw new Error('At least a message body or a file is required.');

    // "file" can be a Buffer or base64 string.
    // base64 string is used for react-native app here.
    let inputFile = null;
    if (file instanceof Buffer) {
      inputFile = file;
    } else if (typeof file === 'string') {
      inputFile = Buffer.from(file, 'base64');

    if (inputFile) {
      const isImage = await isImageFile(inputFile);
      if (!isImage) {
        throw new Error('File is not an image.');

    const message = new ChatMessage({
      body: trimmedBody,
      file: inputFile,
    return message;


View live demo here.
Download code here.