Building Real-time Features with Mifty
Learn how to add real-time functionality to your Mifty applications using WebSockets, including live chat, notifications, collaborative editing, and real-time dashboards.
What You'll Build
A comprehensive real-time system featuring:
- 💬 Live Chat System - Real-time messaging with typing indicators
- 🔔 Push Notifications - Instant notifications for events
- 📊 Live Dashboard - Real-time metrics and data updates
- 👥 Collaborative Editing - Multiple users editing simultaneously
- 🎮 Presence System - Online/offline user status
- 📈 Live Analytics - Real-time charts and statistics
- 🔄 Data Synchronization - Automatic data sync across clients
- 🎯 Event Broadcasting - Custom event system
- 🛡️ Authentication - Secure WebSocket connections
- 📱 Mobile Support - Real-time features for mobile apps
Prerequisites
- Completed Blog API Tutorial or equivalent Mifty experience
- Basic understanding of WebSockets
- 20 minutes of your time
Step 1: Project Setup
# Create new real-time project
mifty init realtime-app
cd realtime-app
# Install WebSocket dependencies
npm install socket.io @types/socket.io
npm install socket.io-client # For client-side testing
# Install Redis for scaling (optional)
npm run adapter install redis
# Start development
npm run dev:full
Step 2: Design Real-time Database Schema
Open the Database Designer at http://localhost:3001/ui and create these tables:
2.1 User Table (Extended for Real-time)
| Column | Type | Constraints | Default |
|---|---|---|---|
id | String | Primary Key, Required | cuid() |
email | String | Required, Unique | - |
username | String | Required, Unique | - |
firstName | String | Required | - |
lastName | String | Required | - |
avatar | String | Optional | - |
status | Enum | Required | "OFFLINE" |
lastSeen | DateTime | Optional | - |
isOnline | Boolean | Required | false |
socketId | String | Optional | - |
createdAt | DateTime | Required | now() |
updatedAt | DateTime | Required, Updated | now() |
User status enum values:
ONLINEAWAYBUSYOFFLINE
2.2 Room Table (Chat Rooms/Channels)
| Column | Type | Constraints | Default |
|---|---|---|---|
id | String | Primary Key, Required | cuid() |
name | String | Required | - |
description | String | Optional | - |
type | Enum | Required | "PUBLIC" |
ownerId | String | Required | - |
isActive | Boolean | Required | true |
maxMembers | Int | Optional | - |
createdAt | DateTime | Required | now() |
updatedAt | DateTime | Required, Updated | now() |
Room type enum values:
PUBLICPRIVATEDIRECT
2.3 RoomMember Table
| Column | Type | Constraints | Default |
|---|---|---|---|
id | String | Primary Key, Required | cuid() |
roomId | String | Required | - |
userId | String | Required | - |
role | Enum | Required | "MEMBER" |
joinedAt | DateTime | Required | now() |
lastReadAt | DateTime | Optional | - |
Member role enum values:
OWNERADMINMODERATORMEMBER
2.4 Message Table
| Column | Type | Constraints | Default |
|---|---|---|---|
id | String | Primary Key, Required | cuid() |
content | String | Required | - |
type | Enum | Required | "TEXT" |
senderId | String | Required | - |
roomId | String | Required | - |
replyToId | String | Optional | - |
editedAt | DateTime | Optional | - |
deletedAt | DateTime | Optional | - |
metadata | Json | Optional | {} |
createdAt | DateTime | Required | now() |
Message type enum values:
TEXTIMAGEFILESYSTEMTYPING
2.5 Notification Table
| Column | Type | Constraints | Default |
|---|---|---|---|
id | String | Primary Key, Required | cuid() |
userId | String | Required | - |
title | String | Required | - |
message | String | Required | - |
type | Enum | Required | "INFO" |
data | Json | Optional | {} |
isRead | Boolean | Required | false |
readAt | DateTime | Optional | - |
createdAt | DateTime | Required | now() |
Notification type enum values:
INFOSUCCESSWARNINGERRORMESSAGEMENTION
2.6 Document Table (For Collaborative Editing)
| Column | Type | Constraints | Default |
|---|---|---|---|
id | String | Primary Key, Required | cuid() |
title | String | Required | - |
content | String | Required | "" |
ownerId | String | Required | - |
isPublic | Boolean | Required | false |
version | Int | Required | 1 |
lastEditedBy | String | Optional | - |
createdAt | DateTime | Required | now() |
updatedAt | DateTime | Required, Updated | now() |
2.7 DocumentEdit Table (Edit History)
| Column | Type | Constraints | Default |
|---|---|---|---|
id | String | Primary Key, Required | cuid() |
documentId | String | Required | - |
userId | String | Required | - |
operation | Json | Required | - |
version | Int | Required | - |
createdAt | DateTime | Required | now() |
2.8 Create Relationships
Set up these relationships:
- User → Room (Many-to-Many through RoomMember)
- User → Message (One-to-Many)
- User → Notification (One-to-Many)
- User → Document (One-to-Many)
- User → DocumentEdit (One-to-Many)
- Room → RoomMember (One-to-Many)
- Room → Message (One-to-Many)
- Message → Message (Self-referencing for replies)
- Document → DocumentEdit (One-to-Many)
Step 3: Generate Real-time Modules
# Generate all modules
npm run generate
# This creates modules for:
# - User, Room, RoomMember, Message
# - Notification, Document, DocumentEdit
Step 4: Set Up WebSocket Server
4.1 Create WebSocket Service
Create src/services/websocket.service.ts:
import { Injectable } from '@nestjs/common';
import { Server, Socket } from 'socket.io';
import { JwtService } from '@nestjs/jwt';
import { UserService } from '../modules/user/user.service';
@Injectable()
export class WebSocketService {
private io: Server;
private connectedUsers = new Map<string, Socket>();
constructor(
private jwtService: JwtService,
private userService: UserService
) {}
setServer(server: Server) {
this.io = server;
this.setupEventHandlers();
}
private setupEventHandlers() {
this.io.use(async (socket, next) => {
try {
const token = socket.handshake.auth.token;
const payload = this.jwtService.verify(token);
const user = await this.userService.findById(payload.sub);
if (!user) throw new Error('User not found');
socket.data.user = user;
next();
} catch (error) {
next(new Error('Authentication failed'));
}
});
this.io.on('connection', (socket) => {
this.handleConnection(socket);
});
}
private async handleConnection(socket: Socket) {
const user = socket.data.user;
// Store connection
this.connectedUsers.set(user.id, socket);
// Update user status
await this.userService.update(user.id, {
isOnline: true,
status: 'ONLINE',
socketId: socket.id
});
// Join user to their rooms
const userRooms = await this.getUserRooms(user.id);
userRooms.forEach(room => {
socket.join(`room:${room.id}`);
});
// Broadcast user online status
this.broadcastUserStatus(user.id, 'ONLINE');
// Handle events
this.setupSocketEvents(socket);
// Handle disconnect
socket.on('disconnect', () => {
this.handleDisconnect(socket);
});
}
private setupSocketEvents(socket: Socket) {
const user = socket.data.user;
// Chat events
socket.on('message:send', (data) => {
this.handleSendMessage(socket, data);
});
socket.on('message:typing', (data) => {
this.handleTyping(socket, data);
});
// Room events
socket.on('room:join', (roomId) => {
this.handleJoinRoom(socket, roomId);
});
socket.on('room:leave', (roomId) => {
this.handleLeaveRoom(socket, roomId);
});
// Document collaboration events
socket.on('document:edit', (data) => {
this.handleDocumentEdit(socket, data);
});
socket.on('document:cursor', (data) => {
this.handleCursorUpdate(socket, data);
});
// Presence events
socket.on('status:update', (status) => {
this.handleStatusUpdate(socket, status);
});
}
private async handleDisconnect(socket: Socket) {
const user = socket.data.user;
// Remove connection
this.connectedUsers.delete(user.id);
// Update user status
await this.userService.update(user.id, {
isOnline: false,
status: 'OFFLINE',
lastSeen: new Date(),
socketId: null
});
// Broadcast user offline status
this.broadcastUserStatus(user.id, 'OFFLINE');
}
// Event handlers
private async handleSendMessage(socket: Socket, data: any) {
const user = socket.data.user;
const { roomId, content, type = 'TEXT', replyToId } = data;
try {
// Create message in database
const message = await this.messageService.create({
content,
type,
senderId: user.id,
roomId,
replyToId
});
// Broadcast to room
this.io.to(`room:${roomId}`).emit('message:new', {
...message,
sender: {
id: user.id,
username: user.username,
avatar: user.avatar
}
});
// Send notifications to offline users
await this.sendMessageNotifications(roomId, message, user);
} catch (error) {
socket.emit('error', { message: 'Failed to send message' });
}
}
private handleTyping(socket: Socket, data: any) {
const user = socket.data.user;
const { roomId, isTyping } = data;
socket.to(`room:${roomId}`).emit('user:typing', {
userId: user.id,
username: user.username,
isTyping
});
}
private async handleDocumentEdit(socket: Socket, data: any) {
const user = socket.data.user;
const { documentId, operation, version } = data;
try {
// Apply operation to document
const updatedDocument = await this.documentService.applyOperation(
documentId,
operation,
version,
user.id
);
// Broadcast to all document collaborators
socket.to(`document:${documentId}`).emit('document:operation', {
operation,
version: updatedDocument.version,
userId: user.id
});
} catch (error) {
socket.emit('document:conflict', {
message: 'Document conflict detected',
currentVersion: error.currentVersion
});
}
}
// Utility methods
public broadcastToRoom(roomId: string, event: string, data: any) {
this.io.to(`room:${roomId}`).emit(event, data);
}
public sendToUser(userId: string, event: string, data: any) {
const socket = this.connectedUsers.get(userId);
if (socket) {
socket.emit(event, data);
}
}
public broadcastUserStatus(userId: string, status: string) {
this.io.emit('user:status', { userId, status });
}
public getConnectedUsers(): string[] {
return Array.from(this.connectedUsers.keys());
}
}
4.2 Initialize WebSocket in Main App
Update src/app.ts:
import { createServer } from 'http';
import { Server } from 'socket.io';
import { WebSocketService } from './services/websocket.service';
// After creating Express app
const server = createServer(app);
const io = new Server(server, {
cors: {
origin: process.env.CLIENT_URL || "http://localhost:3000",
methods: ["GET", "POST"]
}
});
// Initialize WebSocket service
const webSocketService = container.resolve(WebSocketService);
webSocketService.setServer(io);
// Start server
const PORT = process.env.PORT || 3000;
server.listen(PORT, () => {
console.log(`🚀 Server running on port ${PORT}`);
console.log(`📡 WebSocket server ready`);
});
Step 5: Implement Real-time Chat
5.1 Enhanced Message Service
Update src/modules/message/message.service.ts:
// Add these methods to MessageService
async getRecentMessages(roomId: string, limit = 50) {
return this.messageRepository.findMany({
where: {
roomId,
deletedAt: null
},
include: {
sender: {
select: { id: true, username: true, avatar: true }
},
replyTo: {
include: {
sender: {
select: { username: true }
}
}
}
},
orderBy: { createdAt: 'desc' },
take: limit
});
}
async markAsRead(roomId: string, userId: string) {
// Update last read timestamp for user in room
await this.roomMemberRepository.updateMany({
where: { roomId, userId },
data: { lastReadAt: new Date() }
});
}
async getUnreadCount(roomId: string, userId: string) {
const member = await this.roomMemberRepository.findFirst({
where: { roomId, userId }
});
if (!member) return 0;
return this.messageRepository.count({
where: {
roomId,
createdAt: { gt: member.lastReadAt || member.joinedAt },
senderId: { not: userId }
}
});
}
5.2 Create Chat Controller
Create src/modules/chat/chat.controller.ts:
import { Controller, Get, Post, Body, Param, Query, UseGuards } from '@nestjs/common';
import { JwtAuthGuard } from '../auth/jwt-auth.guard';
import { CurrentUser } from '../auth/current-user.decorator';
import { ChatService } from './chat.service';
@Controller('api/v1/chat')
@UseGuards(JwtAuthGuard)
export class ChatController {
constructor(private chatService: ChatService) {}
@Get('rooms')
async getUserRooms(@CurrentUser() user: any) {
return this.chatService.getUserRooms(user.id);
}
@Post('rooms')
async createRoom(@CurrentUser() user: any, @Body() data: any) {
return this.chatService.createRoom(user.id, data);
}
@Get('rooms/:roomId/messages')
async getRoomMessages(
@Param('roomId') roomId: string,
@Query('limit') limit?: number,
@Query('before') before?: string
) {
return this.chatService.getRoomMessages(roomId, { limit, before });
}
@Post('rooms/:roomId/join')
async joinRoom(@CurrentUser() user: any, @Param('roomId') roomId: string) {
return this.chatService.joinRoom(roomId, user.id);
}
@Post('rooms/:roomId/read')
async markAsRead(@CurrentUser() user: any, @Param('roomId') roomId: string) {
return this.chatService.markAsRead(roomId, user.id);
}
}
Step 6: Add Live Dashboard
6.1 Create Analytics Service
Create src/modules/analytics/realtime-analytics.service.ts:
import { Injectable } from '@nestjs/common';
import { WebSocketService } from '../../services/websocket.service';
import { Cron, CronExpression } from '@nestjs/schedule';
@Injectable()
export class RealtimeAnalyticsService {
constructor(private webSocketService: WebSocketService) {}
@Cron(CronExpression.EVERY_10_SECONDS)
async broadcastLiveMetrics() {
const metrics = await this.getLiveMetrics();
this.webSocketService.io.to('dashboard').emit('metrics:update', metrics);
}
private async getLiveMetrics() {
const connectedUsers = this.webSocketService.getConnectedUsers().length;
const activeRooms = await this.getActiveRoomsCount();
const messagesLastHour = await this.getMessagesLastHour();
const systemLoad = await this.getSystemMetrics();
return {
connectedUsers,
activeRooms,
messagesLastHour,
systemLoad,
timestamp: new Date()
};
}
async getActiveRoomsCount() {
// Count rooms with recent activity
const oneHourAgo = new Date(Date.now() - 60 * 60 * 1000);
return this.messageRepository.count({
where: {
createdAt: { gte: oneHourAgo }
},
distinct: ['roomId']
});
}
async getMessagesLastHour() {
const oneHourAgo = new Date(Date.now() - 60 * 60 * 1000);
return this.messageRepository.count({
where: {
createdAt: { gte: oneHourAgo }
}
});
}
async getSystemMetrics() {
const memoryUsage = process.memoryUsage();
const cpuUsage = process.cpuUsage();
return {
memory: {
used: memoryUsage.heapUsed,
total: memoryUsage.heapTotal,
percentage: (memoryUsage.heapUsed / memoryUsage.heapTotal) * 100
},
cpu: {
user: cpuUsage.user,
system: cpuUsage.system
},
uptime: process.uptime()
};
}
}
6.2 Dashboard WebSocket Events
Add dashboard-specific events to your WebSocket service:
// Add to WebSocketService
private setupDashboardEvents(socket: Socket) {
socket.on('dashboard:subscribe', () => {
socket.join('dashboard');
// Send initial metrics
this.sendDashboardMetrics(socket);
});
socket.on('dashboard:unsubscribe', () => {
socket.leave('dashboard');
});
}
private async sendDashboardMetrics(socket: Socket) {
const metrics = await this.realtimeAnalyticsService.getLiveMetrics();
socket.emit('metrics:initial', metrics);
}
Step 7: Implement Collaborative Editing
7.1 Document Service with Operational Transform
Create src/modules/document/document-collaboration.service.ts:
import { Injectable } from '@nestjs/common';
import { DocumentService } from './document.service';
import { WebSocketService } from '../../services/websocket.service';
@Injectable()
export class DocumentCollaborationService {
constructor(
private documentService: DocumentService,
private webSocketService: WebSocketService
) {}
async applyOperation(documentId: string, operation: any, version: number, userId: string) {
const document = await this.documentService.findById(documentId);
if (!document) {
throw new Error('Document not found');
}
if (document.version !== version) {
// Version conflict - need to transform operation
const transformedOperation = await this.transformOperation(
documentId,
operation,
version,
document.version
);
operation = transformedOperation;
}
// Apply operation to document content
const newContent = this.applyOperationToContent(document.content, operation);
const newVersion = document.version + 1;
// Update document
const updatedDocument = await this.documentService.update(documentId, {
content: newContent,
version: newVersion,
lastEditedBy: userId
});
// Save operation to history
await this.documentEditService.create({
documentId,
userId,
operation,
version: newVersion
});
return updatedDocument;
}
private async transformOperation(documentId: string, operation: any, fromVersion: number, toVersion: number) {
// Get all operations between versions
const operations = await this.documentEditService.findMany({
where: {
documentId,
version: { gt: fromVersion, lte: toVersion }
},
orderBy: { version: 'asc' }
});
// Apply operational transform
let transformedOp = operation;
for (const op of operations) {
transformedOp = this.transform(transformedOp, op.operation);
}
return transformedOp;
}
private transform(op1: any, op2: any) {
// Simplified operational transform
// In production, use a library like ShareJS or Yjs
if (op1.type === 'insert' && op2.type === 'insert') {
if (op1.position <= op2.position) {
return op1;
} else {
return {
...op1,
position: op1.position + op2.text.length
};
}
}
if (op1.type === 'delete' && op2.type === 'insert') {
if (op1.position < op2.position) {
return op1;
} else {
return {
...op1,
position: op1.position + op2.text.length
};
}
}
// Add more transformation rules as needed
return op1;
}
private applyOperationToContent(content: string, operation: any) {
switch (operation.type) {
case 'insert':
return content.slice(0, operation.position) +
operation.text +
content.slice(operation.position);
case 'delete':
return content.slice(0, operation.position) +
content.slice(operation.position + operation.length);
case 'retain':
return content;
default:
throw new Error(`Unknown operation type: ${operation.type}`);
}
}
}
Step 8: Add Push Notifications
8.1 Notification Service
Create src/modules/notification/notification.service.ts:
import { Injectable } from '@nestjs/common';
import { WebSocketService } from '../../services/websocket.service';
import { EmailService } from '../email/email.service';
@Injectable()
export class NotificationService {
constructor(
private webSocketService: WebSocketService,
private emailService: EmailService,
private notificationRepository: NotificationRepository
) {}
async sendNotification(userId: string, notification: CreateNotificationDto) {
// Save to database
const savedNotification = await this.notificationRepository.create({
userId,
...notification
});
// Send real-time notification if user is online
this.webSocketService.sendToUser(userId, 'notification:new', savedNotification);
// Send email for important notifications if user is offline
const user = await this.userService.findById(userId);
if (!user.isOnline && notification.type === 'IMPORTANT') {
await this.emailService.sendNotificationEmail(user.email, notification);
}
return savedNotification;
}
async sendBulkNotification(userIds: string[], notification: CreateNotificationDto) {
const notifications = await Promise.all(
userIds.map(userId => this.sendNotification(userId, notification))
);
return notifications;
}
async markAsRead(notificationId: string, userId: string) {
const notification = await this.notificationRepository.update(notificationId, {
isRead: true,
readAt: new Date()
});
// Notify client of read status change
this.webSocketService.sendToUser(userId, 'notification:read', {
notificationId,
isRead: true
});
return notification;
}
async getUnreadCount(userId: string) {
return this.notificationRepository.count({
where: { userId, isRead: false }
});
}
}
Step 9: Test Real-time Features
9.1 Test Chat System
# Create a chat room
curl -X POST http://localhost:3000/api/v1/chat/rooms \
-H "Authorization: Bearer YOUR_JWT_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "General Chat",
"description": "General discussion room",
"type": "PUBLIC"
}'
# Join the room
curl -X POST http://localhost:3000/api/v1/chat/rooms/ROOM_ID/join \
-H "Authorization: Bearer YOUR_JWT_TOKEN"
# Get room messages
curl http://localhost:3000/api/v1/chat/rooms/ROOM_ID/messages \
-H "Authorization: Bearer YOUR_JWT_TOKEN"
9.2 Test WebSocket Connection
Create a simple client test file test-websocket.js:
const io = require('socket.io-client');
const socket = io('http://localhost:3000', {
auth: {
token: 'YOUR_JWT_TOKEN_HERE'
}
});
socket.on('connect', () => {
console.log('✅ Connected to WebSocket server');
// Test sending a message
socket.emit('message:send', {
roomId: 'ROOM_ID_HERE',
content: 'Hello from WebSocket client!',
type: 'TEXT'
});
});
socket.on('message:new', (message) => {
console.log('📨 New message:', message);
});
socket.on('user:typing', (data) => {
console.log('⌨️ User typing:', data);
});
socket.on('notification:new', (notification) => {
console.log('🔔 New notification:', notification);
});
socket.on('disconnect', () => {
console.log('❌ Disconnected from server');
});
# Run the WebSocket test
node test-websocket.js
9.3 Test Document Collaboration
# Create a document
curl -X POST http://localhost:3000/api/v1/documents \
-H "Authorization: Bearer YOUR_JWT_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"title": "Collaborative Document",
"content": "Initial content",
"isPublic": true
}'
# Test document editing via WebSocket
# (Use the WebSocket client to emit document:edit events)
Step 10: Performance Optimization
10.1 Add Redis for Scaling
# Configure Redis adapter for WebSocket scaling
npm install @socket.io/redis-adapter redis
Update WebSocket service for Redis:
import { createAdapter } from '@socket.io/redis-adapter';
import { createClient } from 'redis';
// In WebSocketService.setServer()
if (process.env.REDIS_URL) {
const pubClient = createClient({ url: process.env.REDIS_URL });
const subClient = pubClient.duplicate();
await Promise.all([pubClient.connect(), subClient.connect()]);
this.io.adapter(createAdapter(pubClient, subClient));
console.log('📡 WebSocket Redis adapter configured');
}
10.2 Add Rate Limiting
// Add to WebSocket service
private rateLimitMap = new Map<string, number[]>();
private checkRateLimit(userId: string, maxRequests = 10, windowMs = 60000): boolean {
const now = Date.now();
const userRequests = this.rateLimitMap.get(userId) || [];
// Remove old requests outside the window
const validRequests = userRequests.filter(time => now - time < windowMs);
if (validRequests.length >= maxRequests) {
return false; // Rate limit exceeded
}
validRequests.push(now);
this.rateLimitMap.set(userId, validRequests);
return true;
}
// Use in event handlers
private async handleSendMessage(socket: Socket, data: any) {
const user = socket.data.user;
if (!this.checkRateLimit(user.id)) {
socket.emit('error', { message: 'Rate limit exceeded' });
return;
}
// ... rest of message handling
}
Step 11: Deploy Real-time Features
11.1 Production Configuration
Update your production environment:
# WebSocket Configuration
WEBSOCKET_CORS_ORIGIN=https://your-frontend-domain.com
WEBSOCKET_TRANSPORTS=websocket,polling
# Redis for scaling
REDIS_URL=redis://your-redis-server:6379
# Rate limiting
RATE_LIMIT_WINDOW=60000
RATE_LIMIT_MAX_REQUESTS=100
11.2 Load Balancer Configuration
For multiple server instances, configure sticky sessions:
# nginx.conf
upstream websocket_backend {
ip_hash; # Enable sticky sessions
server app1:3000;
server app2:3000;
server app3:3000;
}
server {
location /socket.io/ {
proxy_pass http://websocket_backend;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_cache_bypass $http_upgrade;
}
}
What You've Accomplished
🎉 Outstanding work! You've built a comprehensive real-time system with:
- ✅ WebSocket Server with authentication and room management
- ✅ Live Chat System with typing indicators and message history
- ✅ Real-time Notifications with push delivery and email fallback
- ✅ Collaborative Editing with operational transform and conflict resolution
- ✅ Presence System with online/offline status and user activity
- ✅ Live Dashboard with real-time metrics and system monitoring
- ✅ Performance Optimization with Redis scaling and rate limiting
- ✅ Production Deployment with load balancer and sticky sessions
- ✅ Mobile Support with Socket.IO client compatibility
- ✅ Security Features with JWT authentication and rate limiting
Performance Metrics
Traditional Real-time Development: ~120 hours With Mifty: ~6 hours Time Saved: 114 hours (95% faster!)
Next Steps
Enhance your real-time system with:
- 🎮 Gaming Features - Real-time multiplayer game mechanics
- 🎥 Video Chat - WebRTC integration for video calls
- 📍 Location Tracking - Real-time GPS and geofencing
- 🔄 Offline Sync - Progressive Web App with offline capabilities
- 📊 Advanced Analytics - Real-time user behavior tracking
- 🤖 AI Integration - Real-time AI responses and chatbots
- 🌐 Multi-language - Real-time translation and localization
- 📱 Push Notifications - Mobile push notifications with FCM
Ready to build more? Check out our File Upload Service Tutorial or explore advanced Testing and Debugging Guides!