Chapters

Hide chapters

Push Notifications by Tutorials

Third Edition · iOS 14 · Swift 5.3 · Xcode 12

Before You Begin

Section 0: 4 chapters
Show chapters Hide chapters

Section I: Push Notifications by Tutorials

Section 1: 14 chapters
Show chapters Hide chapters

6. Server-Side Pushes
Written by Scott Grosch

While you’ve successfully sent yourself a notification, doing this manually won’t be very useful. As customers run your app and register for receiving notifications, you’ll need to somehow store their device tokens so that you can send them notifications at a later date.

Using third-party services

There is a slew of services online that will handle the server-side for you. You can simply search Google for something along the lines of “Apple push notification companies” and you’ll find multiple examples. Some of the most popular ones are:

Each company will vary in its pricing and API, so discussing any specific service is beyond the scope of this book. If you want to get running quickly or don’t want to deal with anything on the server-side, then solutions like the above may be perfect for you.

You may find, however, that you prefer avoiding third-party services, as you can run into issues if the service changes how its API works or if the company goes out of business. These services will usually also charge a fee based on how many notifications you send.

As an iOS developer, you might already be paying for a web hosting service for your website, which gives you the tools you need to do this work yourself — and you can find multiple vendors that charge $10 or less per month. Most web hosting services provide SSH access and the ability to run a database. Since handling the server-side only requires a single database table, a couple of REST endpoints, and a few easy-to-write pieces of code, you may want to do this work yourself.

If you have no interest in running your own server, you can skip to Chapter 7, “Expanding the Application.”

Note: Some examples in the rest of the book assume you are connecting to the server you’ll set up in this chapter.

Installing Docker

If you don’t already have Docker installed, please go to the Docker for Mac (dockr.ly/2JOzJ31) site and follow the installation instructions. Since you’ll be using the Docker CLI tools, you might need to use the docker login command for the initial setup.

Generate the Vapor project

Now it’s time to build your web service. For this tutorial, you’ll implement the web service with Vapor. Vapor is a very well supported implementation of server-side development using Swift. Without too much code you can use it to control your SQL database as well as your RESTful API. To use Vapor, though, there’s a little bit of setup that needs to happen. If you’re not familiar with Vapor, you can find a list of resources at the end of this chapter.

It’s time to create your server app, which will allow you to store your web tokens!

If you had Vapor 3 or earlier installed on your machine, you’ll need to remove the previous installation by running this command, in Terminal:

$ brew untap vapor/tap/vapor

If you don’t have Vapor installed, or after removing the older version, run the following command in Terminal:

$ brew install vapor

Note: If you don’t have Homebrew already installed, install it by following the instructions at brew.sh.

Now, from Terminal, generate your project by running the following commands:

$ vapor new WebService --fluent.db Postgres

This sets up a new Vapor web service that uses the Posgres database. It already adds all the dependencies needed for Vapor and the database as well as a general folder structure with a few example files.

Next, navigate to the folder Vapor created for you.

$ cd WebService

The Vapor CLI has also generated appropriate Docker configuration files. Make sure you open Docker and give it the necessary permissions to install its helpers. Once Docker is running, from Terminal, tell Docker to bring your PostgreSQL database online:

$ docker-compose up db

If you’re already running PostgreSQL natively on your Mac you’ll get messages like this:

Recreating webservice_db_1 …
Recreating webservice_db_1 … error

ERROR: for webservice_db_1 Cannot start service db: driver failed programming external connectivity on endpoint webservice_db_1 (388144b568ed26530852f351bb374db228e35ad5a246b26b53c5926f626b5fa6): Bind for 0.0.0.0:5432 failed: port is already allocated

ERROR: for db Cannot start service db: driver failed programming external connectivity on endpoint webservice_db_1 (388144b568ed26530852f351bb374db228e35ad5a246b26b53c5926f626b5fa6): Bind for 0.0.0.0:5432 failed: port is already allocated
ERROR: Encountered errors while bringing up the project.

