Skip to content

Repository files navigation

MiniChat - Hệ Thống Chat Thời Gian Thực Enterprise (NestJS & Next.js)

MiniChat là nền tảng chat thời gian thực quy mô Enterprise được xây dựng theo mô hình pnpm Monorepo, kết hợp giữa NestJS Server (Port 4000)Next.js App Router Client (Port 3000). Hệ thống kết hợp hài hòa giữa kênh truyền REST API cho dữ liệu phi trạng thái/truyền tải tệp lớn và kênh WebSocket song công (Full-duplex) qua Socket.IO cho tương tác tức thời.


Mục Lục

  1. Kiến Trúc Client - Server Hiện Đại
  2. Giao Thức Socket.IO & WebSocket
  3. Các Sơ Đồ Luồng Tuần Tự Hệ Thống (Sequence Diagrams)
  4. Framework NestJS - Phía Server (Port 4000)
  5. Framework Next.js - Phía Client (Port 3000)
  6. Ma Trận Sự Kiện WebSocket
  7. Thực Nghiệm Mã Nguồn
  8. Hướng Dẫn Cài Đặt & Khởi Chạy

1. Kiến Trúc Client - Server Hiện Đại

1.1. Sơ đồ Kiến trúc Tổng thể

flowchart TB
    subgraph Client_Tier["CLIENT TIER: Next.js (App Router - Port 3000)"]
        direction TB
        RSC["React Server Components (RSC)<br/>• Layouts, Static UI Shell, Server Prefetch<br/>• Zero Client JS Bundle Size"]
        CC["Client Components ('use client')<br/>• ChatBox, MessageList, TypingIndicator, FileUploader"]
        Hook["Custom Hook: useMiniChatSocket / useChatSocket<br/>• Singleton socket instance qua useRef<br/>• Cleanup Phase qua useEffect (socket.off, disconnect)"]
        
        RSC --> CC
        CC --> Hook
    end

    subgraph Transport_Layer["DUAL-CHANNEL TRANSPORT & PROTOCOL LAYER"]
        direction LR
        REST_Chan["HTTP/1.1 REST Channel (Port 4000)<br/>• POST /upload/stream (Multipart Chunked Stream)<br/>• GET /api/server-status (Health check & Monitoring)"]
        WS_Chan["Full-Duplex Socket.IO Channel (Port 4000)<br/>• Upgrade Handshake: HTTP 101 Switching Protocols<br/>• Persistent TCP Frame Engine"]
    end

    subgraph Server_Tier["SERVER TIER: NestJS Enterprise Architecture (Port 4000)"]
        direction TB
        AppModule["AppModule (Root IoC Container)"]

        subgraph Chat_Module["ChatModule"]
            ChatGateway["ChatGateway (@WebSocketGateway)<br/>• Decorators: @WebSocketServer, @SubscribeMessage, @ConnectedSocket, @MessageBody<br/>• Lifecycle: OnGatewayInit, OnGatewayConnection, OnGatewayDisconnect"]
            ChatService["ChatService (@Injectable)<br/>• Quản lý Room Memory & Connection Sessions<br/>• Broadcast & Direct Message Dispatcher"]
        end

        subgraph Media_Module["MediaStreamModule"]
            UploadController["MediaStreamController (@Controller 'upload')<br/>• Endpoint: POST /upload/stream"]
            StreamService["MediaStreamService (@Injectable)<br/>• pipeline(readable, writable) với highWaterMark<br/>• Backpressure Flow Control & SHA-256 Hashing"]
        end

        AppModule --> Chat_Module
        AppModule --> Media_Module
        ChatGateway --> ChatService
        UploadController --> StreamService
        StreamService -.->|Trigger Metadata & Message Event| ChatGateway
    end

    CC -->|1. Multipart Binary Stream| REST_Chan --> UploadController
    Hook <==>|2. Bidirectional Events & Ack| WS_Chan <==> ChatGateway
Loading

1.2. Cơ chế Kết hợp Dual-Channel (REST & WebSocket)

  • Kênh REST API: Đảm nhiệm các thao tác phi trạng thái (Stateless), yêu cầu xác thực độc lập, truy vấn dữ liệu ban đầu, và truyền tải tệp tin dung lượng lớn qua cơ chế Multipart / Binary Stream.
  • Kênh WebSocket (Socket.IO): Thiết lập kênh kết nối 2 chiều liên tục (Full-duplex) với độ trễ cực thấp và chi phí overhead chỉ vài bytes/frame, phục vụ trao đổi tin nhắn tức thời, trạng thái gõ phím (typing indicator), cập nhật danh sách người dùng và phát tán thông báo metadata.

