Back to Blog

Mobile App Backend Development: APIs, Push Notifications, and Offline Sync

Mobile app backend development in 2026 — REST vs GraphQL for mobile, push notification architecture, offline-first sync patterns, and production backend design

Viprasol Tech Team
April 3, 2026
12 min read

Mobile App Backend Development: APIs, Push Notifications, and Offline Sync

Mobile backends have unique requirements that web backends often handle poorly: intermittent connectivity, battery constraints, platform-specific push notification routing, background sync, and the need to support multiple API versions simultaneously (because users don't always update their apps).

This guide covers the complete mobile backend stack — API design for mobile clients, push notification architecture, offline-first sync patterns, and the versioning strategies that prevent old app versions from breaking.


BaaS vs. Custom Backend

Before building, consider managed Backend-as-a-Service options:

OptionBest ForMonthly Cost (at scale)Limitations
Firebase (Google)Rapid prototyping, real-time apps$25–$500Vendor lock-in, limited querying
SupabasePostgreSQL-based apps, open source$25–$599Younger ecosystem
AWS AmplifyAWS-native apps$25–$300Complex config
Custom (Node.js/Python)Complex business logic, compliance$100–$1,000 (infra)Higher build cost

Rule: Use Firebase or Supabase for apps where real-time sync, simple data models, and fast iteration are priorities. Build custom for apps with complex business logic, compliance requirements (HIPAA, PCI), or multi-platform requirements (mobile + web + third-party integrations).


API Design for Mobile Clients

Mobile APIs have different constraints than web APIs:

Minimize round trips. A web browser on WiFi handles 10 API calls per page load fine. A mobile client on 3G in rural India cannot. Design endpoints that return everything the screen needs in one call.

// ❌ BAD: 4 separate calls to render a profile screen
// GET /users/123
// GET /users/123/posts
// GET /users/123/followers
// GET /users/123/following

// ✅ GOOD: Single call returns everything the profile screen needs
app.get('/api/v1/users/:id/profile', authenticate, async (req, res) => {
  const userId = req.params.id;
  
  const [user, posts, followerCount, followingCount, isFollowing] = await Promise.all([
    db('users').where({ id: userId }).select('id', 'username', 'bio', 'avatar_url').first(),
    db('posts').where({ user_id: userId }).orderBy('created_at', 'desc').limit(12),
    db('follows').where({ following_id: userId }).count('id as count').first(),
    db('follows').where({ follower_id: userId }).count('id as count').first(),
    db('follows').where({ follower_id: req.user.sub, following_id: userId }).first(),
  ]);

  res.json({
    user,
    recentPosts: posts,
    followerCount: Number(followerCount?.count ?? 0),
    followingCount: Number(followingCount?.count ?? 0),
    isFollowing: !!isFollowing,
  });
});

Pagination for lists. Mobile screens are small; infinite scroll is the pattern. Use cursor-based pagination (not offset) for consistent results with real-time data:

// Cursor-based pagination — stable across real-time inserts/deletes
app.get('/api/v1/feed', authenticate, async (req, res) => {
  const { cursor, limit = 20 } = req.query;
  const pageLimit = Math.min(Number(limit), 50); // Cap at 50

  let query = db('posts')
    .join('users', 'posts.user_id', 'users.id')
    .select(
      'posts.id', 'posts.content', 'posts.created_at',
      'users.username', 'users.avatar_url'
    )
    .orderBy('posts.created_at', 'desc')
    .limit(pageLimit + 1); // Fetch one extra to determine hasMore

  if (cursor) {
    // Cursor is a base64-encoded timestamp
    const cursorTime = new Date(Buffer.from(cursor as string, 'base64').toString());
    query = query.where('posts.created_at', '<', cursorTime);
  }

  const items = await query;
  const hasMore = items.length > pageLimit;
  const results = hasMore ? items.slice(0, pageLimit) : items;
  const nextCursor = hasMore
    ? Buffer.from(results[results.length - 1].created_at.toISOString()).toString('base64')
    : null;

  res.json({ items: results, nextCursor, hasMore });
});

Response compression. Enable gzip/brotli — mobile clients benefit significantly from smaller payloads:

import compression from 'compression';

app.use(compression({
  level: 6,
  threshold: 1024, // Only compress responses > 1KB
}));

🌐 Looking for a Dev Team That Actually Delivers?

Most agencies sell you a project manager and assign juniors. Viprasol is different — senior engineers only, direct Slack access, and a 5.0★ Upwork record across 100+ projects.

  • React, Next.js, Node.js, TypeScript — production-grade stack
  • Fixed-price contracts — no surprise invoices
  • Full source code ownership from day one
  • 90-day post-launch support included

API Versioning for Mobile

Mobile apps can't force users to update. You must maintain backward compatibility for old versions, sometimes for 12–24 months.

// Version-aware routing
app.use('/api/v1', v1Router);
app.use('/api/v2', v2Router);

// Or version via Accept header (more RESTful)
app.use('/api/users', (req, res, next) => {
  const version = req.headers['accept-version'] ?? 'v1';
  req.apiVersion = version;
  next();
});

// In controllers, branch on version
async function getUserProfile(req: Request, res: Response) {
  const user = await fetchUser(req.params.id);
  
  if (req.apiVersion === 'v2') {
    // v2 includes social graph counts inline
    res.json(formatUserV2(user));
  } else {
    // v1 — legacy format
    res.json(formatUserV1(user));
  }
}

Sunset policy: When deprecating a version, return Deprecation and Sunset headers so clients can detect the timeline:

// Add to deprecated API versions
app.use('/api/v1', (req, res, next) => {
  res.setHeader('Deprecation', 'true');
  res.setHeader('Sunset', 'Sat, 01 Oct 2026 00:00:00 GMT');
  res.setHeader('Link', '</api/v2>; rel="successor-version"');
  next();
});

Push Notifications Architecture

Push notifications route through platform-specific services:

  • iOS → Apple Push Notification Service (APNs)
  • Android → Firebase Cloud Messaging (FCM)

Use a backend service that abstracts both:

// Device token registration — store with platform info
app.post('/api/devices/register', authenticate, async (req, res) => {
  const { token, platform, deviceId } = req.body;
  // platform: 'ios' | 'android'

  await db('device_tokens').insert({
    user_id: req.user.sub,
    token,
    platform,
    device_id: deviceId,
    created_at: new Date(),
  }).onConflict('device_id').merge(['token', 'updated_at']); // Update token if rotated

  res.json({ registered: true });
});
// Push notification service — abstracts APNs and FCM
import admin from 'firebase-admin';

// Initialize Firebase Admin (handles both APNs via Firebase and FCM)
admin.initializeApp({
  credential: admin.credential.cert({
    projectId: process.env.FIREBASE_PROJECT_ID,
    clientEmail: process.env.FIREBASE_CLIENT_EMAIL,
    privateKey: process.env.FIREBASE_PRIVATE_KEY!.replace(/\\n/g, '\n'),
  }),
});

interface PushNotification {
  title: string;
  body: string;
  data?: Record<string, string>;
  badge?: number;    // iOS only
  sound?: string;
}

async function sendPushToUser(userId: string, notification: PushNotification): Promise<void> {
  const tokens = await db('device_tokens')
    .where({ user_id: userId })
    .select('token', 'platform');

  if (tokens.length === 0) return;

  const messages = tokens.map(({ token, platform }) => ({
    token,
    notification: {
      title: notification.title,
      body: notification.body,
    },
    data: notification.data,
    apns: platform === 'ios' ? {
      payload: {
        aps: {
          badge: notification.badge,
          sound: notification.sound ?? 'default',
        },
      },
    } : undefined,
    android: platform === 'android' ? {
      priority: 'high' as const,
      notification: { sound: 'default' },
    } : undefined,
  }));

  const response = await admin.messaging().sendEach(messages);

  // Remove invalid/expired tokens
  const expiredTokens = response.responses
    .map((r, i) => (!r.success && isExpiredTokenError(r.error)) ? tokens[i].token : null)
    .filter(Boolean);

  if (expiredTokens.length > 0) {
    await db('device_tokens').whereIn('token', expiredTokens).delete();
  }
}

function isExpiredTokenError(error: admin.FirebaseError | undefined): boolean {
  return error?.code === 'messaging/registration-token-not-registered' ||
         error?.code === 'messaging/invalid-registration-token';
}

🚀 Senior Engineers. No Junior Handoffs. Ever.

You get the senior developer, not a project manager who relays your requirements to someone you never meet. Every Viprasol project has a senior lead from kickoff to launch.

  • MVPs in 4–8 weeks, full platforms in 3–5 months
  • Lighthouse 90+ performance scores standard
  • Works across US, UK, AU timezones
  • Free 30-min architecture review, no commitment

Offline-First Sync

Users expect apps to work without connectivity. Offline-first means the app reads from and writes to a local database, then syncs when connectivity returns.

Sync Pattern

┌─────────────────────────────────────────────────┐
│  Mobile Client                                  │
│  Local DB (SQLite / WatermelonDB / Realm)        │
│    ↓ (always reads/writes locally)              │
│  Sync Engine                                    │
│    - Queue local changes                        │
│    - Detect connectivity                        │
│    - Push changes to server on reconnect        │
│    - Pull server changes, merge conflicts       │
└─────────────────────────────────────────────────┘
              ↕ (sync on connectivity)
┌─────────────────────────────────────────────────┐
│  Backend                                        │
│  Sync API (/api/sync)                           │
│  Conflict Resolution Logic                      │
│  Change Log Table                               │
└─────────────────────────────────────────────────┘
// Backend sync endpoint — handles delta sync
app.post('/api/v1/sync', authenticate, async (req, res) => {
  const { lastSyncedAt, changes } = req.body;
  const userId = req.user.sub;

  // 1. Apply client changes to server (with conflict detection)
  const conflicts: SyncConflict[] = [];
  
  for (const change of changes) {
    const existing = await db('items').where({ id: change.id }).first();
    
    if (existing && existing.updated_at > new Date(change.clientUpdatedAt)) {
      // Server version is newer — conflict!
      // Last-write-wins: use server version (or implement more complex merge)
      conflicts.push({ clientChange: change, serverRecord: existing });
    } else {
      // Apply client change
      await db('items').insert({
        id: change.id,
        user_id: userId,
        ...change.data,
        updated_at: new Date(),
      }).onConflict('id').merge();
    }
  }

  // 2. Return server changes since last sync
  const serverChanges = await db('items')
    .where({ user_id: userId })
    .where('updated_at', '>', new Date(lastSyncedAt ?? 0))
    .select('*');

  res.json({
    serverChanges,
    conflicts,
    syncedAt: new Date().toISOString(),
  });
});

Cost to Build a Mobile Backend

ScopeTimelineInvestment
Simple CRUD backend (BaaS-based)2–4 weeks$5,000–$15,000
Custom REST API (no real-time)4–8 weeks$15,000–$35,000
Full backend (auth + push + offline sync)8–16 weeks$35,000–$80,000
Backend + admin dashboard + analytics16–24 weeks$80,000–$200,000

Working With Viprasol

We build mobile backends for React Native, Flutter, and native iOS/Android apps — from simple API layers through real-time, offline-first systems with push notification infrastructure.

Mobile backend consultation →
Mobile App Development →
Web Development Services →


See Also


Share this article:

About the Author

V

Viprasol Tech Team

Custom Software Development Specialists

The Viprasol Tech team specialises in algorithmic trading software, AI agent systems, and SaaS development. With 100+ projects delivered across MT4/MT5 EAs, fintech platforms, and production AI systems, the team brings deep technical experience to every engagement. Based in India, serving clients globally.

MT4/MT5 EA DevelopmentAI Agent SystemsSaaS DevelopmentAlgorithmic Trading

Need a Modern Web Application?

From landing pages to complex SaaS platforms — we build it all with Next.js and React.

Free consultation • No commitment • Response within 24 hours

Viprasol · Web Development

Need a custom web application built?

We build React and Next.js web applications with Lighthouse ≥90 scores, mobile-first design, and full source code ownership. Senior engineers only — from architecture through deployment.