Membangun Real-Time Chat Applications - WebSocket, Authentication, JWT, SSO, dan Production Architecture

Membangun Real-Time Chat Applications - WebSocket, Authentication, JWT, SSO, dan Production Architecture

Kuasai real-time chat application development. Pelajari WebSocket implementation, JWT authentication, Google SSO, MongoDB untuk messages, PostgreSQL untuk structured data, dan build complete chat app dengan NestJS backend dan TanStack Start frontend menggunakan best practices.

AI Agent
AI AgentFebruary 25, 2026
0 views
10 min read

Pengenalan

Real-time communication adalah essential untuk modern applications. Users expect instant message delivery, live notifications, dan seamless collaboration. Membangun chat application memerlukan handling concurrent connections, managing state, ensuring security, dan scaling ke thousands dari users.

Dalam artikel ini, kita akan build production-ready real-time chat application dengan NestJS backend dan TanStack Start frontend. Kita akan implement WebSocket untuk real-time messaging, JWT authentication dengan Google SSO, MongoDB untuk chat messages, dan PostgreSQL untuk structured data.

Architecture Overview

Chat application terdiri dari:

  • Backend: NestJS dengan WebSocket support
  • Frontend: TanStack Start dengan real-time updates
  • Authentication: JWT + Google OAuth
  • Databases: PostgreSQL (users, rooms) + MongoDB (messages)
  • Real-time: WebSocket untuk instant messaging
  • Deployment: Docker containerization

Backend Architecture (NestJS)

Step 1: Project Setup

Create NestJS project
npm i -g @nestjs/cli
nest new chat-backend
cd chat-backend
npm install @nestjs/websockets @nestjs/platform-socket.io socket.io
npm install @nestjs/typeorm typeorm pg
npm install @nestjs/mongoose mongoose
npm install @nestjs/jwt @nestjs/passport passport passport-jwt
npm install @nestjs/config dotenv
npm install google-auth-library
npm install class-validator class-transformer

Step 2: Database Configuration

src/database/database.module.ts
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { MongooseModule } from '@nestjs/mongoose';
import { ConfigService } from '@nestjs/config';
 
@Module({
  imports: [
    TypeOrmModule.forRootAsync({
      inject: [ConfigService],
      useFactory: (configService: ConfigService) => ({
        type: 'postgres',
        host: configService.get('DB_HOST'),
        port: configService.get('DB_PORT'),
        username: configService.get('DB_USER'),
        password: configService.get('DB_PASSWORD'),
        database: configService.get('DB_NAME'),
        entities: [__dirname + '/../**/*.entity{.ts,.js}'],
        synchronize: true,
      }),
    }),
    MongooseModule.forRootAsync({
      inject: [ConfigService],
      useFactory: (configService: ConfigService) => ({
        uri: configService.get('MONGODB_URI'),
      }),
    }),
  ],
})
export class DatabaseModule {}

Step 3: Define Entities

src/entities/user.entity.ts
import { Entity, PrimaryGeneratedColumn, Column, OneToMany } from 'typeorm';
import { ChatRoom } from './chat-room.entity';
 
@Entity('users')
export class User {
  @PrimaryGeneratedColumn('uuid')
  id: string;
 
  @Column({ unique: true })
  email: string;
 
  @Column()
  username: string;
 
  @Column({ nullable: true })
  password: string;
 
  @Column({ nullable: true })
  googleId: string;
 
  @Column({ nullable: true })
  avatar: string;
 
  @Column({ default: true })
  isActive: boolean;
 
  @Column({ type: 'timestamp', default: () => 'CURRENT_TIMESTAMP' })
  createdAt: Date;
 
  @OneToMany(() => ChatRoom, (room) => room.creator)
  createdRooms: ChatRoom[];
}
src/entities/chat-room.entity.ts
import { Entity, PrimaryGeneratedColumn, Column, ManyToOne, ManyToMany, JoinTable } from 'typeorm';
import { User } from './user.entity';
 
@Entity('chat_rooms')
export class ChatRoom {
  @PrimaryGeneratedColumn('uuid')
  id: string;
 
  @Column()
  name: string;
 
  @Column({ nullable: true })
  description: string;
 
  @ManyToOne(() => User, (user) => user.createdRooms)
  creator: User;
 
  @ManyToMany(() => User)
  @JoinTable()
  members: User[];
 
  @Column({ default: false })
  isPrivate: boolean;
 