1.3. Bảng So Sánh HTTP Polling vs Long-Polling vs SSE vs WebSocket

Tiêu chí HTTP Polling HTTP Long-Polling Server-Sent Events (SSE) WebSocket / Socket.IO
Cơ chế hoạt động Client gửi request định kỳ (ví dụ mỗi 2-5s) Client gửi request, Server giữ connection đến khi có dữ liệu Server mở stream 1 chiều liên tục đẩy dữ liệu về Client Thiết lập kết nối TCP bền bỉ 2 chiều (Full-duplex) sau bắt tay HTTP 101
Băng thông & Overhead Rất lãng phí (~1KB header HTTP cho mỗi request rỗng) Lãng phí (Tạo lại kết nối TCP và gửi lặp lại Header HTTP) Tiết kiệm (Header chỉ gửi 1 lần khi thiết lập stream) Tối ưu vượt trội (Frame nhị phân chỉ từ 2 đến 6 bytes)
Độ trễ (Latency) Rất cao (Bằng polling interval) Trung bình (Độ trễ tái thiết lập connection) Rất thấp (Server gửi tức thì) Cực thấp (Real-time: 15 - 25 ms)
Tính hai chiều Bán song công (Half-duplex) Bán song công (Half-duplex) Đơn công (Chỉ Server $\to$ Client) Toàn song công (Full-duplex song song đồng thời)
Phù hợp nhất Dashboard dữ liệu cập nhật theo phút Ứng dụng chat đời cũ Bảng tin tức, chứng khoán, thông báo một chiều Chat thời gian thực, Game, Live Collaboration, Video/Audio Signaling

2. Giao Thức Socket.IO & WebSocket

2.1. Tiến trình Bắt tay Nâng cấp (Upgrade Handshake)

1. Client gửi HTTP GET Request kèm Upgrade Headers:
GET /socket.io/?EIO=4&transport=polling HTTP/1.1
Host: localhost:4000
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Sec-WebSocket-Version: 13

2. Server NestJS xử lý SHA-1 Magic String và phản hồi:
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=

Sau bước này, giao thức HTTP được nâng cấp thành kết nối WebSocket nhị phân hai chiều trực tiếp trên cổng 4000.

2.2. Quản lý Room & Cơ chế Broadcast

  • Room Management: Không gian định tuyến logic ảo (ví dụ: general, project-alpha). Socket instance có thể tham gia (client.join(roomId)) hoặc rời khỏi (client.leave(roomId)).
  • Cơ chế Phân Phối (Broadcast Scope):
    • Broadcast To All: this.server.emit('event', data) — Gửi tới toàn bộ client kết nối vào server.
    • Broadcast To Room: this.server.to(roomId).emit('event', data) — Gửi tới toàn bộ client trong room chỉ định.
    • Broadcast To Room Except Sender: client.to(roomId).emit('event', data) — Gửi tới mọi client trong room trừ client phát tín hiệu.

3. Các Sơ Đồ Luồng Tuần Tự Hệ Thống (Sequence Diagrams)

3.1. Luồng 1: Thiết Lập Kết Nối & Gia Nhập Phòng (Handshake & Join Room)

sequenceDiagram
    autonumber
    actor User as Người dùng (Browser)
    participant Client as Next.js Client (Port 3000)
    participant Gateway as ChatGateway (Port 4000)
    participant Service as ChatService
    participant Room as Thành viên trong Phòng

    User->>Client: Mở giao diện chat và đăng nhập (Alice, phòng general)
    Client->>Gateway: 1. HTTP GET /socket.io/?transport=websocket (Upgrade Handshake)
    Gateway-->>Client: 2. HTTP 101 Switching Protocols (Nâng cấp thành công)
    Note over Client,Gateway: Kết nối WebSocket hai chiều toàn song công (Full-duplex) được thiết lập
    Gateway->>Service: handleConnection(client) -> Ghi nhận Socket ID
    
    Client->>Gateway: 3. Emit "joinRoom" (room: general, username: Alice)
    Gateway->>Service: handleStandardJoinRoom(client, payload)
    Service->>Service: client.join("general") và Lưu Session
    Service->>Room: 4. Emit "userJoined" và "userStatusChanged" (Alice JOINED)
    Gateway-->>Client: 5. Ack Callback "joinRoomSuccess"
    Client-->>User: Hiển thị giao diện phòng chat sẵn sàng
