Client Area

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

ByDomain India Team·DomainIndia Engineering
6 min readPublished 22 Apr 2026Updated 23 Jun 2026135 views

In this article

  • 1Three kinds of real-time for mobile
  • 2Part 1 — WebSocket for in-app real-time
  • 3Server (Node.js with `ws`)
  • 4Client (React Native / Flutter)
  • 5Part 2 — Reconnection & offline buffer

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).

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