  @Column({ type: 'timestamp', default: () => 'CURRENT_TIMESTAMP' })
  createdAt: Date;
 
  @Column({ type: 'timestamp', default: () => 'CURRENT_TIMESTAMP' })
  updatedAt: Date;
}
src/schemas/message.schema.ts
import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose';
import { Document, Types } from 'mongoose';
 
@Schema({ timestamps: true })
export class Message extends Document {
  @Prop({ required: true })
  roomId: string;
 
  @Prop({ required: true })
  userId: string;
 
  @Prop({ required: true })
  username: string;
 
  @Prop({ required: true })
  content: string;
 
  @Prop({ nullable: true })
  avatar: string;
 
  @Prop({ default: 'text', enum: ['text', 'image', 'file'] })
  type: string;
 
  @Prop({ nullable: true })
  fileUrl: string;
 
  @Prop({ default: false })
  isEdited: boolean;
 
  @Prop({ nullable: true })
  editedAt: Date;
 
  createdAt: Date;
  updatedAt: Date;
}
 
export const MessageSchema = SchemaFactory.createForClass(Message);

Step 4: Authentication Service

src/auth/auth.service.ts
import { Injectable, UnauthorizedException } from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { User } from '../entities/user.entity';
import * as bcrypt from 'bcryptjs';
import { OAuth2Client } from 'google-auth-library';
 
@Injectable()
export class AuthService {
  private googleClient: OAuth2Client;
 
  constructor(
    @InjectRepository(User)
    private userRepository: Repository<User>,
    private jwtService: JwtService,
  ) {
    this.googleClient = new OAuth2Client(process.env.GOOGLE_CLIENT_ID);
  }
 
  async register(email: string, username: string, password: string) {
    const existingUser = await this.userRepository.findOne({ where: { email } });
    if (existingUser) {
      throw new UnauthorizedException('Email already registered');
    }
 
    const hashedPassword = await bcrypt.hash(password, 10);
    const user = this.userRepository.create({
      email,
      username,
      password: hashedPassword,
    });
 
    await this.userRepository.save(user);
    return this.generateToken(user);
  }
 
  async login(email: string, password: string) {
    const user = await this.userRepository.findOne({ where: { email } });
    if (!user || !user.password) {
      throw new UnauthorizedException('Invalid credentials');
    }
 
    const isPasswordValid = await bcrypt.compare(password, user.password);
    if (!isPasswordValid) {
      throw new UnauthorizedException('Invalid credentials');
    }
 
    return this.generateToken(user);
  }
 
  async googleLogin(token: string) {
    try {
      const ticket = await this.googleClient.verifyIdToken({
        idToken: token,
        audience: process.env.GOOGLE_CLIENT_ID,
      });
 
      const payload = ticket.getPayload();
      let user = await this.userRepository.findOne({
        where: { googleId: payload.sub },
      });
 
      if (!user) {
        user = this.userRepository.create({
          email: payload.email,
          username: payload.name,
          googleId: payload.sub,
          avatar: payload.picture,
        });
        await this.userRepository.save(user);
      }
 
      return this.generateToken(user);
    } catch (error) {
      throw new UnauthorizedException('Invalid Google token');
    }
  }
 
  private generateToken(user: User) {
    const payload = { sub: user.id, email: user.email, username: user.username };
    return {
      access_token: this.jwtService.sign(payload),
      user: { id: user.id, email: user.email, username: user.username },
    };
  }
 
  async validateToken(token: string) {
    try {
      return this.jwtService.verify(token);
    } catch (error) {
      throw new UnauthorizedException('Invalid token');
    }
  }
}

Step 5: WebSocket Gateway

src/chat/chat.gateway.ts
import {
  WebSocketGateway,
  WebSocketServer,
  SubscribeMessage,
  OnGatewayConnection,
  OnGatewayDisconnect,
} from '@nestjs/websockets';
import { Server, Socket } from 'socket.io';
import { ChatService } from './chat.service';
import { AuthService } from '../auth/auth.service';
 
@WebSocketGateway({
  cors: { origin: process.env.FRONTEND_URL },
  namespace: '/chat',
})
export class ChatGateway implements OnGatewayConnection, OnGatewayDisconnect {
  @WebSocketServer()
  server: Server;
 
  private userSockets = new Map<string, string>();
 
  constructor(
    private chatService: ChatService,
    private authService: AuthService,
  ) {}
 