Don’t worry if that happens, the fix is quite simple! The last line of docker-compose.yml should be changed to this:

- '32768:5432'

That tells Docker to internally map port 5432, which is what PostgreSQL expects to use, to port 32768 on your Mac. By changing the port, you remove the conflict. UNIX defines ports 32768 – 65535 as available for your app to use.

When Docker successfully starts your database, you’ll see output like this:

Recreating webservice_db_1 … done
Attaching to webservice_db_1
db_1 |
db_1 | PostgreSQL Database directory appears to contain a database; Skipping initialization
db_1 |
db_1 | 2020-11-16 06:42:21.450 UTC [1] LOG: starting PostgreSQL 12.3 on x86_64-pc-linux-musl, compiled by gcc (Alpine 9.3.0) 9.3.0, 64-bit
db_1 | 2020-11-16 06:42:21.450 UTC [1] LOG: listening on IPv4 address “0.0.0.0”, port 5432
db_1 | 2020-11-16 06:42:21.450 UTC [1] LOG: listening on IPv6 address “::”, port 5432
db_1 | 2020-11-16 06:42:21.455 UTC [1] LOG: listening on Unix socket “/var/run/postgresql/.s.PGSQL.5432”
db_1 | 2020-11-16 06:42:21.509 UTC [20] LOG: database system was shut down at 2020-11-02 00:26:24 UTC
db_1 | 2020-11-16 06:42:21.522 UTC [1] LOG: database system is ready to accept connections

Edit the Xcode project

In Finder, navigate to your WebService folder and double-click on the Package.swift file.

Vapor uses Apple’s Swift Package Manager to generate the Xcode project. Instead of opening an Xcode project file, you open the Package.swift file with Xcode. After opening it, Xcode will take a few minutes to fetch all of your project’s dependencies. You’ll know it’s done downloading the dependencies when Xcode shows the version number next to each listed package.

Defining the model

The device token you receive from Apple is the model that you’ll store. Create the Sources/App/Models/Token.swift file and add the following code into it:

import Fluent
import Vapor

final class Token: Model {
  // 1
  static let schema = "tokens"
  // 2
  @ID(key: .id)
  var id: UUID?
  // 3
  @Field(key: "token")
  var token: String
  @Field(key: "debug")
  var debug: Bool
  // 4
  init() { }

  init(token: String, debug: Bool) {
    self.token = token
    self.debug = debug
  }
}

Vapor’s Model is how you represent a table in the database. All of the models you create will follow the above template.

  1. You identify what the name of the table is in the database.
  2. You identify what the primary key is. Vapor requires the Swift variable for the identifier to be called id. You can use almost any type but the convention for Vapor is that the ID should be a UUID as that’s portable to all database types.
  3. Next, you’ll create a property for each column in the database. Using the @Field(key:) property wrapper, you tell Swift what the name of the column is in the database. In the examples above, the SQL column name matches the Swift variable. However, if the database uses snake_case, you’ll be able to map back to a camelCase Swift variable.
  4. Finally, you create the required initializers. A Vapor model always requires an empty initializer, and you’ll generally want to provide one that takes the non-ID properties.

When you’re creating a new token, you’ll not have an ID to specify, which is why that has to be specified as an optional value.

Configuring the database table

Now Xcode knows the structure of your model, but it doesn’t yet exist in the database. Vapor will handle that task for you as well!

Create the file Sources/App/Migrations/CreateToken.swift and paste the following code:

import Vapor
import Fluent

struct CreateToken: Migration {
  func prepare(on database: Database) -> EventLoopFuture<Void> {
    return database.schema("tokens")
      .id()
      .field("token", .string, .required)
      .field("debug", .bool, .required)
      .unique(on: "token")
      .create()
  }

  func revert(on database: Database) -> EventLoopFuture<Void> {
    return database.schema("tokens").delete()
  }
}

