Tuesday, 25 February 2025

Building a Scalable & Secure Private Chat with Laravel 11, Reverb, Redis & Vue 3 + TypeScript

This guide walks through creating a production-ready real-time private chat system using:
✅ Laravel 11 (Backend)
✅ Reverb + Redis (Real-time WebSockets)
✅ Vue 3 + TypeScript (Frontend)
✅ Nginx Load Balancer (Multi-server Scaling)


🔹 Backend: Laravel 11 + Reverb + Redis

1️⃣ Setup Laravel 11 & Dependencies

bash
composer create-project laravel/laravel chat-app cd chat-app composer require predis/predis

2️⃣ Install & Configure Laravel Reverb

bash
php artisan reverb:install php artisan vendor:publish --tag=reverb-config

🔹 Modify .env

env
BROADCAST_CONNECTION=redis QUEUE_CONNECTION=redis

🔹 Modify config/broadcasting.php

php
'connections' => [ 'redis' => [ 'driver' => 'redis', 'connection' => 'default', ], ],

🔹 Start Redis

bash
redis-server

🛠 Step 2: Database & Models

1️⃣ Create Chat Model & Migration

bash
php artisan make:model Chat -m

Define chats table structure

php
use Illuminate\Database\Migrations\Migration; use Illuminate\Database\Schema\Blueprint; use Illuminate\Support\Facades\Schema; return new class extends Migration { public function up() { Schema::create('chats', function (Blueprint $table) { $table->id(); $table->unsignedBigInteger('sender_id'); $table->unsignedBigInteger('receiver_id'); $table->text('message'); // Store encrypted messages $table->timestamps(); $table->foreign('sender_id')->references('id')->on('users')->onDelete('cascade'); $table->foreign('receiver_id')->references('id')->on('users')->onDelete('cascade'); }); } public function down() { Schema::dropIfExists('chats'); } };

Run Migration

bash
php artisan migrate

🛠 Step 3: Secure Private Channels

1️⃣ Define Private Channel Authorization

Edit routes/channels.php:

php
use Illuminate\Support\Facades\Broadcast; Broadcast::channel('chat.{receiverId}', function ($user, $receiverId) { return (int) $user->id === (int) $receiverId; });

🛠 Step 4: Broadcast Events Securely

1️⃣ Create Chat Event

bash
php artisan make:event MessageSent

Modify MessageSent.php

php
namespace App\Events; use App\Models\Chat; use Illuminate\Broadcasting\Channel; use Illuminate\Broadcasting\InteractsWithSockets; use Illuminate\Broadcasting\PrivateChannel; use Illuminate\Contracts\Broadcasting\ShouldBroadcastNow; use Illuminate\Queue\SerializesModels; class MessageSent implements ShouldBroadcastNow { use InteractsWithSockets, SerializesModels; public $chat; public function __construct(Chat $chat) { $this->chat = $chat; } public function broadcastOn() { return new PrivateChannel('chat.' . $this->chat->receiver_id); } public function broadcastWith() { return [ 'message' => $this->chat->message, 'sender_id' => $this->chat->sender_id, 'receiver_id' => $this->chat->receiver_id, 'timestamp' => $this->chat->created_at->toDateTimeString(), ]; } }

🛠 Step 5: Send Messages

1️⃣ Create ChatController.php

bash
php artisan make:controller ChatController

2️⃣ Store & Broadcast Messages Securely

Modify ChatController.php:

php
namespace App\Http\Controllers; use App\Events\MessageSent; use App\Models\Chat; use Illuminate\Http\Request; use Illuminate\Support\Facades\Auth; class ChatController extends Controller { public function sendMessage(Request $request) { $request->validate([ 'receiver_id' => 'required|exists:users,id', 'message' => 'required|string', ]); // Encrypt message before storing $chat = Chat::create([ 'sender_id' => Auth::id(), 'receiver_id' => $request->receiver_id, 'message' => encrypt($request->message), ]); // Broadcast event broadcast(new MessageSent($chat))->toOthers(); return response()->json(['message' => 'Sent successfully']); } }

🔹 Frontend: Vue 3 + TypeScript

🛠 Step 1: Setup Vue 3 + TypeScript

bash
npm create vue@latest chat-app-frontend cd chat-app-frontend npm install npm install @vueuse/core axios pusher-js laravel-echo

🛠 Step 2: Configure Laravel Echo

Modify src/plugins/echo.ts:

ts
import Echo from 'laravel-echo'; import Pusher from 'pusher-js'; window.Pusher = Pusher; const echo = new Echo({ broadcaster: 'pusher', key: 'reverb', wsHost: import.meta.env.VITE_APP_WS_HOST, wsPort: 6001, wssPort: 6001, forceTLS: false, disableStats: true, enabledTransports: ['ws', 'wss'] }); export default echo;

🔹 Modify .env

ini
VITE_APP_WS_HOST=localhost

🛠 Step 3: Chat Component

Create src/components/Chat.vue:

vue
<script setup lang="ts"> import { ref, onMounted } from 'vue'; import axios from 'axios'; import echo from '@/plugins/echo'; const userId = 1; // Replace with auth user ID const messages = ref<{ sender_id: number; message: string }[]>([]); const newMessage = ref(''); const sendMessage = async () => { await axios.post('http://localhost:8000/api/send-message', { receiver_id: 2, // Change to actual receiver message: newMessage.value }); newMessage.value = ''; }; onMounted(() => { echo.private(`chat.${userId}`) .listen('MessageSent', (e: any) => { messages.value.push(e); }); }); </script> <template> <div> <div v-for="msg in messages" :key="msg.message"> <strong v-if="msg.sender_id === userId">You:</strong> <strong v-else>Friend:</strong> {{ msg.message }} </div> <input v-model="newMessage" @keyup.enter="sendMessage" placeholder="Type a message..." /> <button @click="sendMessage">Send</button> </div> </template>

🔹 Deployment: Multi-Server Scaling

🛠 Step 1: Configure Load Balancer

Modify Nginx config:

nginx
upstream websocket_servers { server 192.168.1.101:6001; server 192.168.1.102:6001; } server { listen 80; server_name example.com; location /reverb { proxy_pass http://websocket_servers; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "Upgrade"; proxy_set_header Host $host; } }

🚀 Summary

✅ Private Chat with Laravel 11 + Reverb + Redis
✅ Vue 3 + TypeScript Frontend
✅ Private Channels for Authentication
✅ Load Balanced WebSockets for Multi-Server Scaling

💡 Next Step: Want a Docker setup for full production deployment? 🚀

Building a private chat real-time system using Laravel 11, Reverb, and Redis.

Here’s a step-by-step guide to building a private chat real-time system using Laravel 11, Reverb, and Redis.


Step 1: Install Laravel 11

If you haven’t already installed Laravel 11, create a new project:

bash
composer create-project laravel/laravel chat-app cd chat-app

Then, install dependencies:

bash
composer require predis/predis

Step 2: Install & Configure Laravel Reverb

1️⃣ Install Reverb

bash
php artisan reverb:install

2️⃣ Publish Reverb config file

bash
php artisan vendor:publish --tag=reverb-config

3️⃣ Update .env to use Redis broadcasting

env
BROADCAST_CONNECTION=redis QUEUE_CONNECTION=redis

4️⃣ Update config/broadcasting.php to use Redis

php
'connections' => [ 'redis' => [ 'driver' => 'redis', 'connection' => 'default', ], ],

5️⃣ Start Redis

bash
redis-server

Step 3: Create WebSocket Channel for Private Chat

1️⃣ Define a private channel in routes/channels.php

php
use Illuminate\Support\Facades\Broadcast; Broadcast::channel('chat.{receiverId}', function ($user, $receiverId) { return (int) $user->id === (int) $receiverId; });

💡 This ensures only authenticated users can listen to their own chat events.


Step 4: Create a Chat Model & Migration

1️⃣ Generate Chat Model & Migration

bash
php artisan make:model Chat -m

2️⃣ Define the Schema in database/migrations/YYYY_MM_DD_create_chats_table.php

php
use Illuminate\Database\Migrations\Migration; use Illuminate\Database\Schema\Blueprint; use Illuminate\Support\Facades\Schema; return new class extends Migration { public function up() { Schema::create('chats', function (Blueprint $table) { $table->id(); $table->unsignedBigInteger('sender_id'); $table->unsignedBigInteger('receiver_id'); $table->text('message'); $table->timestamps(); $table->foreign('sender_id')->references('id')->on('users')->onDelete('cascade'); $table->foreign('receiver_id')->references('id')->on('users')->onDelete('cascade'); }); } public function down() { Schema::dropIfExists('chats'); } };

3️⃣ Run Migration

bash
php artisan migrate

Step 5: Create Chat Event

1️⃣ Generate an Event

bash
php artisan make:event MessageSent

2️⃣ Edit app/Events/MessageSent.php

php
namespace App\Events; use App\Models\Chat; use Illuminate\Broadcasting\Channel; use Illuminate\Broadcasting\InteractsWithSockets; use Illuminate\Broadcasting\PresenceChannel; use Illuminate\Broadcasting\PrivateChannel; use Illuminate\Contracts\Broadcasting\ShouldBroadcast; use Illuminate\Contracts\Broadcasting\ShouldBroadcastNow; use Illuminate\Queue\SerializesModels; class MessageSent implements ShouldBroadcastNow { use InteractsWithSockets, SerializesModels; public $chat; public function __construct(Chat $chat) { $this->chat = $chat; } public function broadcastOn() { return new PrivateChannel('chat.' . $this->chat->receiver_id); } public function broadcastWith() { return [ 'message' => $this->chat->message, 'sender_id' => $this->chat->sender_id, 'receiver_id' => $this->chat->receiver_id, 'timestamp' => $this->chat->created_at->toDateTimeString(), ]; } }

💡 This event will broadcast new messages to the receiver’s private channel.


Step 6: Create Chat Controller

1️⃣ Generate a Chat Controller

bash
php artisan make:controller ChatController

2️⃣ Edit app/Http/Controllers/ChatController.php

php
namespace App\Http\Controllers; use App\Events\MessageSent; use App\Models\Chat; use Illuminate\Http\Request; use Illuminate\Support\Facades\Auth; class ChatController extends Controller { public function sendMessage(Request $request) { $request->validate([ 'receiver_id' => 'required|exists:users,id', 'message' => 'required|string', ]); $chat = Chat::create([ 'sender_id' => Auth::id(), 'receiver_id' => $request->receiver_id, 'message' => $request->message, ]); broadcast(new MessageSent($chat))->toOthers(); return response()->json(['message' => 'Sent successfully']); } }

💡 This method stores the message and broadcasts it to the receiver.


Step 7: Define API Route

Edit routes/api.php:

php
use App\Http\Controllers\ChatController; Route::middleware('auth:sanctum')->post('/send-message', [ChatController::class, 'sendMessage']);

💡 Ensure your API uses auth:sanctum or another authentication method.


Step 8: Start Reverb WebSocket Server

1️⃣ Run Laravel Queues (Redis required)

bash
php artisan queue:work

2️⃣ Start Laravel Reverb

bash
php artisan reverb:start

Step 9: Frontend Integration (Example in JavaScript)

Here’s how to listen for messages using Laravel Echo:

1️⃣ Install Laravel Echo & Pusher JS

bash
npm install laravel-echo pusher-js

2️⃣ Configure Laravel Echo in resources/js/bootstrap.js

js
import Echo from 'laravel-echo'; import Pusher from 'pusher-js'; window.Pusher = Pusher; window.Echo = new Echo({ broadcaster: 'pusher', key: 'reverb', wsHost: window.location.hostname, wsPort: 6001, wssPort: 6001, forceTLS: false, disableStats: true, enabledTransports: ['ws', 'wss'] });

3️⃣ Listen for messages (Example in Vue.js or React)

js
window.Echo.private(`chat.${userId}`) .listen('MessageSent', (e) => { console.log("New message received:", e.message); });

Step 10: Test Everything

1️⃣ Start Laravel Backend

bash
php artisan serve

2️⃣ Start Reverb

bash
php artisan reverb:start

3️⃣ Send a Message using Postman or Frontend

json
POST /api/send-message { "receiver_id": 2, "message": "Hello, how are you?" }

4️⃣ Check if the receiver gets the real-time message!


How to Scale Laravel Reverb for Multi-Server?

  1. Ensure Redis is used for broadcasting (BROADCAST_CONNECTION=redis).
  2. Deploy Laravel on multiple servers.
  3. Run php artisan reverb:start on each server.
  4. Use a Load Balancer (e.g., Nginx, AWS ALB) to route WebSocket traffic.
  5. Enable Redis clustering for large-scale chat applications.

Summary

✅ Laravel Reverb provides a native WebSocket solution for Laravel 11.
✅ Redis is used to broadcast events across multiple servers.
✅ Private Channels ensure secure messaging.
✅ Frontend listens for messages using Laravel Echo.
✅ For multi-server support, use Redis + Load Balancer.

Would you like a Docker setup for a production-ready chat app? 🚀


Xác thực Private Chat trong Laravel Reverb

Để đảm bảo chỉ người dùng hợp lệ mới có thể lắng nghe và nhận tin nhắn trong Private Chat, Laravel sử dụng Private Channels kết hợp với cơ chế xác thực.


1. Xác thực Private Channel

Trong routes/channels.php:

php
use Illuminate\Support\Facades\Broadcast; Broadcast::channel('chat.{receiverId}', function ($user, $receiverId) { return (int) $user->id === (int) $receiverId; });

🔹 Giải thích:

  • Khi một người dùng cố gắng lắng nghe kênh chat.{receiverId}, Laravel sẽ gọi callback xác thực.
  • Chỉ khi $user->id === $receiverId, Laravel mới cho phép người dùng lắng nghe kênh.
  • Điều này đảm bảo chỉ người nhận tin nhắn mới có thể nghe tin nhắn đến.

2. Xác thực Người Dùng Trước Khi Gửi Tin Nhắn

Trong ChatController.php, Laravel bắt buộc user phải đăng nhập để gửi tin nhắn:

php
public function sendMessage(Request $request) { $request->validate([ 'receiver_id' => 'required|exists:users,id', 'message' => 'required|string', ]); $chat = Chat::create([ 'sender_id' => Auth::id(), 'receiver_id' => $request->receiver_id, 'message' => $request->message, ]); broadcast(new MessageSent($chat))->toOthers(); return response()->json(['message' => 'Sent successfully']); }

🔹 Giải thích:

  • Auth::id() lấy ID của user hiện tại (bắt buộc phải login).
  • Chặn gửi tin nhắn đến user không tồn tại với exists:users,id.
  • Dữ liệu chỉ được lưu & phát đi nếu user hợp lệ.

3. Xác thực Khi Lắng Nghe Kênh Trên Frontend

Laravel Echo sẽ gửi request xác thực trước khi lắng nghe Private Channel.

Ví dụ, trong Vue.js/React/JavaScript:

js
window.Echo.private(`chat.${userId}`) .listen('MessageSent', (e) => { console.log("Tin nhắn mới:", e.message); });

📌 Lưu ý:

  • Laravel sẽ tự động chặn kết nối nếu user không được phép nghe kênh chat.{receiverId}.
  • Nếu user chưa đăng nhập, Laravel sẽ từ chối kết nối.

4. Laravel Xác Thực Private Channel Như Thế Nào?

Khi frontend yêu cầu lắng nghe một Private Channel, Laravel sẽ:

  1. Kiểm tra user đã đăng nhập hay chưa.
  2. Chạy callback trong routes/channels.php để xem user có quyền nghe không.
  3. Trả về lỗi nếu không hợp lệ, hoặc cho phép kết nối nếu hợp lệ.

📌 Tổng Kết

✅ Private Channel (chat.{receiverId}) đảm bảo chỉ người nhận mới được nghe tin nhắn.
✅ Laravel Echo gửi yêu cầu xác thực trước khi cho phép kết nối.
✅ Backend yêu cầu user phải đăng nhập khi gửi tin nhắn (Auth::id()).
✅ Nếu không xác thực, user không thể nhận tin nhắn private.

🔹 Với cách này, tin nhắn của user sẽ an toàn và không bị nghe lén. 🚀

What is Laravel Reverb?

Laravel Reverb is a built-in WebSocket server introduced in Laravel 11 that allows you to create real-time applications without needing third-party services like Pusher or Laravel WebSockets.

🔥 In short: Laravel Reverb makes it easy to implement WebSockets in Laravel without any extra setup or costs.


📌 Why use Laravel Reverb?

✅ No need for Pusher → Reduces costs, no third-party dependencies.
✅ Built into Laravel 11 → No need to install Laravel WebSockets.
✅ High performance → Optimized for Laravel applications.
✅ Easy to configure & deploy → Runs as a service within Laravel.


🔧 How to Use Laravel Reverb?

1️⃣ Enable Reverb in Laravel

Open your .env file and set the broadcast driver to reverb:

env
BROADCAST_DRIVER=reverb

Start the Reverb WebSocket server with:

sh
php artisan reverb:start

By default, Reverb runs on port 6001.


2️⃣ Create an Event for Broadcasting Messages

📌 Generate a new event:

sh
php artisan make:event MessageSent

📌 Edit app/Events/MessageSent.php:

php
use Illuminate\Broadcasting\Channel; use Illuminate\Broadcasting\InteractsWithSockets; use Illuminate\Broadcasting\SerializesModels; use Illuminate\Contracts\Broadcasting\ShouldBroadcast; class MessageSent implements ShouldBroadcast { use InteractsWithSockets, SerializesModels; public $message; public function __construct($message) { $this->message = $message; } public function broadcastOn() { return new Channel('chat'); } }

📌 Explanation:

  • This event broadcasts on the chat channel.
  • When a message is sent, all clients listening to this channel will receive the event in real-time.

3️⃣ Update the Controller to Send Messages

📌 Create ChatController:

sh
php artisan make:controller ChatController

📌 Modify app/Http/Controllers/ChatController.php:

php
use Illuminate\Http\Request; use App\Events\MessageSent; class ChatController extends Controller { public function sendMessage(Request $request) { $message = [ 'user' => auth()->user()->name, 'message' => $request->message, ]; broadcast(new MessageSent($message))->toOthers(); return response()->json(['message' => $message]); } }

📌 Explanation:

  • When a user sends a message, it broadcasts (broadcast()) the MessageSent event over WebSockets.

4️⃣ Add API Routes for Chat

📌 Open routes/api.php and add:

php
use App\Http\Controllers\ChatController; Route::middleware('auth:sanctum')->post('/send-message', [ChatController::class, 'sendMessage']);

📌 Now, the API will send messages in real-time!


5️⃣ Set Up Laravel Echo on Frontend

📌 Install Laravel Echo & Reverb Client:

sh
npm install --save laravel-echo @laravel/reverb

📌 Open resources/js/bootstrap.js and update:

js
import Echo from 'laravel-echo'; import Reverb from '@laravel/reverb'; window.Echo = new Echo({ broadcaster: Reverb, host: 'http://localhost:6001', }); window.Echo.channel('chat') .listen('MessageSent', (event) => { console.log("New message:", event.message); });

📌 Explanation:

  • window.Echo.channel('chat') → Listens for the MessageSent event on the chat channel.
  • When a new message is received, it will log the message to the console.

📌 Rebuild the frontend assets:

sh
npm run dev

6️⃣ Start WebSocket and Test Chat

📌 Run Laravel server:

sh
php artisan serve

📌 Start Laravel Reverb WebSocket:

sh
php artisan reverb:start

📌 Send a message via API:

sh
curl -X POST "http://127.0.0.1:8000/api/send-message" -H "Authorization: Bearer YOUR_ACCESS_TOKEN" -H "Content-Type: application/json" -d '{"message":"Hello, world!"}'

📌 If successful, the frontend console should display the message in real-time! 🎉


🎯 Summary

✅ Laravel Reverb allows you to implement WebSockets without needing Pusher.
✅ Simple setup—just start with php artisan reverb:start.
✅ Works seamlessly with Laravel Echo for real-time events.
✅ Integrates easily with Vue.js, React, or Blade templates.

👉 Now you have a real-time chat system in Laravel 11 without using any third-party services! 🚀


Integrating Laravel Reverb with Vue 3 + TypeScript for Real-time Chat

Since you're using Vue 3 with TypeScript, you'll need to set up Laravel Echo with Reverb in Vue properly.


🛠 1. Install Dependencies

In your Vue 3 project, install Laravel Echo and Reverb client:

sh
npm install --save laravel-echo @laravel/reverb

📌 2. Set Up Laravel Echo in Vue 3 (TypeScript)

Create a new file src/utils/reverb.ts to handle WebSocket connections:

ts
import Echo from 'laravel-echo'; import Reverb from '@laravel/reverb'; declare global { interface Window { Echo: Echo; } } // Initialize Laravel Echo with Reverb window.Echo = new Echo({ broadcaster: Reverb, host: 'http://localhost:6001', // Ensure this matches your Laravel Reverb server }); export default window.Echo;

📌 3. Implement Chat Component in Vue 3

Create a new component src/components/Chat.vue:

vue
<script setup lang="ts"> import { ref, onMounted, onUnmounted } from 'vue'; import Echo from '@/utils/reverb'; import axios from 'axios'; interface Message { user: string; message: string; } const messages = ref<Message[]>([]); const newMessage = ref(''); const fetchMessages = async () => { try { const response = await axios.get<Message[]>('http://127.0.0.1:8000/api/messages', { headers: { Authorization: `Bearer YOUR_ACCESS_TOKEN` }, }); messages.value = response.data; } catch (error) { console.error('Error fetching messages:', error); } }; const sendMessage = async () => { if (!newMessage.value.trim()) return; try { await axios.post( 'http://127.0.0.1:8000/api/send-message', { message: newMessage.value }, { headers: { Authorization: `Bearer YOUR_ACCESS_TOKEN` } } ); newMessage.value = ''; // Clear input after sending } catch (error) { console.error('Error sending message:', error); } }; onMounted(() => { fetchMessages(); Echo.channel('chat') .listen('MessageSent', (event: { message: Message }) => { messages.value.push(event.message); }); }); onUnmounted(() => { Echo.leave('chat'); // Cleanup WebSocket connection }); </script> <template> <div class="chat-container"> <div class="messages"> <div v-for="(msg, index) in messages" :key="index" class="message"> <strong>{{ msg.user }}</strong>: {{ msg.message }} </div> </div> <div class="input-box"> <input v-model="newMessage" placeholder="Type a message..." @keyup.enter="sendMessage" /> <button @click="sendMessage">Send</button> </div> </div> </template> <style scoped> .chat-container { width: 400px; border: 1px solid #ddd; padding: 10px; border-radius: 5px; } .messages { max-height: 300px; overflow-y: auto; margin-bottom: 10px; } .message { padding: 5px; border-bottom: 1px solid #ddd; } .input-box { display: flex; gap: 10px; } input { flex: 1; padding: 5px; } button { padding: 5px 10px; cursor: pointer; } </style>

📡 4. Update Laravel Backend

Make sure you have these APIs in Laravel 11:

📌 Modify routes/api.php:

php
use App\Http\Controllers\ChatController; Route::middleware('auth:sanctum')->group(function () { Route::post('/send-message', [ChatController::class, 'sendMessage']); Route::get('/messages', [ChatController::class, 'getMessages']); });

📌 Modify app/Http/Controllers/ChatController.php:

php
use Illuminate\Http\Request; use App\Models\Message; use App\Events\MessageSent; class ChatController extends Controller { public function sendMessage(Request $request) { $message = Message::create([ 'user_id' => auth()->id(), 'message' => $request->message, ]); broadcast(new MessageSent($message))->toOthers(); return response()->json(['message' => $message]); } public function getMessages() { return Message::with('user')->latest()->take(50)->get(); } }

📌 Modify app/Events/MessageSent.php:

php
use Illuminate\Broadcasting\Channel; use Illuminate\Broadcasting\InteractsWithSockets; use Illuminate\Broadcasting\SerializesModels; use Illuminate\Contracts\Broadcasting\ShouldBroadcast; use App\Models\Message; class MessageSent implements ShouldBroadcast { use InteractsWithSockets, SerializesModels; public $message; public function __construct(Message $message) { $this->message = $message; } public function broadcastOn() { return new Channel('chat'); } }

🔥 5. Start the Application

Run Laravel server:

sh
php artisan serve

Start Laravel Reverb WebSocket:

sh
php artisan reverb:start

Run Vue frontend:

sh
npm run dev

🎯 Summary

✅ Laravel 11 Backend

  • Uses Laravel Reverb instead of Pusher.
  • API for sending & fetching messages.
  • Broadcasts messages using Laravel Echo.

✅ Vue 3 + TypeScript Frontend

  • Uses Laravel Echo with Reverb for real-time messaging.
  • Listens for events and updates the chat UI.
  • API calls for sending & receiving messages.

🚀 Now you have a real-time chat system with Laravel Reverb + Vue 3 + TypeScript! 🎉

Thank you

Publish npm package

  Để publish   pav-kit  lên NPM, bạn hãy làm theo các bước dưới đây. Tôi đã tạo thêm file  index.js  để đảm bảo gói tin hợp lệ. Bước 1: Tạo ...