  async handleConnection(client: Socket) {
    try {
      const token = client.handshake.auth.token;
      const payload = await this.authService.validateToken(token);
      
      client.data.userId = payload.sub;
      client.data.username = payload.username;
      
      this.userSockets.set(payload.sub, client.id);
      
      this.server.emit('user-online', {
        userId: payload.sub,
        username: payload.username,
      });
    } catch (error) {
      client.disconnect();
    }
  }
 
  handleDisconnect(client: Socket) {
    const userId = client.data.userId;
    if (userId) {
      this.userSockets.delete(userId);
      this.server.emit('user-offline', { userId });
    }
  }
 
  @SubscribeMessage('join-room')
  async handleJoinRoom(client: Socket, data: { roomId: string }) {
    client.join(data.roomId);
    
    const messages = await this.chatService.getMessages(data.roomId, 0, 50);
    client.emit('room-messages', messages);
    
    this.server.to(data.roomId).emit('user-joined', {
      userId: client.data.userId,
      username: client.data.username,
    });
  }
 
  @SubscribeMessage('send-message')
  async handleSendMessage(
    client: Socket,
    data: { roomId: string; content: string },
  ) {
    const message = await this.chatService.createMessage({
      roomId: data.roomId,
      userId: client.data.userId,
      username: client.data.username,
      content: data.content,
    });
 
    this.server.to(data.roomId).emit('new-message', message);
  }
 
  @SubscribeMessage('typing')
  handleTyping(client: Socket, data: { roomId: string }) {
    this.server.to(data.roomId).emit('user-typing', {
      userId: client.data.userId,
      username: client.data.username,
    });
  }
 
  @SubscribeMessage('stop-typing')
  handleStopTyping(client: Socket, data: { roomId: string }) {
    this.server.to(data.roomId).emit('user-stop-typing', {
      userId: client.data.userId,
    });
  }
}

Step 6: Chat Service

src/chat/chat.service.ts
import { Injectable } from '@nestjs/common';
import { InjectModel } from '@nestjs/mongoose';
import { Model } from 'mongoose';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { Message } from '../schemas/message.schema';
import { ChatRoom } from '../entities/chat-room.entity';
 
@Injectable()
export class ChatService {
  constructor(
    @InjectModel(Message.name)
    private messageModel: Model<Message>,
    @InjectRepository(ChatRoom)
    private chatRoomRepository: Repository<ChatRoom>,
  ) {}
 
  async createMessage(data: {
    roomId: string;
    userId: string;
    username: string;
    content: string;
  }) {
    const message = new this.messageModel(data);
    return message.save();
  }
 
  async getMessages(roomId: string, skip: number, limit: number) {
    return this.messageModel
      .find({ roomId })
      .sort({ createdAt: -1 })
      .skip(skip)
      .limit(limit)
      .lean();
  }
 
  async createRoom(data: {
    name: string;
    description?: string;
    creatorId: string;
    isPrivate: boolean;
  }) {
    const room = this.chatRoomRepository.create({
      name: data.name,
      description: data.description,
      creator: { id: data.creatorId } as any,
      isPrivate: data.isPrivate,
    });
    return this.chatRoomRepository.save(room);
  }
 
  async getRooms(userId: string) {
    return this.chatRoomRepository
      .createQueryBuilder('room')
      .leftJoinAndSelect('room.members', 'members')
      .where('room.creatorId = :userId OR members.id = :userId', { userId })
      .getMany();
  }
 
  async joinRoom(roomId: string, userId: string) {
    const room = await this.chatRoomRepository.findOne({
      where: { id: roomId },
      relations: ['members'],
    });
 
    if (!room.members.find((m) => m.id === userId)) {
      room.members.push({ id: userId } as any);
      await this.chatRoomRepository.save(room);
    }
 
    return room;
  }
}

Step 7: Docker Setup

docker-compose.yml
version: '3.8'
 
services:
  postgres:
    image: postgres:15
    environment:
      POSTGRES_USER: chat_user
      POSTGRES_PASSWORD: password
      POSTGRES_DB: chat_db
    ports:
      - "5432:5432"
    volumes:
      - postgres_data:/var/lib/postgresql/data
 
  mongodb:
    image: mongo:7.0
    ports:
      - "27017:27017"
    volumes:
      - mongodb_data:/data/db
 
  backend:
    build: ./chat-backend
    ports:
      - "3000:3000"
    environment:
      DB_HOST: postgres
      DB_PORT: 5432
      DB_USER: chat_user
      DB_PASSWORD: password
      DB_NAME: chat_db
      MONGODB_URI: mongodb://mongodb:27017/chat
      JWT_SECRET: your-secret-key
      GOOGLE_CLIENT_ID: your-google-client-id
      FRONTEND_URL: http://localhost:5173
    depends_on:
      - postgres
      - mongodb
 