Vapor uses a process called migrations to handle the creation and deletion of database tables, which is implemented via the Migration protocol

The Migration code is what Vapor uses to properly create the database schema. This simply tells PostgreSQL to create the table if it doesn’t already exist, make a required column for each property in the Token class, then ensure that the token column has a UNIQUE constraint assigned to it.

Creating the controller

Now that you’ve got a model, you’ll need to create the controller that will respond to your HTTP POST and DELETE requests. Controllers in Vapor are similar to a UIViewController in Swift. They are what controls the implementation.

You will need to create a couple of endpoints for your HTTP clients to call.

Creating tokens

Create the Sources/App/Controllers/TokenController.swift file and add the following code:

import Fluent
import Vapor

struct TokenController {
  func create(req: Request) throws -> EventLoopFuture<HTTPStatus> {
    // 1
    try req.content.decode(Token.self)
      // 2
      .create(on: req.db)
      // 3
      .transform(to: .noContent)
  }
}

Vapor uses futures heavily to implement an asynchronous programming model. The create(req:) method is where you implement the creation of a new database token in the database. While there are only three lines of code, there’s quite a bit of functionality!

  1. First, the method examines the content of the request and attempts to decode the JSON payload into the Token structure which you previously defined. If the payload doesn’t match the structure then the method will throw an error.
  2. Once the content has been decoded, the data is created as a new row in the database.
  3. Finally, because the client doesn’t care about the details of the newly created row, you transform the return into an HTTP status code of .noContent, which equates to a 204 status.

Database operations are slow and expensive, which is why the methods are defined using a future. Using a future means the create(req:) method will exit before the row has been created, which results in much better performance.

Deleting tokens

Now that you have a way to create tokens, you should probably handle the need to delete tokens which are no longer valid. Add this method to your controller:

func delete(req: Request) throws -> EventLoopFuture<HTTPStatus> {
  // 1
  let token = req.parameters.get("token")!
  // 2
  return Token.query(on: req.db)
    // 3
    .filter(\.$token == token)
    // 4
    .first()
    .unwrap(or: Abort(.notFound))
    // 5
    .flatMap { $0.delete(on: req.db) }
    // 6
    .transform(to: .noContent)
}

The nice thing about the Vapor framework is that it’s pretty intuitive to figure out what is happening. Even if you’re completely new to Vapor, I bet you can figure out what the code is doing.

  1. Start by grabbing the token parameter from the request. For now, just trust that it’s the specified token.
  2. You’ve identified that you want to search the database’s Token table.
  3. Instead of querying every token, filter the results to just the token you asked for. Notice how Vapor uses Swift 5’s property wrappers. It’s easy to forget to add that ‘$’ to the keypath!
  4. SQL queries can always return multiple rows. However, since you made the token a unique constraint in the database, you can tell Xcode to just grab the first record. If no rows were returned, then the unwrap will fail and the HTTP client will get a 404 status.
  5. Remember that database results in Vapor are always futures. By passing the future through a flatMap call, you get access to the actual row, as opposed to just the future value of the row. So delete it!
  6. Finally, as before, you just want to pass a 204 status code back to the client.

Pay attention to the fact you’ve done the lookup via the token and not the token’s database ID. You’ll only try to delete a token if Apple said that the token was invalid. When that happens, the calling client has no idea what the primary key is in your database. It just wants to pass the token itself.

Setting up routes

In the delete method you just implemented, you’re expecting the caller to pass the token which should be deleted as part of the request. When the calling HTTP client connects to a URL like this:

https://..../token/0549f2c6d0d2887b0f8122b8b1ac45

You want it to call your delete method and know that the token parameter is the 0549f2c6d0d2887b0f8122b8b1ac45 part.

To accomplish linking a URL to a method, you’ll set up what is known as routing. Add the following code to the end of the file:

extension TokenController: RouteCollection {
  func boot(routes: RoutesBuilder) throws {
    let tokens = routes.grouped("token")
    tokens.post(use: create)
    tokens.delete(":token", use: delete)
  }
}

At startup, when properly registered, the boot(routes:) method will be called to register the aforementioned routes. By calling routes.grouped("token") you’re letting Vapor know that you’ll be implementing a group of routes that are all accessible after the token component of a URL.

Then you’ve identified that when making an HTTP POST request, the create method defined on TokenController should be executed.

Similarly, you’ve identified that when an HTTP DELETE request is sent that it should call the delete method on TokenController. However, this time, you’ve specified that after the tokens part of the URL you’ll provide one more path component. That last piece will be assigned to the token parameter which you queried at the start of the delete(req:) method. Placing the : character at the start of the string lets Vapor know you’re identifying a placeholder, as opposed to wanting the text token to appear in the URL.

There’s just one last piece to making your two routes work. Remember how I said, “When properly registered”? Open Sources/routes.swift and you’ll see the default example that Vapor provided when you created the project. Delete the entire implementation of the routes(_:) method and replace it with a single line:

try app.register(collection: TokenController())

Registering a collection is how you identify that the controller has implemented the RouteCollection protocol.

That’s all you need for handling APNs tokens! You might be wondering why there are no methods to get a token. If you consider the usage of the API, you need to store and delete tokens. There’s never a case in which you would want an HTTP client to be able to find out which APNs tokens are registered.

Configuring the app

Because you’re running the server locally during debugging, you’ll have to take an extra step to tell Vapor that it should respond to more than just local connections. You only have to do this during development.

Edit the file Sources/App/configure.swift and add the following lines just before the // register routes comment.

if app.environment != .production {
    app.http.server.configuration.hostname = "0.0.0.0"
}

By setting the hostname to 0.0.0.0 you’ve let Vapor, and your Mac, know that it should accept HTTP connections that come from outside the Mac. When your iOS app starts up and wants to register with your web service for push notifications, it has to be allowed to connect over WiFi.

Registering the migrations

There’s just one step left to make everything work. You have to tell Vapor that it should run the migrations for the Token class. While still in configure.swift, replace this line:

app.migrations.add(CreateTodo())

with these lines:

app.migrations.add(CreateToken())
try! app.autoMigrate().wait()

The todo files are provided as samples, so there’s no reason to register a migration for it. Vapor will not regenerate your database tables based on the migrations you define automatically unless you tell it to.

Build and run your project. If you’re getting error messages related to NIO, that usually means there’s a problem connecting to your database. Some common items you may want to look into if you get errors:

  • Is another webserver running on port 8080? Try lsof -i :8080.
  • Is another Docker instance already running?
    • Use docker ps to find your container, and then stop it with docker stop <container-id>, and re-run the docker setup command from earlier in this chapter.
  • Are the database, user and password all configured correctly?

Testing your API

At this point, you can use any REST-capable app to test out your endpoints. A good choice is Rested, which is available as a free download from the Mac App Store at https://apple.co/2HP0lEH.

To test your POST endpoint, set up the request as follows:

  • URL: http://192.168.1.1:8080/token (Use your IP address).
  • METHOD: POST.
  • Add a parameter called token and put any value you like.
  • Add a parameter called debug and put the value true
  • Select JSON-encoded as the request type. This ensures that the data is sent as JSON and that the Content-Type header is set to application/json.

Your request will look similar to the following:

Press the Send Request button. You should see HTTP/1.1 204 No Content in the Response Body section at the lower-right of the image, telling you that your token was stored in the database and given a unique identifier.

HTTPie

If you’re more of a command-line person, you might want to look at HTTPie (httpie.io). After installing you can do this from Terminal:

$ http POST 192.168.1.39:8080/token debug:=true token=qwer

Running your iOS app

Now that your server is operational and has an endpoint to store a token, you can make your iOS app send the token to the server once it registers for push notifications. Chapter 7, “Expanding the Application” already includes a ready-made app that performs this task in the final folder of its materials. First, build and run your server. Next, open the PushNotfications iOS app from Chapter 7 in a separate Xcode window.

