AboutBlogContact
Mobile DevelopmentApril 22, 2012 7 min read 129Updated: June 22, 2026

Push Notifications Mastery: How We First Engaged Users in the Early iOS/Android Days

AunimedaAunimeda
📋 Table of Contents

Before push notifications, keeping users engaged with a mobile app meant hoping they'd open it. Email was the only re-engagement channel. SMS was expensive and invasive. Apps were islands - once the user left, you had no way to reach them until they came back.

Apple introduced Push Notification Service (APNS) with iPhone OS 3.0 in June 2009. Google responded with Cloud to Device Messaging (C2DM) in 2010, then Google Cloud Messaging (GCM) in 2012. Suddenly apps could reach users at any time.

The implementation was not simple.


How APNS Worked

Apple Push Notification Service used a persistent TLS connection between your backend server and Apple's servers. You sent a binary payload; Apple delivered it to the device.

Step 1: The device registers

// AppDelegate.m - iOS 5 era registration
- (BOOL)application:(UIApplication *)application
    didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
    
    // Register for remote notifications
    [[UIApplication sharedApplication] registerForRemoteNotificationTypes:
        UIRemoteNotificationTypeBadge |
        UIRemoteNotificationTypeSound |
        UIRemoteNotificationTypeAlert
    ];
    
    return YES;
}

// Called if registration succeeds - send token to YOUR server
- (void)application:(UIApplication *)application
    didRegisterForRemoteNotificationsWithDeviceToken:(NSData *)deviceToken {
    
    // Convert token to hex string
    NSString *token = [deviceToken.description
        stringByTrimmingCharactersInSet:[NSCharacterSet characterSetWithCharactersInString:@"<>"]];
    token = [token stringByReplacingOccurrencesOfString:@" " withString:@""];
    
    // POST to your backend API
    [self registerTokenWithServer:token];
}

- (void)application:(UIApplication *)application
    didFailToRegisterForRemoteNotificationsWithError:(NSError *)error {
    NSLog(@"Push registration failed: %@", error.localizedDescription);
}

The device token was a 32-byte identifier assigned by Apple to your app on a specific device. It changed if the user wiped their device or reinstalled the app. Your server had to store these tokens and handle the churn.

Step 2: Your server connects to APNS

APNS required a persistent binary TCP connection on port 2195, authenticated with an SSL certificate issued by Apple. No REST API. Raw binary protocol:

<?php
// PHP backend - sending via APNS binary protocol (simplified)

class APNSSender {
    private $socket;
    private $certPath;
    
    public function connect() {
        $ctx = stream_context_create();
        stream_context_set_option($ctx, 'ssl', 'local_cert', $this->certPath);
        stream_context_set_option($ctx, 'ssl', 'passphrase', 'your-cert-passphrase');
        
        // Production: gateway.push.apple.com:2195
        // Sandbox: gateway.sandbox.push.apple.com:2195
        $this->socket = stream_socket_client(
            'ssl://gateway.push.apple.com:2195',
            $errno, $errstr, 60,
            STREAM_CLIENT_CONNECT | STREAM_CLIENT_PERSISTENT,
            $ctx
        );
        
        if (!$this->socket) {
            throw new Exception("APNS connection failed: $errstr ($errno)");
        }
    }
    
    public function send($deviceToken, $message, $badge = 0, $sound = 'default') {
        $payload = json_encode([
            'aps' => [
                'alert' => $message,
                'badge' => $badge,
                'sound' => $sound,
            ]
        ]);
        
        // APNS binary frame format
        $tokenBinary = pack('H*', $deviceToken);   // 32 bytes
        $payloadLength = strlen($payload);
        
        // Enhanced notification format (added item ID and expiry for error feedback)
        $notification = pack('C', 1)             // Command (1 = enhanced)
            . pack('N', time())                  // Identifier
            . pack('N', time() + 86400)          // Expiry: 24 hours
            . pack('n', 32) . $tokenBinary       // Device token (length + data)
            . pack('n', $payloadLength) . $payload; // Payload (length + data)
        
        fwrite($this->socket, $notification);
    }
}

This was fragile. APNS silently dropped the connection on error - invalid tokens, malformed payloads - with a brief error response before closing. If you didn't read the error response immediately after each write, you'd miss it. Many implementations sent thousands of notifications before discovering the connection had died after the first bad token.


APNS Feedback Service

Apple ran a separate "feedback service" on port 2196. It returned a list of device tokens that should be removed - devices where the user had uninstalled your app:

public function getFeedback() {
    $ctx = stream_context_create();
    stream_context_set_option($ctx, 'ssl', 'local_cert', $this->certPath);
    
    $fp = stream_socket_client(
        'ssl://feedback.push.apple.com:2196',
        $errno, $errstr, 60, STREAM_CLIENT_CONNECT, $ctx
    );
    
    $tokens = [];
    while (!feof($fp)) {
        $data = fread($fp, 38);  // Timestamp(4) + TokenLength(2) + Token(32)
        if (strlen($data) === 38) {
            $parts = unpack('Ntimestamp/ntoken_length/H*token', $data);
            $tokens[] = [
                'token' => $parts['token'],
                'timestamp' => $parts['timestamp']
            ];
        }
    }
    fclose($fp);
    return $tokens;
}

You were supposed to poll this service daily and purge invalid tokens from your database. Apps that didn't do this accumulated huge lists of dead tokens, wasted bandwidth, and eventually got rate-limited by Apple.


Android: GCM Was Different

Google Cloud Messaging used a REST API. Much simpler from the server side:

// PHP - Sending via GCM REST API
function sendGCM($registrationId, $title, $message) {
    $apiKey = 'your-gcm-server-api-key';
    
    $payload = json_encode([
        'registration_ids' => [$registrationId],
        'data' => [
            'title'   => $title,
            'message' => $message,
            'time'    => time(),
        ],
        'collapse_key' => 'update',  // Replace pending notifications of same key
        'time_to_live' => 86400,     // 24 hours
    ]);
    
    $ch = curl_init('https://android.googleapis.com/gcm/send');
    curl_setopt_array($ch, [
        CURLOPT_POST           => true,
        CURLOPT_HTTPHEADER     => [
            'Authorization: key=' . $apiKey,
            'Content-Type: application/json',
        ],
        CURLOPT_POSTFIELDS     => $payload,
        CURLOPT_RETURNTRANSFER => true,
    ]);
    
    $response = json_decode(curl_exec($ch), true);
    curl_close($ch);
    
    // GCM returns canonical IDs for updated tokens
    if (isset($response['results'][0]['registration_id'])) {
        $newId = $response['results'][0]['registration_id'];
        // Update token in database
        updateDeviceToken($registrationId, $newId);
    }
    
    return $response;
}

GCM's canonical_id response was the equivalent of APNS feedback - if the token had changed, GCM told you the new one in the send response. Much cleaner design.

On the Android side:

// Android - handling GCM in a BroadcastReceiver
public class GCMReceiver extends BroadcastReceiver {
    
    @Override
    public void onReceive(Context context, Intent intent) {
        String action = intent.getAction();
        
        if ("com.google.android.c2dm.intent.RECEIVE".equals(action)) {
            String title = intent.getStringExtra("title");
            String message = intent.getStringExtra("message");
            
            showNotification(context, title, message);
        }
    }
    
    private void showNotification(Context context, String title, String body) {
        NotificationManager nm = (NotificationManager)
            context.getSystemService(Context.NOTIFICATION_SERVICE);
        
        Notification.Builder builder = new Notification.Builder(context)
            .setContentTitle(title)
            .setContentText(body)
            .setSmallIcon(R.drawable.ic_notification)
            .setAutoCancel(true);
        
        nm.notify(1, builder.build());
    }
}

The Database Schema

Storing device tokens required handling the multi-platform, multi-device reality:

CREATE TABLE push_tokens (
    id          INT AUTO_INCREMENT PRIMARY KEY,
    user_id     INT NOT NULL,
    platform    ENUM('ios', 'android') NOT NULL,
    token       VARCHAR(255) NOT NULL UNIQUE,
    app_version VARCHAR(20),
    created_at  TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    last_active TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    INDEX (user_id),
    INDEX (platform)
);

-- Track notification history for analytics
CREATE TABLE push_notifications (
    id          INT AUTO_INCREMENT PRIMARY KEY,
    token_id    INT NOT NULL,
    message     TEXT,
    payload     TEXT,
    sent_at     TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    status      ENUM('sent', 'failed', 'invalid_token') DEFAULT 'sent'
);

One user could have multiple devices. One device could have multiple apps (dev and production builds had different tokens). Token rotation meant updating rather than inserting duplicate users.


What Users Actually Responded To

The engagement data from our first year of push notifications:

Message Type Open Rate
"Check out our new arrivals!" 1.8%
"Your order has shipped" 34%
"[Username], you have a new message from [Name]" 41%
"Flash sale: 20% off in the next 2 hours" 12%
Weekly "top content" digest 6%

Transactional notifications (order shipped, new message from a specific person) had dramatically higher open rates than promotional blasts. This was obvious in retrospect, less obvious before seeing the data.

The lesson: push notifications were a privilege, not a marketing channel. Users who felt spammed disabled notifications; iOS required explicit permission and users used it. Apps that blasted promotional messages saw notification permissions drop to 40-50%. Apps that used notifications only for genuinely timely personal events kept permission rates above 80%.


How It Changed

Firebase Cloud Messaging (FCM) unified iOS and Android under a single API in 2016. Apple replaced the binary protocol with an HTTP/2 API in the same year. The architectural complexity of maintaining two separate pipelines, two certificate management processes, and two feedback polling jobs collapsed into one REST endpoint.

The best reminder of 2012: genuinely hard infrastructure problems eventually get standardized. Appreciate the complexity while it exists - it teaches you things. Then appreciate when someone builds an abstraction that makes it irrelevant.


Aunimeda develops mobile applications for iOS and Android - from MVP to production-ready apps with full backend integration.

Contact us to discuss your mobile project. See also: Mobile App Development, Mobile Game Development

Read Also

Flutter App Development in Bishkek: Cost, Timeline, and What to Expectaunimeda
Mobile Development

Flutter App Development in Bishkek: Cost, Timeline, and What to Expect

Everything you need to know about Flutter mobile app development in Bishkek, Kyrgyzstan in 2026: costs, timelines, team structure, and how to evaluate a development partner.

React Native Push Notifications in 2026: Complete Guide (Expo + Firebase)aunimeda
Mobile Development

React Native Push Notifications in 2026: Complete Guide (Expo + Firebase)

Push notifications are the single highest-ROI feature in mobile apps. Open rates are 7x higher than email. Here's how to implement them correctly in React Native - including background handling, deep linking, and analytics.

Flutter vs React Native in 2026: An Engineer's Honest Comparisonaunimeda
Mobile Development

Flutter vs React Native in 2026: An Engineer's Honest Comparison

Flutter and React Native both ship to iOS and Android from a single codebase. But they make radically different bets. Here's the concrete trade-offs - performance benchmarks, ecosystem, hiring market, and which one fits which product.

Need IT development for your business?

We build websites, mobile apps and AI solutions. Free consultation.

Mobile App Development

Get Consultation All articles