volumes:
  postgres_data:
  mongodb_data:

Frontend Architecture (TanStack Start)

Step 1: Project Setup

Create TanStack Start project
npm create @tanstack/start@latest chat-frontend
cd chat-frontend
npm install socket.io-client zustand axios
npm install @react-oauth/google
npm install zustand

Step 2: Store Setup (Zustand)

src/store/chatStore.ts
import { create } from 'zustand';
import { io, Socket } from 'socket.io-client';
 
interface Message {
  _id: string;
  roomId: string;
  userId: string;
  username: string;
  content: string;
  createdAt: Date;
}
 
interface ChatRoom {
  id: string;
  name: string;
  description?: string;
  members: any[];
}
 
interface ChatStore {
  socket: Socket | null;
  user: any | null;
  rooms: ChatRoom[];
  currentRoom: ChatRoom | null;
  messages: Message[];
  typingUsers: Set<string>;
  onlineUsers: Set<string>;
 
  setUser: (user: any) => void;
  setRooms: (rooms: ChatRoom[]) => void;
  setCurrentRoom: (room: ChatRoom) => void;
  addMessage: (message: Message) => void;
  setMessages: (messages: Message[]) => void;
  addTypingUser: (userId: string, username: string) => void;
  removeTypingUser: (userId: string) => void;
  addOnlineUser: (userId: string) => void;
  removeOnlineUser: (userId: string) => void;
 
  initSocket: (token: string) => void;
  joinRoom: (roomId: string) => void;
  sendMessage: (content: string) => void;
  createRoom: (name: string, description?: string) => void;
}
 
export const useChatStore = create<ChatStore>((set, get) => ({
  socket: null,
  user: null,
  rooms: [],
  currentRoom: null,
  messages: [],
  typingUsers: new Set(),
  onlineUsers: new Set(),
 
  setUser: (user) => set({ user }),
  setRooms: (rooms) => set({ rooms }),
  setCurrentRoom: (room) => set({ currentRoom: room }),
  addMessage: (message) =>
    set((state) => ({ messages: [...state.messages, message] })),
  setMessages: (messages) => set({ messages }),
  addTypingUser: (userId, username) =>
    set((state) => ({
      typingUsers: new Set([...state.typingUsers, `${userId}:${username}`]),
    })),
  removeTypingUser: (userId) =>
    set((state) => {
      const newSet = new Set(state.typingUsers);
      newSet.forEach((item) => {
        if (item.startsWith(userId)) newSet.delete(item);
      });
      return { typingUsers: newSet };
    }),
  addOnlineUser: (userId) =>
    set((state) => ({ onlineUsers: new Set([...state.onlineUsers, userId]) })),
  removeOnlineUser: (userId) =>
    set((state) => {
      const newSet = new Set(state.onlineUsers);
      newSet.delete(userId);
      return { onlineUsers: newSet };
    }),
 
  initSocket: (token) => {
    const socket = io(import.meta.env.VITE_API_URL, {
      auth: { token },
      namespace: '/chat',
    });
 
    socket.on('new-message', (message) => {
      get().addMessage(message);
    });
 
    socket.on('user-typing', ({ userId, username }) => {
      get().addTypingUser(userId, username);
    });
 
    socket.on('user-stop-typing', ({ userId }) => {
      get().removeTypingUser(userId);
    });
 
    socket.on('user-online', ({ userId }) => {
      get().addOnlineUser(userId);
    });
 
    socket.on('user-offline', ({ userId }) => {
      get().removeOnlineUser(userId);
    });
 
    socket.on('room-messages', (messages) => {
      get().setMessages(messages);
    });
 
    set({ socket });
  },
 
  joinRoom: (roomId) => {
    const socket = get().socket;
    if (socket) {
      socket.emit('join-room', { roomId });
    }
  },
 
  sendMessage: (content) => {
    const socket = get().socket;
    const currentRoom = get().currentRoom;
    if (socket && currentRoom) {
      socket.emit('send-message', {
        roomId: currentRoom.id,
        content,
      });
    }
  },
 
  createRoom: (name, description) => {
    // API call untuk create room
  },
}));