Loading

3.2. Luồng 2: Nhắn Tin Thời Gian Thực & Trạng Thái Gõ Phím (Chat & Typing)

sequenceDiagram
    autonumber
    actor Alice as Alice (Người gửi)
    participant ClientA as Next.js Client (Alice)
    participant Gateway as ChatGateway (NestJS)
    participant Service as ChatService & Store
    participant ClientB as Next.js Client (Bob)
    actor Bob as Bob (Người nhận)

    Alice->>ClientA: Bắt đầu gõ phím vào khung soạn thảo
    ClientA->>Gateway: 1. Emit "typingStatus" (isTyping: true)
    Gateway->>ClientB: 2. Broadcast "typingStatus" (Alice isTyping: true)
    ClientB-->>Bob: Hiển thị chỉ báo "Alice đang soạn tin nhắn..."

    Alice->>ClientA: Nhấn nút "Gửi" (Nội dung: "Xin chào Bob!")
    ClientA->>Gateway: 3. Emit "sendMessage" (room: general, sender: Alice)
    Gateway->>Service: handleStandardSendMessage(client, payload)
    Service->>Service: Đóng gói MessageDto, tăng totalMessages, ghi Activity Log
    Service->>Gateway: Phát tán tin nhắn tới phòng general
    
    par Phân phối tin nhắn đồng thời
        Gateway->>ClientA: 4a. Ack Callback (status: success, messageId)
        Gateway->>ClientB: 4b. Broadcast "receiveMessage" (Alice: "Xin chào Bob!")
    end

    ClientA->>Gateway: 5. Emit "typingStatus" (isTyping: false)
    Gateway->>ClientB: 6. Broadcast "typingStatus" (Alice isTyping: false)
    ClientB-->>Bob: Render tin nhắn mới vào MessageList và ẩn Typing Indicator
Loading

3.3. Luồng 3: Đính Kèm & Dán Ảnh Clipboard (Image Attachment & Preview)

sequenceDiagram
    autonumber
    actor Alice as Alice (Người dùng)
    participant UI as Giao diện Chat (Next.js)
    participant SocketHook as useChatSocket / Socket.IO
    participant Gateway as ChatGateway (NestJS)
    participant Members as Thành viên nhận ảnh

    alt Cách 1: Chụp màn hình và Dán trực tiếp
        Alice->>UI: Nhấn Ctrl+V vào ô chat (Paste từ Clipboard)
    else Cách 2: Chọn từ tệp
        Alice->>UI: Click icon ảnh hoặc Kéo thả tệp vào ô chat
    end

    UI->>UI: 1. processImageFile (Kiểm tra PNG/JPEG/GIF/WEBP và dung lượng <= 5MB)
    UI->>UI: 2. FileReader tạo DataURL và hiển thị Thẻ Preview
    Note over Alice,UI: Ảnh CHƯA gửi ngay - người dùng có thể xem trước hoặc bấm xóa

    Alice->>UI: 3. Nhập chú thích kèm theo và bấm nút Gửi
    UI->>SocketHook: 4. emitImage (Sự kiện gửi ảnh kèm caption)
    SocketHook->>Gateway: 5. Socket.IO Event "group-image-message"
    Gateway->>Members: 6. Broadcast "receive-image-message" (Base64 Image + Caption)
    Members-->>Members: Hiển thị hình ảnh và nội dung chú thích
    UI->>UI: 7. Reset ô nhập liệu và đóng Thẻ Preview
Loading

3.4. Luồng 4: Truyền Tải Tệp Phân Đoạn Stream, Backpressure & SHA-256