You’ll need to know the IP address of your Mac so that your iOS app can make a connection to the webserver. It’s relatively easy to find the IP address by clicking on the Apple icon in your Mac’s menu bar and then choosing the System Preferences… option.

In the search field, simply type ipv4 and select the IPv4 item shown in the drop-down:

Note: This is your internal IP address, not what’s visible outside your network. Do not try to use a webpage like www.whatsmyip.org to get this value!

Open AppDelegate.swift and change the IP address in the sendPushNotificationDetails(to:using:) call to the one from System Preferences. It should look like http://YOUR-IP-ADDRESS:8080/token. Finally, build and run the iOS project on a device.

When the device runs, you should an output that looks like [ INFO ] POST / [request-id: ...] in the WebService Xcode window command line.

Your server received a request from your device and stored its device token in the database. Don’t worry about the specifics of how this works just yet — you’ll go over the iOS code in the next chapter. Now that your server has your device’s token, it’s time to send some pushes to the device!

Sending pushes

While you’re used to Apple providing libraries for iOS development, server-side Swift is built around community-made packages for specific tasks. Vapor has their own APNs package that makes it easy to send notifications through Apple’s servers.

Send with Vapor

Still in Xcode, edit the Package.swift file. Add the following line to the top-most dependencies key to add a new package:

.package(url: "https://github.com/vapor/apns", from: "1.0.0")

Next, under the targets key, add the following line inside dependencies to add the new package’s corresponding product:

.product(name: "APNS", package: "apns")

Don’t forget to add a comma after the previous item. After closing Package.swift, Xcode will download the package, as well as its dependencies, and show them in the navigator with all the other Swift Package Dependencies.

The first step is to tell configure Vapor with APNs, so edit the configure.swift to add the appropriate import:

import APNS

Then, add the following code at the end of the configure(_:) method:

let apnsEnvironment: APNSwiftConfiguration.Environment
apnsEnvironment = app.environment == .production ? .production : .sandbox

let auth: APNSwiftConfiguration.AuthenticationMethod = try .jwt(
  key: .private(filePath: "/full/path/to/AuthKey_...p8"),
  keyIdentifier: "...",
  teamIdentifier: "..."
)

app.apns.configuration = .init(authenticationMethod: auth,
                               topic: "com.raywenderlich.PushNotifications",
                               environment: apnsEnvironment)