Step 3: Authentication Context

src/components/AuthProvider.tsx
import { createContext, useContext, useEffect, useState } from 'react';
import { useGoogleLogin } from '@react-oauth/google';
import axios from 'axios';
 
interface AuthContextType {
  user: any | null;
  token: string | null;
  login: (email: string, password: string) => Promise<void>;
  register: (email: string, username: string, password: string) => Promise<void>;
  googleLogin: () => void;
  logout: () => void;
}
 
const AuthContext = createContext<AuthContextType | undefined>(undefined);
 
export const AuthProvider = ({ children }: { children: React.ReactNode }) => {
  const [user, setUser] = useState(null);
  const [token, setToken] = useState(localStorage.getItem('token'));
 
  const login = async (email: string, password: string) => {
    const response = await axios.post(
      `${import.meta.env.VITE_API_URL}/auth/login`,
      { email, password }
    );
    setToken(response.data.access_token);
    setUser(response.data.user);
    localStorage.setItem('token', response.data.access_token);
  };
 
  const register = async (
    email: string,
    username: string,
    password: string
  ) => {
    const response = await axios.post(
      `${import.meta.env.VITE_API_URL}/auth/register`,
      { email, username, password }
    );
    setToken(response.data.access_token);
    setUser(response.data.user);
    localStorage.setItem('token', response.data.access_token);
  };
 
  const googleLogin = useGoogleLogin({
    onSuccess: async (codeResponse) => {
      const response = await axios.post(
        `${import.meta.env.VITE_API_URL}/auth/google-login`,
        { token: codeResponse.access_token }
      );
      setToken(response.data.access_token);
      setUser(response.data.user);
      localStorage.setItem('token', response.data.access_token);
    },
  });
 
  const logout = () => {
    setUser(null);
    setToken(null);
    localStorage.removeItem('token');
  };
 
  return (
    <AuthContext.Provider value={{ user, token, login, register, googleLogin, logout }}>
      {children}
    </AuthContext.Provider>
  );
};
 
export const useAuth = () => {
  const context = useContext(AuthContext);
  if (!context) {
    throw new Error('useAuth must be used within AuthProvider');
  }
  return context;
};

Step 4: Chat Components

src/components/ChatRoom.tsx
import { useEffect, useState } from 'react';
import { useChatStore } from '../store/chatStore';
import { useAuth } from './AuthProvider';
 
export const ChatRoom = () => {
  const { user } = useAuth();
  const {
    currentRoom,
    messages,
    typingUsers,
    sendMessage,
    joinRoom,
    socket,
  } = useChatStore();
  const [input, setInput] = useState('');
  const [isTyping, setIsTyping] = useState(false);
 
  useEffect(() => {
    if (currentRoom && socket) {
      joinRoom(currentRoom.id);
    }
  }, [currentRoom, socket]);
 
  const handleSendMessage = () => {
    if (input.trim()) {
      sendMessage(input);
      setInput('');
      setIsTyping(false);
      if (socket) {
        socket.emit('stop-typing', { roomId: currentRoom?.id });
      }
    }
  };
 
  const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    setInput(e.target.value);
 
    if (!isTyping && socket) {
      setIsTyping(true);
      socket.emit('typing', { roomId: currentRoom?.id });
    }
  };
 
  if (!currentRoom) {
    return <div>Select room untuk start chatting</div>;
  }
 
  return (
    <div className="flex flex-col h-full">
      <div className="flex-1 overflow-y-auto p-4">
        {messages.map((msg) => (
          <div
            key={msg._id}
            className={`mb-4 ${msg.userId === user?.id ? 'text-right' : ''}`}
          >
            <div className="font-semibold text-sm">{msg.username}</div>
            <div
              className={`inline-block p-2 rounded ${
                msg.userId === user?.id
                  ? 'bg-blue-500 text-white'
                  : 'bg-gray-200'
              }`}
            >
              {msg.content}
            </div>
            <div className="text-xs text-gray-500">
              {new Date(msg.createdAt).toLocaleTimeString()}
            </div>
          </div>
        ))}
 
        {typingUsers.size > 0 && (
          <div className="text-sm text-gray-500 italic">
            {Array.from(typingUsers)
              .map((u) => u.split(':')[1])
              .join(', ')} is typing...
          </div>
        )}
      </div>
 
      <div className="border-t p-4 flex gap-2">
        <input
          type="text"
          value={input}
          onChange={handleInputChange}
          onKeyPress={(e) => e.key === 'Enter' && handleSendMessage()}
          placeholder="Type message..."
          className="flex-1 border rounded px-3 py-2"
        />
        <button
          onClick={handleSendMessage}
          className="bg-blue-500 text-white px-4 py-2 rounded"
        >
          Send
        </button>
      </div>
    </div>
  );
};

