Client Area

Real-time Sync and Push Notifications for Mobile Apps on DomainIndia VPS

ByDomain India Team·DomainIndia Engineering
6 min read24 Apr 20263 views
# Real-time Sync and Push Notifications for Mobile Apps on DomainIndia VPS
TL;DR
Mobile apps need three real-time patterns: WebSocket for in-app live updates (chat, presence), Server-Sent Events or long-polling for server-pushed state, and FCM/APNs for out-of-app notifications. This guide covers all three on a DomainIndia VPS with a Node.js/Python backend.
## Three kinds of real-time for mobile
TransportApp stateBest forBattery cost
WebSocketForegroundChat, presence, live cursorsHigh (connection open)
Server-Sent Events (SSE)ForegroundOne-way server → client, simpleMedium
FCM / APNs pushBackground/killedNotifications, silent triggersNone (OS-level)
A typical app uses all three: - WebSocket while user is actively in the chat screen - FCM push when user closes the app ## Part 1 — WebSocket for in-app real-time ### Server (Node.js with `ws`) ```javascript import { WebSocketServer } from 'ws'; import jwt from 'jsonwebtoken'; const wss = new WebSocketServer({ port: 4001 }); // Auth on connect wss.on('connection', (ws, req) => { const url = new URL(req.url, 'http://x'); const token = url.searchParams.get('token'); try { const payload = jwt.verify(token, process.env.JWT_SECRET); ws.userId = payload.sub; } catch { ws.close(4001, 'Unauthorized'); return; } console.log(`User ${ws.userId} connected`); ws.on('message', async (data) => { const msg = JSON.parse(data); if (msg.type === 'chat:send') { await saveMessage(ws.userId, msg.roomId, msg.text); broadcast(msg.roomId, { type: 'chat:new', roomId: msg.roomId, author: ws.userId, text: msg.text, timestamp: Date.now(), }); } }); ws.on('close', () => console.log(`User ${ws.userId} disconnected`)); }); function broadcast(roomId, payload) { wss.clients.forEach((client) => { if (client.readyState === 1 && client.rooms?.has(roomId)) { client.send(JSON.stringify(payload)); } }); } ``` Front behind nginx with WS proxy (see our [GraphQL Subscriptions article](https://domainindia.com/support/kb/graphql-subscriptions-websockets-vps)). ### Client (React Native / Flutter) **React Native:** ```javascript const ws = new WebSocket(`wss://api.yourcompany.com/ws?token=${authToken}`); ws.onopen = () => console.log('Connected'); ws.onmessage = (e) => { const msg = JSON.parse(e.data); if (msg.type === 'chat:new') updateChatUI(msg); }; ws.onerror = (err) => console.warn('WS error', err); ws.onclose = () => scheduleReconnect(); ``` **Flutter:** ```dart import 'package:web_socket_channel/web_socket_channel.dart'; final channel = WebSocketChannel.connect( Uri.parse('wss://api.yourcompany.com/ws?token=$authToken'), ); channel.stream.listen((data) { final msg = jsonDecode(data); if (msg['type'] == 'chat:new') updateUI(msg); }); ``` ## Part 2 — Reconnection & offline buffer Mobile networks drop constantly. Your client code must handle it. ```javascript class ResilientWS { constructor(url) { this.url = url; this.retryMs = 1000; this.buffer = []; this.connect(); } connect() { this.ws = new WebSocket(this.url); this.ws.onopen = () => { this.retryMs = 1000; this.buffer.forEach(m => this.ws.send(m)); this.buffer = []; }; this.ws.onclose = () => { setTimeout(() => this.connect(), this.retryMs); this.retryMs = Math.min(this.retryMs * 1.5, 30000); // backoff to 30s max }; } send(data) { const msg = JSON.stringify(data); if (this.ws.readyState === 1) this.ws.send(msg); else this.buffer.push(msg); // queue until reconnect } } ``` ## Part 3 — Push notifications (FCM covers iOS + Android) ### Setup
1
Create Firebase project at console.firebase.google.com
2
Add iOS + Android apps to the project
3
Download google-services.json (Android) and GoogleService-Info.plist (iOS)
4
In iOS: Apple Developer account → Certificates → Create APNs Auth Key → upload to Firebase
5
Install Firebase SDK in your app
6
On app launch, request notification permission and get FCM token
7
Send token to your backend, store in user_devices table
### Backend (Node.js) ```javascript import admin from 'firebase-admin'; import serviceAccount from './firebase-service-account.json' assert { type: 'json' }; admin.initializeApp({ credential: admin.credential.cert(serviceAccount), }); async function sendToUser(userId, { title, body, data }) { const devices = await db.device.findMany({ where: { userId } }); if (devices.length === 0) return; const messages = devices.map(d => ({ token: d.fcmToken, notification: { title, body }, data, // custom key-value map android: { priority: 'high', notification: { channelId: 'default' }, }, apns: { headers: { 'apns-priority': '10' }, payload: { aps: { sound: 'default', badge: 1 } }, }, })); const batch = await admin.messaging().sendEach(messages); // Clean up invalid tokens batch.responses.forEach((resp, i) => { if (!resp.success && resp.error.code === 'messaging/registration-token-not-registered') { db.device.delete({ where: { id: devices[i].id } }); } }); } // Usage: await sendToUser(user.id, { title: 'New order received', body: 'Order #1234 from Rajesh', data: { orderId: '1234', type: 'new_order' }, }); ``` ### Silent push (background trigger) Android / iOS allow "silent" data-only messages that trigger background code without showing a notification. Useful for "refresh feed now" or "mark read". ```javascript { token: deviceToken, data: { sync: 'true', type: 'refresh_feed' }, android: { priority: 'high' }, apns: { headers: { 'apns-push-type': 'background', 'apns-priority': '5' }, payload: { aps: { 'content-available': 1 } }, }, } ``` Apple throttles silent push aggressively — expect 2–3 per hour max, often less. ## Part 4 — Offline sync pattern For "read offline, write online" apps (note-taking, to-do): **Client side (SQLite local DB):** - Every action writes to local DB first (optimistic) - Background worker tries to sync to server - Conflict resolution: last-write-wins or CRDT **Server side:** ```javascript // Pull changes since client's last sync app.get('/sync/pull', async (req, res) => { const since = req.query.since || '0'; const changes = await db.item.findMany({ where: { userId: req.user.id, updatedAt: { gt: new Date(parseInt(since)) }, }, }); res.json({ changes, now: Date.now() }); }); // Push client's pending changes app.post('/sync/push', async (req, res) => { const { changes } = req.body; for (const change of changes) { // Conflict resolution: compare updatedAt const existing = await db.item.findUnique({ where: { id: change.id } }); if (!existing || existing.updatedAt < change.updatedAt) { await db.item.upsert({ where: { id: change.id }, update: change, create: change, }); } } res.json({ synced: changes.length }); }); ``` Libraries that automate this: - **PowerSync** (Postgres → SQLite sync) - **Realm** (built-in sync, paid for cloud) - **WatermelonDB** (DIY with helpers) ## Presence (who's online) Write presence to Redis with TTL: ```javascript ws.on('open', () => redis.setex(`presence:${userId}`, 60, '1')); ws.on('close', () => redis.del(`presence:${userId}`)); // Heartbeat every 30s to refresh TTL setInterval(() => redis.setex(`presence:${userId}`, 60, '1'), 30000); // Who's online? const online = await redis.keys('presence:*'); ``` For many users, use Redis sorted sets by timestamp — more efficient than individual keys. ## Common pitfalls ## FAQ
Q Can I host WebSocket backend on shared DomainIndia?

Limited — cPanel's Node.js App + Passenger has rough WS support. For production mobile real-time, use VPS.

Q FCM or OneSignal?

FCM is free, lower-level. OneSignal wraps FCM + APNs with delivery analytics, segmentation, A/B tests. Start with FCM; migrate to OneSignal if you need marketing features.

Q Do I need both WS and push?

Yes for most apps. WS for active users, push for re-engagement. They complement.

Q What about Server-Sent Events (SSE)?

SSE is one-way (server→client), simpler, survives most firewalls better than WS. Good for dashboards, notifications. Use WS for bidirectional (chat).

Q Battery impact of heartbeats?

30s heartbeat is the sweet spot — keeps NAT table alive, minimal battery. Lower = more battery; higher = risk of dropped connection unnoticed.

Run WebSocket + push backend on a DomainIndia VPS. View plans

Was this article helpful?

Your feedback helps us improve our documentation

Still need help? Submit a support ticket