sequenceDiagram
    autonumber
    actor Client as Next.js Client (Port 3000)
    participant REST as MediaStreamController (POST /upload/stream)
    participant StreamService as MediaStreamService
    participant Disk as Storage Disk (fs.createWriteStream)
    participant Gateway as ChatGateway (WebSocket)
    participant Room as Room Members (Clients)

    Client->>REST: 1. POST /upload/stream (Gửi Binary Stream)
    REST->>StreamService: 2. handleStreamUpload(req, fileName, roomId, sender)
    
    rect rgb(240, 248, 255)
        Note over StreamService,Disk: Cơ chế Backpressure Flow Control và Tính toán Hash tức thì
        loop Đọc và Ghi từng phân đoạn 64KB (Chunk)
            StreamService->>StreamService: Tính toán mã băm SHA-256 (crypto.createHash)
            StreamService->>Disk: pipeline(readableStream, writableStream)
            Note over Disk: Nếu đĩa ghi chậm - Kích hoạt Backpressure tạm dừng đọc buffer
        end
    end

    Disk-->>StreamService: 3. Ghi đĩa hoàn tất và tạo mã SHA-256
    StreamService->>Gateway: 4. Kích hoạt sự kiện hoàn tất tệp
    
    par Thông báo kép tới toàn bộ phòng
        Gateway->>Room: 5a. WebSocket Emit "fileMetadataReady" (Metadata + SHA-256)
        Gateway->>Room: 5b. WebSocket Emit "receiveMessage" (Thông báo tệp mới)
    end

    StreamService-->>REST: 6. Trả về FileUploadResult DTO
    REST-->>Client: 7. HTTP 201 Created (Upload Response)
Loading

3.5. Luồng 5: Rời Phòng & Ngắt Kết Nối (Leave Room & Disconnect Lifecycle)

sequenceDiagram
    autonumber
    actor User as Người dùng
    participant Client as Next.js Client (Port 3000)
    participant Gateway as ChatGateway (Port 4000)
    participant Service as ChatService & Store
    participant Dashboard as Monitor Dashboard

    alt Kịch bản A: Người dùng chủ động đổi phòng hoặc rời phòng
        User->>Client: Chọn đổi sang phòng khác
        Client->>Gateway: 1a. Emit "leaveRoom" (room: general, username: Alice)
        Gateway->>Service: handleStandardLeaveRoom() -> client.leave("general")
        Service->>Gateway: Broadcast "userLeft" và "userStatusChanged" (LEFT)
        Client->>Gateway: 1b. Emit "joinRoom" (room: dev-team, username: Alice)
    else Kịch bản B: Người dùng tắt trình duyệt hoặc mất mạng
        User->>Client: Đóng Tab hoặc Tắt ứng dụng
        Client-xGateway: 2a. TCP Connection Terminated
        Gateway->>Gateway: 2b. Lifecycle Hook: handleDisconnect(client)
        Gateway->>Service: cleanupUser(client, username)
        Service->>Service: Cập nhật status offline và lastSeen
        
        par Phát thông báo cập nhật toàn hệ thống
            Service->>Gateway: Broadcast "userStatusChanged" (Alice OFFLINE)
            Service->>Gateway: Broadcast "groups-updated" tới các client còn lại
            Service->>Dashboard: Broadcast "stats-update" (Cập nhật số người online)
        end
    end
Loading

4. Framework NestJS - Phía Server (Port 4000)

4.1. Triết lý Modular Architecture & IoC / DI

  • Modules (@Module): Đóng gói các ranh giới nghiệp vụ riêng biệt (ChatModule, MediaStreamModule, ApiModule, SharedModule).
  • Controllers (@Controller): Tiếp nhận các REST HTTP requests (MediaStreamController, ApiController).
  • Services / Providers (@Injectable): Xử lý logic cốt lõi. IoC Container của NestJS tự động quản lý vòng đời và tiêm phụ thuộc (Dependency Injection) qua constructor.

4.2. WebSocket Gateway & Lifecycle Interfaces

  • @WebSocketGateway(4000, { cors: ..., namespace: '/chat' }): Khởi tạo và liên kết WebSocket Server trên port 4000.
  • @WebSocketServer(): Tiêm đối tượng Socket.IO Server vào Gateway.
  • @SubscribeMessage('event'): Đăng ký listener lắng nghe sự kiện từ client.
  • @ConnectedSocket() & @MessageBody(): Trích xuất socket của client và payload của thông điệp.
  • 3 Giao diện Vòng đời:
    1. OnGatewayInit (afterInit): Chạy một lần khi gateway được khởi tạo thành công.
    2. OnGatewayConnection (handleConnection): Chạy khi có client mới kết nối.
    3. OnGatewayDisconnect (handleDisconnect): Chạy khi client ngắt kết nối.