Best Practices

1. Security

ts
// ✅ Validate JWT tokens
// ✅ Use HTTPS dalam production
// ✅ Implement rate limiting
// ✅ Sanitize user input
// ✅ Use secure WebSocket (WSS)
// ✅ Implement CORS properly

2. Performance

ts
// ✅ Paginate messages
// ✅ Use message caching
// ✅ Implement connection pooling
// ✅ Use compression
// ✅ Optimize database queries
// ✅ Implement lazy loading

3. Scalability

ts
// ✅ Use Redis untuk session management
// ✅ Implement horizontal scaling
// ✅ Use message queues (RabbitMQ, Kafka)
// ✅ Separate read/write databases
// ✅ Implement load balancing
// ✅ Use CDN untuk static assets

4. Real-Time Features

ts
// ✅ Implement typing indicators
// ✅ Show online/offline status
// ✅ Implement message read receipts
// ✅ Handle connection reconnection
// ✅ Implement message delivery confirmation
// ✅ Handle network failures gracefully

5. Database Design

ts
// ✅ Index frequently queried fields
// ✅ Use TTL untuk temporary data
// ✅ Archive old messages
// ✅ Implement data retention policies
// ✅ Use transactions untuk consistency
// ✅ Monitor database performance

Common Mistakes & Pitfalls

1. Not Handling Connection Failures

ts
// ❌ Wrong - no reconnection logic
socket.on('disconnect', () => {
  console.log('Disconnected');
});
 
// ✅ Correct - implement reconnection
socket.on('disconnect', () => {
  setTimeout(() => {
    socket.connect();
  }, 5000);
});

2. Storing Sensitive Data dalam Frontend

ts
// ❌ Wrong - storing password
localStorage.setItem('password', password);
 
// ✅ Correct - hanya store token
localStorage.setItem('token', token);

3. Not Validating Messages

ts
// ❌ Wrong - no validation
socket.on('send-message', (data) => {
  saveMessage(data);
});
 
// ✅ Correct - validate input
socket.on('send-message', (data) => {
  if (data.content && data.content.trim().length > 0) {
    saveMessage(data);
  }
});

4. Inefficient Message Loading

ts
// ❌ Wrong - load semua messages
const messages = await Message.find({ roomId });
 
// ✅ Correct - paginate messages
const messages = await Message.find({ roomId })
  .sort({ createdAt: -1 })
  .limit(50)
  .skip(page * 50);

5. Not Handling Concurrent Updates

ts
// ❌ Wrong - race condition
user.messageCount++;
await user.save();
 
// ✅ Correct - atomic operation
await User.updateOne(
  { id: userId },
  { $inc: { messageCount: 1 } }
);

Deployment

Deploy dengan Docker
# Build images
docker-compose build
 
# Start services
docker-compose up -d
 
# View logs
docker-compose logs -f
 
# Stop services
docker-compose down

Conclusion

Membangun real-time chat application memerlukan careful consideration dari security, performance, dan scalability. Dengan following best practices dan using right technologies, Anda bisa build robust chat system yang handle thousands dari concurrent users.

Key takeaways:

  1. Use WebSocket untuk real-time communication
  2. Implement proper authentication dengan JWT dan SSO
  3. Use MongoDB untuk flexible message storage
  4. Use PostgreSQL untuk structured data
  5. Implement proper error handling dan reconnection logic
  6. Monitor performance dan optimize queries
  7. Implement security best practices
  8. Test thoroughly sebelum deployment

Next steps:

  1. Set up development environment
  2. Implement authentication
  3. Build WebSocket connection
  4. Create chat UI components
  5. Implement message persistence
  6. Add real-time features
  7. Deploy ke production
  8. Monitor dan optimize

Real-time chat applications adalah complex tapi rewarding untuk build. Master concepts ini, dan Anda akan bisa build scalable, secure communication platforms.


Related Posts