There are a couple of things that you need to modify to match your project:

  1. Change the key’s filePath to be a path to the your .p8 authentication key file you obtained in the previous chapter. Be sure that you specify a fully qualified path!
  2. keyIdentifier is the middle part of the filename that you downloaded from Apple.
  3. teamIdentifier comes from your developer account’s Membership page (https://apple.co/2tXpJ2m).
  4. Finally, make sure to change the configuration’s topic to match your apps bundle ID.

Edit the TokenController.swift file and import the APNS module:

import APNS

Now add a new method to send everyone in the database a notification.

func notify(req: Request) throws -> EventLoopFuture<HTTPStatus> {
  let alert = APNSwiftAlert(title: "Hello!", body: "How are you today?")
}

For this example, you’ll send the same notification to everyone, so generate the alert text first. The constructor takes a ton of optional parameters, which you might want to use in your app.

Next, continue writing the method with the following code:

// 1
return Token.query(on: req.db)
  .all()
  // 2
  .flatMap { tokens in
    // 3
    tokens.map { token in
      req.apns.send(alert, to: token.token)
        // 4
        .flatMapError {
          // Unless APNs said it was a bad device token, just ignore the error.
          guard case let APNSwiftError.ResponseError.badRequest(response) = $0,
            response == .badDeviceToken else {
            return req.db.eventLoop.future()
          }

          return token.delete(on: req.db)
        }
    }
    // 5
    .flatten(on: req.eventLoop)
    // 6
    .transform(to: .noContent)
  }

The preceding code works as follows:

  1. Query all tokens in the database without any filtering.
  2. You use .flatMap to resolve the future, giving you an actual array of Token objects.
  3. Loop through each token and send a push, returning the future that the push generates.
  4. If you send a bad device token then remove it from the database so you don’t do that again.
  5. Flatten the array of futures back down to a single future.
  6. There’s nothing to tell the caller, so return a 204 status code via the .noContent enum value.

In order to be able to call that route, add another line to the end of the boot(routes:) method:

tokens.post("notify", use: notify)

Build and run the app again, and then send a POST to the new route. With HTTPie, send the following command:

$ http POST 0.0.0.0:8080/token/notify

Or, if you prefer Rested, open the app and change the URL to 0.0.0.0:8080/token/notify, then change the method to POST and press Send Request.

If everything worked, your device should receive a push notification! While the sample app sends notifications based on a route, you wouldn’t normally do that in a production app. Using a route is just an easy way to show you the code necessary to send a push notification.

Send with curl

Before it was possible to use Swift, PHP with libcurl was the most common solution used by developers to send a push notification. If you wish to use PHP, you’ll need to make sure that the curl command built for your system supports HTTP2. Run it with the -V flag and ensure you see HTTP2 in the output:

$ curl -V  
curl 7.48.0 (x86_64-pc-linux-gnu) libcurl/7.48.0 OpenSSL/1.0.2h zlib/1.2.7 libidn/1.28 libssh2/1.4.3 nghttp2/1.11.1    
Protocols: dict file ftp ftps gopher http https imap imaps ldap ldaps pop3 pop3s rtsp scp sftp smb smbs smtp smtps telnet tftp   
Features: IDN IPv6 Largefile NTLM NTLM_WB SSL libz TLS-SRP **HTTP2** UnixSockets

If HTTP2 isn’t there, you’ll need to install a newer version. If your target system supports Homebrew (brew.sh) then you can install by running these commands:

$ brew install curl-openssl
$ echo 'export PATH="/usr/local/opt/curl/bin:$PATH"' >> ~/.zshrc

Once you do that, restart Terminal and run curl -V again. You should now see HTTP2 in the list of features.

If you’re using some type of Linux system, you’ll have to build curl yourself. That can become quite cumbersome though as you’ll also need nghttp2, openssl, etc…

On to the script! Create a new file using your favorite editor called sendPushes.php. This isn’t part of your Xcode project so store it wherever you’re keeping your webserver’s source files. You’ll create a small PHP script that will send a HTTP/2 network request to APNs.

Firstly, you’ll need to specify your Auth Key details and what the payload will be:

<?php

const AUTH_KEY_PATH = '/full/path/to/AuthKey_keyid.p8';
const AUTH_KEY_ID = '<your auth key id here>';
const TEAM_ID = '<your team id here>';
const BUNDLE_ID = 'com.raywenderlich.APNS';

$payload = [
  'aps' => [
    'alert' => [
      'title' => 'This is the notification.',
    ],
    'sound'=> 'default',
  ],
];

Fill in those const values based on your specific details.

Next, create a method to get your list of tokens. This will be very app-specific, but as a simple example, you can just get all the registered tokens in the database.

Add the following code below your $payload variable:

$db = new PDO('pgsql:host=localhost;dbname=apns;user=apns;password=password');

function tokensToReceiveNotification($debug) {
  $sql = 'SELECT DISTINCT token FROM tokens WHERE debug = :debug';
  $stmt = $GLOBALS['db']->prepare($sql);
  $stmt->execute(['debug' => $debug ? 't' : 'f']);

  return $stmt->fetchAll(PDO::FETCH_COLUMN, 0);
}

Notice how you’re differentiating between debug and production tokens.

Note: Any app that was installed directly via Xcode is considered a debugging app and must be sent to a different server than apps installed via TestFlight or the App Store. More on this in a moment.

The only tricky part to sending a push notification using the newer HTTP/2 protocol is getting the authentication header right. This is the part that Apple didn’t provide much guidance on when it released its authentication token implementation.

Append the following code:

function generateAuthenticationHeader() {
  // 1
  $header = base64_encode(json_encode([
                 'alg' => 'ES256',
                 'kid' => AUTH_KEY_ID
            ]));

  // 2
  $claims = base64_encode(json_encode([
                 'iss' => TEAM_ID,
                 'iat' => time()
            ]));

  // 3
  $pkey = openssl_pkey_get_private('file://' . AUTH_KEY_PATH);
  openssl_sign("$header.$claims", $signature, $pkey, 'sha256');

  // 4
  $signed = base64_encode($signature);

  // 5
  return "$header.$claims.$signed";
}

The preceding code takes care of generating the needed JWT authentication header. Breaking it down:

  1. You specify that the encryption algorithm (alg) is using the SHA-256 hash algorithm and that the key identifier (kid) is the 10-character identifier from your p8 file.
  2. Next, you’ll generate the claims payload by specifying the issuer (iss) using your 10-character Team ID, obtained from your developer account (https://apple.co/2tXpJ2m), along with the issue time (iat), when the JWT was generated, in terms of the number of seconds since the epoch, in UTC.
  3. You read your p8 auth key file and digitally sign the header and claim into $signature.
  4. You take your digitally signed $signature and encode it using base 64.
  5. Finally, you wrap it up by concatenating all three pieces, which you’ll pass down to the Authentication header.

The only signature algorithm that Apple accepts is the ES256 algorithm. Don’t try to sign the payload with any other algorithm or Apple will send an InvalidProviderToken (403) response to your request.

You should generate a new authentication header at the start of every group of pushes that you’ll be sending. Additionally, these generated tokens last for about an hour; any request sent with a token older than an hour will be rejected by Apple with a ExpiredProviderToken (403) error.

You’ll notice that nothing here actually encrypts the header. JWTs are signed and encoded, but they do nothing to provide security for sensitive data.

Now that you know what tokens you need to send and how to sign your request, you’ll open an HTTP/2 session to the APNs. Add the following function to the file:

function sendNotifications($debug) {
  $ch = curl_init();
  curl_setopt($ch, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_2_0);
  curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($GLOBALS['payload']));
  curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
  curl_setopt($ch, CURLOPT_HTTPHEADER, [
      'apns-topic: ' . BUNDLE_ID,
      'authorization: bearer ' . generateAuthenticationHeader(),
      'apns-push-type: alert'
  ]);
}

Notice how you’re explicitly telling libcurl that it should use the HTTP/2 protocol for this connection while passing your JWT as the authorization header. At this point, the session is open and signed, so you just need to loop through each token and send your payload across. Add the following code to the end of sendNotifications():

$removeToken = $GLOBALS['db']->prepare('DELETE FROM apns WHERE token = ?');
$server = $debug ? 'api.development' : 'api';
$tokens = tokensToReceiveNotification($debug);

This creates a PDO statement to remove a single token from the database, determines which APNs to connect to, and then queries all of the tokens using your previously defined function. You’re almost done, keep going!

Add this final piece of PHP code inside sendNotifications():

foreach ($tokens as $token) {
  // 1
  $url = "https://$server.push.apple.com/3/device/$token";
  curl_setopt($ch, CURLOPT_URL, "{$url}");

  // 2
  $response = curl_exec($ch);
  if ($response === false) {
    echo("curl_exec failed: " . curl_error($ch));
    continue;
  }

  // 3
  $code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
  if ($code === 400 || $code === 410) {
    $json = @json_decode($response);
    if ($json->reason === 'BadDeviceToken') {
      $removeToken->execute([$token]);
    }
  }
}

curl_close($ch);

Here’s what’s happening in the preceding code:

  1. You construct the actual URL to be used to send a notification to this token.
  2. You try to submit the request to Apple over the cURL HTTP/2 Session you previously opened.
  3. If Apple said something was wrong, and the reason was that the token was bad (BadDeviceToken), you remove this token from your database using the $removeToken PDO statement you prepared earlier. A token will become invalid if the user uninstalls your app.

Now all that you need to do is call the function! Add this code to the end of the file:

sendNotifications(true); // Development (Sandbox)
sendNotifications(false); // Production
?>

Depending on the way your development cycle works, you’ll need to determine which type of tokens you’re sending your push notifications to. At the start of development, when you’re the only user, you’ll just call sendNotifications(true). Once you have some beta testers, you’ll have to start calling it again with false so they get notifications. There will then be a period when both have to go out.

What happens when you finally push your app to the App Store? That’s again dependent on your development flow.

While you continue to develop some other awesome features, you’ll probably continue to send both Sandbox and Production notifications during your development and release cycle.

To run a PHP script, simply prepend the script name with php on the command line, like so:

$ php sendPushes.php

A PHP solution should support most server types. Another option would be using Node.js for your server, in which case you’re not forced to add a PHP solution. There are multiple options on GitHub that you can use. For example, if you install the apn and pg modules using Terminal:

$ npm install apn --save
$ npm install pg --save

Your Node.js server could look a lot like this:

#!/usr/bin/env node

var apn = require('apn');
const { Client } = require('pg')

const options = {
  token: {
    key: '/full/path/to/AuthKey_keyid.p8',
    keyId: '',
    teamId: ''
  },
  production: false
}

const apnProvider = new apn.Provider(options);

var note = new apn.Notification();
note.expiry = Math.floor(Date.now() / 1000) + 3600; // 1 hour
note.badge = 3;
note.sound = "default";
note.alert = "Your alert here";
note.topic = "com.raywenderlich.PushNotifications";

const client = new Client({
  user: 'apns',
  host: 'localhost',
  database: 'apns',
  password: 'apns',
  port: 5433
})

client.connect()

client.query('SELECT DISTINCT token FROM tokens WHERE debug = true', (err, res) => {
  client.end()

  const tokens = res.rows.map(row => row.token)

  apnProvider.send(note, tokens).then( (response) => {
    // response.sent has successful pushes
    // response.failed has error details
  });
})

But they disabled pushes!

You’ll notice that you remove tokens from your database when a failure occurs. There’s nothing there to handle the case where your user disables push notifications, nor should there be. Your user can toggle the status of push notifications at any time, and nothing requires them to go into the app to do that, since it’s done from their device’s Settings. Even if push notifications are disabled, it’s still valid for Apple to send the push. The device simply ignores the push when it arrives.

Note: Do not try detecting when pushes are off and removing the token. If the end-user goes into Settings and turns them back on, but doesn’t run your app again for a while, they’ll miss all the notifications they are expecting to receive!

Key points

  • You’ll need to have a SQL server available to store device tokens.
  • You’ll need an API available to your iOS app to store and delete tokens.
  • Do not use native Foundation network commands to send push notifications. Apple will consider that as a denial of service attack due to the repetitive opening and closing of connections.
  • There are many options available for building your push server. Choose the one(s) that work best for your skillset.

Where to go from here?

As stated, if you are interested in learning more about the Vapor framework, you can check out our great set of videos (bit.ly/3n8bRLH) as well as our Vapor book, Server-Side Swift with Vapor (bit.ly/399PhxP).

There’s also the Vapor documentation at docs.vapor.codes as well as a great community on Discord via the Vapor channel. To join the Discord group simply point your browser to vapor.team.

In the next chapter, “Expanding the Application,” you’ll configure your iOS app to talk to the server that you just configured.

Have a technical question? Want to report a bug? You can ask questions and report bugs to the book authors in our official book forum here.
© 2026 Kodeco Inc.