4.3. Xử lý Stream & Kiểm soát Backpressure (Chống tràn RAM)

Khi truyền tải tệp dung lượng lớn (hàng trăm MB - GB), đọc file toàn bộ vào Buffer sẽ gây Buffer Overflow / OOM.

  • Cơ chế Backpressure: Khi ReadableStream đọc nhanh hơn tốc độ ghi của WritableStream, WritableStream đạt ngưỡng highWaterMark (64KB), tự động phát tín hiệu tạm dừng (pause()) và kích hoạt sự kiện drain để resume().
  • Sử dụng pipeline():
import { pipeline } from 'node:stream/promises';
import { createWriteStream } from 'node:fs';
import { Readable } from 'node:stream';

// Tự động kiểm soát luồng dữ liệu, backpressure và an toàn giải phóng bộ nhớ
await pipeline(
  readableStream,
  createWriteStream(destinationPath, { highWaterMark: 64 * 1024 })
);

5. Framework Next.js - Phía Client (Port 3000)

5.1. App Router & Cơ chế Dựng hình (SSR, CSR, Hydration)

  • SSR (Server-Side Rendering): Render giao diện ban đầu thành HTML tĩnh trên Server giúp tối ưu SEO và thời gian First Contentful Paint (FCP).
  • CSR (Client-Side Rendering): Xử lý các logic thay đổi trạng thái và tương tác real-time trên trình duyệt.
  • Hydration: Quá trình React gắn các event listener và state vào HTML đã được render sẵn từ SSR.

5.2. Bảng So Sánh React Server Components vs Client Components

Tiêu chí So Sánh React Server Components (RSC - Default) Client Components ('use client')
Môi trường thực thi Hoàn toàn trên Server (Node.js/Edge) Server (Pre-render HTML) & Browser (Hydration)
Kích thước Bundle Client 0 KB (Không đóng gói mã nguồn JS vào client bundle) Tính vào dung lượng JavaScript bundle tải về browser
Khả năng dùng Hooks Không (useState, useEffect, useRef...) Đầy đủ (useState, useEffect, useCallback...)
Quyền truy cập Web API Không (Không có window, document, localStorage) (Truy cập window, WebSockets, DOM Events)
Phạm vi ứng dụng trong MiniChat Root Layout, Sidebar tĩnh, Prefetch dữ liệu tĩnh ChatBox, MessageList, FileUploader, WebSocket Hooks

5.3. Quản lý Socket Lifecycle với Custom Hooks (useMiniChatSocket & useChatSocket)

Để loại bỏ tình trạng Memory LeakDuplicate Connection (đặc biệt trong chế độ React StrictMode):

  1. Singleton Instance: Sử dụng useRef<Socket | null> lưu socket instance để không bị khởi tạo lại qua các lần re-render.
  2. Quản lý Vòng đời: Thiết lập useEffect với dependency rõ ràng, tự động kết nối và join room khi mount.
  3. Cleanup Phase: Khi component unmount, tự động gọi socket.off() cho mọi listener và socket.disconnect() để giải phóng kết nối trên Server.

6. Ma Trận Sự Kiện WebSocket (Event Matrix)

Tên Sự kiện (Event) Chiều (Direction) Payload Schema (DTO) Phạm vi Phát tán (Scope) Mục đích Xử lý Nghiệp vụ
joinRoom Client $\to$ Server { room: string, username: string } Direct to Server Đăng ký client vào Socket.IO Room logic
userJoined Server $\to$ Client { username: string, socketId: string, timestamp: string } client.broadcast.to(roomId) Thông báo thành viên mới tham gia phòng
userStatusChanged Server $\to$ Client { username: string, status: string, room?: string } server.to(roomId).emit Cập nhật trạng thái JOINED / LEFT / OFFLINE
sendMessage Client $\to$ Server { room: string, sender: string, message: string, type?: string } Direct to Server Gửi tin nhắn mới có Ack callback
receiveMessage Server $\to$ Client ChatMessage (id, room, sender, message, timestamp) server.to(roomId).emit Phân phối tin nhắn mới đến toàn bộ phòng
typingStatus Client $\to$ Server $\to$ Client { room: string, username: string, isTyping: boolean } client.broadcast.to(roomId) Hiển thị thông báo đang soạn tin nhắn
fileMetadataReady Server $\to$ Client { roomId: string, fileId: string, fileName: string, fileUrl: string, size: number, sha256: string } server.to(roomId).emit Phát thông báo khi tệp lớn stream xong lên server
leaveRoom Client $\to$ Server { room: string, username: string } Direct to Server Rời phòng chat và thông báo cho các thành viên

6. Thực Nghiệm Mã Nguồn

6.1. ChatGateway.ts (NestJS)

@WebSocketGateway(4000, {
  cors: { origin: process.env.CLIENT_ORIGIN || 'http://localhost:3000', credentials: true },
  namespace: '/chat',
  transports: ['websocket', 'polling'],
})
@Injectable()
export class ChatGateway implements OnGatewayInit, OnGatewayConnection, OnGatewayDisconnect {
  @WebSocketServer() server!: Server;

  constructor(private readonly chatService: ChatService) {}

  afterInit(server: Server) { /* Khởi tạo server */ }
  handleConnection(client: Socket) { /* Kết nối client */ }
  handleDisconnect(client: Socket) { /* Dọn dẹp ngắt kết nối */ }

  @SubscribeMessage('joinRoom')
  handleJoinRoom(@ConnectedSocket() client: Socket, @MessageBody() payload: JoinRoomDto) {
    return this.chatService.handleStandardJoinRoom(client, payload);
  }

  @SubscribeMessage('sendMessage')
  handleSendMessage(@ConnectedSocket() client: Socket, @MessageBody() payload: SendMessageDto) {
    return this.chatService.handleStandardSendMessage(client, payload);
  }

  @SubscribeMessage('typingStatus')
  handleTypingStatus(@ConnectedSocket() client: Socket, @MessageBody() payload: TypingStatusDto) {
    return this.chatService.handleStandardTypingStatus(client, payload);
  }
}

6.2. Custom Hook useChatSocket.ts (Next.js)

'use client';
import { useEffect, useRef, useState, useCallback } from 'react';
import { io, Socket } from 'socket.io-client';

export function useChatSocket({ serverUrl = 'http://localhost:4000', roomId = 'general', username = '' }) {
  const socketRef = useRef<Socket | null>(null);
  const [isConnected, setIsConnected] = useState(false);
  const [messages, setMessages] = useState<StandardChatMessage[]>([]);

  useEffect(() => {
    if (!socketRef.current) {
      socketRef.current = io(serverUrl, { transports: ['websocket', 'polling'], withCredentials: true });
    }
    const socket = socketRef.current;
    
    socket.on('connect', () => {
      setIsConnected(true);
      socket.emit('joinRoom', { roomId, username });
    });
    socket.on('receiveMessage', (msg) => setMessages((prev) => [...prev, msg]));

    return () => {
      socket.off('connect');
      socket.off('receiveMessage');
      socket.disconnect();
      socketRef.current = null;
    };
  }, [serverUrl, roomId, username]);

  const sendMessage = useCallback((content: string) => {
    socketRef.current?.emit('sendMessage', { roomId, sender: username, content });
  }, [roomId, username]);

  return { isConnected, messages, sendMessage };
}

7. Hướng Dẫn Cài Đặt & Khởi Chạy

7.1. Yêu cầu Hệ thống

  • Node.js: v18.0.0 trở lên
  • pnpm: v9.0.0 trở lên

7.2. Cài đặt Dependencies

pnpm install

7.3. Chạy môi trường Phát triển (Development)

# Khởi chạy đồng thời cả Server (Port 4000) và Client (Port 3000)
pnpm run dev

# Hoặc khởi chạy độc lập từng service:
pnpm run dev:server    # NestJS Server: http://localhost:4000
pnpm run dev:client    # Next.js Client: http://localhost:3000

7.4. Build và Chạy Production với PM2

# 1. Build production bundles
pnpm run build

# 2. Khởi chạy thông qua PM2
pnpm start

# 3. Quản trị dịch vụ
pnpm run status      # Xem trạng thái services
pnpm run logs        # Xem streaming logs
pnpm run stop        # Dừng hệ thống
pnpm run restart     # Khởi động lại hệ thống

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages