🎉 New! Web Push Notifications for Chatkit. Learn more in our latest blog post.
Hide
Products
chatkit_full-logo

Extensible API for in-app chat

channels_full-logo

Build scalable realtime features

beams_full-logo

Programmatic push notifications

Developers

Docs

Read the docs to learn how to use our products

Tutorials

Explore our tutorials to build apps with Pusher products

Support

Reach out to our support team for help and advice

Sign in
Sign up

Implement push notifications with Chatkit and Vapor - Part 1: Build the backend

  • Christopher Batin

July 27th, 2019
You will need Xcode 10 and Vapor 3.

Introduction

In this tutorial we will be creating a new iOS chat app using Chatkit with a Vapor backend that handles creation of our users. We will also implement push notifications using Pusher Beams. At the time of writing there is no official Chatkit server SDK so we will be interacting directly with the Pusher Chatkit API.

Prerequisites

  • Understanding of Vapor - Please complete:
  • Xcode 10 and the latest Xcode command line tools.
  • MacOS
  • Vapor 3.0 - Install instructions here.
  • An understanding of iOS development and Xcode environment.

Setup

Creating our Vapor project

From a terminal in your working directory enter the following command to create your Vapor application.

     $ vapor new ChatKingServer
     $ cd ChatKingServer

Now we will build your application before opening it in Xcode. Remember your first build could take some time to complete.

    $ vapor build

Now open your project in Xcode. Remember to open using Xcode you must run the following command in terminal:

    $ vapor xcode -y

Also remember to change the run scheme to be the “run” scheme in case it is not already set to this.

Add dependencies

Open your Package.swift file and replace the contents with:

    // ./Package.swift
    // swift-tools-version:4.0
    import PackageDescription
    let package = Package(
        name: "ChatKingServer",
        dependencies: [
            // 💧 A server-side Swift web framework.
            .package(url: "https://github.com/vapor/vapor.git", from: "3.0.0"),
            .package(url: "https://github.com/PerfectlySoft/Perfect-Crypto.git", from: "3.0.0"),
            // 🔵 Swift ORM (queries, models, relations, etc) built on SQLite 3.
            .package(url: "https://github.com/vapor/fluent-sqlite.git", from: "3.0.0")
        ],
        targets: [
            .target(name: "App", dependencies: ["FluentSQLite", "Vapor", "PerfectCrypto"]),
            .target(name: "Run", dependencies: ["App"]),
            .testTarget(name: "AppTests", dependencies: ["App"])
        ]
    )

Note: Leave the line swift-tools-version:X.X as it was when you opened the file.

Now close Xcode and run the following commands to install the package and reopen Xcode:

    $ swift package update
    $ vapor xcode -y

Set up the SQLite database

We already have our SQLite dependency installed as this comes as default. To configure it we need to make some changes to our configure.swift.

    // Sources/App/configure.swift
    import FluentSQLite
    import Vapor
    /// Called before your application initializes.
    public func configure(_ config: inout Config, _ env: inout Environment, _ services: inout Services) throws {
        /// Register providers first
        try services.register(FluentSQLiteProvider())

        /// Register routes to the router
        let router = EngineRouter.default()
        try routes(router)
        services.register(router, as: Router.self)

        /// Register middleware
        var middlewares = MiddlewareConfig() // Create _empty_ middleware config
        /// middlewares.use(FileMiddleware.self) // Serves files from `Public/` directory
        middlewares.use(ErrorMiddleware.self) // Catches errors and converts to HTTP response
        services.register(middlewares)

        // Configure a SQLite database
        /// Register the configured SQLite database to the database config.
        var databases = DatabasesConfig()
        try databases.add(database: SQLiteDatabase(storage: .memory), as: .sqlite)
    //    try databases.add(database: SQLiteDatabase(storage: .file(path: "db.sqlite")),
    //                      as: .sqlite)
        services.register(databases)
    }

The important section here is the line:

    try databases.add(database: SQLiteDatabase(storage: .memory), as: .sqlite)

We’ve set the database to use the memory storage. This means that every time we restart our server the database will be reset. This is great for debugging. If we want persistent storage we can change this line to be:

    try databases.add(database: SQLiteDatabase(storage: .file(path: "db.sqlite")), 
                      as: .sqlite)

You will also notice that we have added our user migration in here as well ready for creating our model.

Create models

User model

We need to create a new model for our user. Close Xcode and open terminal at your working directly and enter the following commands to create a new file and reopen Xcode.

    $ touch Sources/App/Models/User.swift
    $ vapor xcode -y

Open your User.swift file and add the following:

    // Sources/App/Models/User.swift
    import Vapor
    import FluentSQLite

    final class User: Codable {
        var id: UUID?
        var username: String
        var name: String?

        init(username: String) {
            self.username = username
            self.name = username
        }

        init(id: UUID?, username: String) {
            self.id = id
            self.username = username
            self.name = username
        }
    }

    extension User: Content {}
    extension User: SQLiteUUIDModel {
      static func prepare(on connection: SQLiteConnection)
          -> Future<Void> {
              // 1
              return Database.create(self, on: connection) { builder in
                  // 2
                  try addProperties(to: builder)
                  // 3
                  builder.unique(on: \.username)
              }
      }
    }
    extension User: Migration {}
    extension User: Parameter {}

Note: We have an optional name parameter. This is because the Chatkit API expects a name key in the JSON. We will just be assigning our username to this parameter however you could modify the init methods to handle a name parameter being passed in as well.

Here we are creating our user type. We are saying that the ID field for the database is a UUID that we will provide. We will be able to use this UUID to create our user on the Pusher platform as well. We have also provided a constraint on the username field and said that this must be unique. If we try to create a new row in the database with a name that already exists it will fail.

Room model

We now need to create a new model for our rooms. Close Xcode and open terminal at your working directly and enter the following commands to create a new file and reopen Xcode.

    $ touch Sources/App/Models/Room.swift
    $ vapor xcode -y

Open your Room.swift file and add the following:

    // Sources/App/Models/Room.swift
    import Vapor
    import FluentSQLite

    final class Room: Codable {
      var id: UUID?
      var name: String

      init(name: String) {
          self.name = name
      }

      init(id: UUID?, name: String) {
          self.id = id
          self.name = name
      }
    }

    extension Room: Content {}
    extension Room: SQLiteUUIDModel {
      static func prepare(on connection: SQLiteConnection)
          -> Future<Void> {
              // 1
              return Database.create(self, on: connection) { builder in
                  // 2
                  try addProperties(to: builder)
                  // 3
                  builder.unique(on: \.name)
              }
      }
    }
    extension Room: Migration {}
    extension Room: Parameter {}

The room model is very similar to our User model.

Add migrations

Now reopen your configure.swift file and below the line services.register(databases) add the following to configure our migrations.

    // Sources/App/configure.swift
    /// Configure migrations
    var migrations = MigrationConfig()
    migrations.add(model: User.self, database: .sqlite)
    migrations.add(model: Room.self, database: .sqlite)
    services.register(migrations)   

Set up Chatkit

Create our instance

Open your Pusher dashboard or create a free account by following this link, create a new Chatkit instance and name it ChatKing.

Once your instance is created go to your credentials tab. Make a note of your instance locator and your secret key you will need them later. You will also need to turn on your test token provider, we will need this for the second part of this tutorial.

Your instance locator is in the form:

    v1:us1:YOUR_INSTANCE_ID

Your secret key is in the form:

    API_KEY_ID:SECRET_KEY

You will need to make note of your all of these formats. You will need them for interacting with the API endpoints directly. Your test token endpoint has an example of its use.

Set up push notifications

Whilst we are in the dashboard we will also setup push notifications for our iOS application. Switch to the Push Notifications tab.

Note: Push Notifications in Chatkit is in beta at the time of writing this tutorial.

You will need to upload your signing key and provide your team ID in the APNS integration section. Follow the links provided in the dashboard if you are unsure of where these are.

Creating our controllers

Auth controller

The Chatkit API requires the requests to be signed with a JSON Web Token. We need to create a function that will create this token for us. Fortunately we have installed a package that will help with this, PerfectoCrypto. Let’s create a controller that will handle this, close Xcode and enter the following in terminal.

    $ touch Sources/App/Controllers/AuthController.swift
    $ vapor xcode -y

Open your newly created AuthController.swift and add the following:

    // ../Sources/App/Controllers/AuthController.swift
    import Vapor
    import Foundation
    import PerfectCrypto

    final class AuthController {
        // Creates a JWT token lasting 15 mins
        static func createJWToken(withUserId userId: String = "MasterShake") -> String {
            let timeStamp = Int(Date.init().timeIntervalSince1970)
            let tstPayload = ["instance": "YOUR_INSTANCE_ID",
                              "iss": "api_keys/YOUR_API_KEY_ID",
                              "exp": timeStamp + 86400, //24 hours
                              "iat": timeStamp,
                              "sub": userId,
                              "su":true] as [String : Any]
            let secret = "YOUR_SECRET_KEY"
            guard let jwt1 = JWTCreator(payload: tstPayload) else {
                return ""
            }
            let token = try! jwt1.sign(alg: .hs256, key: secret)
            return token
        }  
    }

Replace the placeholders with your instance ID, API key and secret key that we made a note of earlier. This function will use the package we installed in order to create a token that we can add as BearerAuthorization in our requests with the API. The token will last for 24 hours from creation and importantly the su key gives admin access. We can also pass in a userId to this method, this allows the Chatkit API to know the user who signed the token.

Create the user controller

We need to create a new controller for handling of user creation. Close Xcode and open terminal at your working directly and enter the following commands to create a new file and reopen Xcode.

    $ touch Sources/App/Controllers/UserController.swift
    $ vapor xcode -y

Open your UserController.swift file and add the following:

    // ../Sources/App/Controllers/UserController.swift
    import Vapor
    import Foundation
    import PerfectCrypto
    import FluentSQLite

    struct UserController: RouteCollection {
        // 1
        func boot(router: Router) throws {
            let usersRoute = router.grouped("api", "users")
            usersRoute.post("new", use: createHandler)
            usersRoute.post("login", use: find)
        }

        // 2
        func find(_ req: Request) throws -> Future<User> {
            return try req.content.decode(User.self).flatMap({ user in
                return User.query(on: req).filter(\.username == user.username).first().map(to: User.self, { user in
                    guard let user = user else {
                        throw Abort(.notFound)
                    }
                    return user
                })
            })
        }
        // 3
        func createHandler(_ req: Request) throws -> Future<User> {
            return try req.content.decode(User.self).flatMap { user in
                let chatkitEndPoint = "https://us1.pusherplatform.io/services/chatkit/v2/YOUR_CHATKIT_INSTANCE_ID/users"
                guard let url = URL(string: chatkitEndPoint) else {
                    throw Abort.init(HTTPResponseStatus.internalServerError)
                }
                user.id = UUID.init()
                let newUser = user.create(on: req)
                newUser.save(on: req).whenSuccess({ _ in
                    let bearer = BearerAuthorization.init(token: AuthController.createJWToken())
                    _ = try! req.client().post(url) { post in
                        post.http.headers.bearerAuthorization = bearer
                        post.http.headers.add(name: HTTPHeaderName.contentType.description, value: "application/json")
                        try post.content.encode(User.init(id: user.id, username: user.username))
                    }
                })
                return newUser
            }
        }
    }
  1. This controller conforms to the RouteCollection and defines it’s routes within it. This is instead of using the routes.swift file.
  2. Our find method takes a name property and performs a search on the Users in the database. If it finds a user with the matching name it returns it in the response, if it doesn’t it sends a 404 status code. We will use this method to act as our login method. We’re keeping it simple with no password authentication.
  3. This method also takes a name property as a parameter and creates a new user. We start by creating our Chatkit end point, you will need to add your instance ID here. We then create a new UUID for our user and then attempt to create and save this user. We can only create the user if the name is unique. If we successfully save the user to our database we then create a JWT token and use this as our authorization token before we post the details of our user to the Chatkit endpoint to create our user.

Create the room controller

We need to create a new controller for handling of user creation. Close Xcode and open terminal at your working directly and enter the following commands to create a new file and reopen Xcode.

    $ touch Sources/App/Controllers/RoomController.swift
    $ vapor xcode -y

Open your RoomController.swift file and add the following:

    // ../Sources/App/Controllers/RoomController.swift
    import Vapor

    struct RoomController: RouteCollection {
        // 1
        func boot(router: Router) throws {
            let roomsRoute = router.grouped("api", "rooms")
            roomsRoute.post("new", "user", User.parameter, use: createHandler)
            roomsRoute.get(use: getAllHandler)
            roomsRoute.get(Room.parameter, use: getHandler)
        }
        // 2
        func createHandler(
            _ req: Request) throws -> Future<Room> {
            return try flatMap(to: Room.self, req.content.decode(Room.self), req.parameters.next(User.self)) { room, user in
                let chatkitEndPoint = "https://us1.pusherplatform.io/services/chatkit/v2/YOUR_CHATKIT_INSTANCE_ID/rooms"
                guard let url = URL(string: chatkitEndPoint) else {
                    throw Abort.init(HTTPResponseStatus.internalServerError)
                }
                guard let userId = user.id else {
                    throw Abort.init(HTTPResponseStatus.notFound)
                }
                let _ = User.find(userId, on: req).unwrap(or: Abort.init(HTTPResponseStatus.notFound))
                room.id = UUID.init()
                let newRoom = room.create(on: req)
                newRoom.save(on: req).whenSuccess({ _ in
                    let bearer = BearerAuthorization.init(token: AuthController.createJWToken(withUserId: userId.uuidString))
                    _ = try! req.client().post(url) { post in
                        post.http.headers.bearerAuthorization = bearer
                        post.http.headers.add(name: HTTPHeaderName.contentType.description, value: "application/json")
                        try post.content.encode(Room.init(name: room.name))
                    }
                })
                return newRoom
            }
        }
        // 3
        func getAllHandler(
            _ req: Request
            ) throws -> Future<[Room]> {
            return Room.query(on: req).all()
        }
        // 4
        func getHandler(_ req: Request) throws -> Future<Room> {
            return try req.parameters.next(Room.self)
        }
    }
  1. This controller conforms to the RouteCollection and defines it’s routes within it. This is instead of using the routes.swift file.
  2. This method takes a name property as a parameter and creates a new room. We start by creating our Chatkit end point, you will need to add your instance ID here. We then check that a user UUID has been passed in. We then check if a user exists with this UUID. Chatkit requires the authorization token to be signed with the ID of the user creating the room. We then create a new UUID for our room and then attempt to create and save this room. We can only create the room if the name is unique. If we successfully save the room to our database we then create a JWT token and use this as our authorization token before we post the details of our user to the Chatkit endpoint to create our user.
  3. We use this handler to get all of rooms we have created. This allows us to get all the rooms from our database if we wish rather than talking to Pusher directly.
  4. This request allows us to get a individual room.

Register routes

We need to register our collection of routes we created in our controllers above. Open your routes.swift and replace it’s contents with the following:

    import Vapor

    /// Register your application's routes here.
    public func routes(_ router: Router) throws {

        let roomsController = RoomController()
        try router.register(collection: roomsController)

        let usersController = UserController()
        try router.register(collection: usersController)

        router.post("auth", String.parameter) { req -> Token in
            let userId = try req.parameters.next(String.self)
            let userJWToken = AuthController.createJWToken(withUserId: userId)
            let jWToken = AuthController.createJWToken()
            return Token.init(access_token: userJWToken,
                              refresh_token: jWToken,
                              user_id: userId,
                              token_type: "access_token",
                              expires_in: 86400)
        }

        struct Token: Content {
            var access_token: String
            var refresh_token: String
            var user_id: String
            var token_type: String
            var expires_in: Int
        }
    }

Our final route allows us to get an auth token for a user Id from our client. This will allow us to connect to our Chatkit instance as an authenticated user. Notice the token structure defined at the bottom, this is the structure that the Pusher iOS SDK will be expecting in order to authenticate when we hit our auth URL.

Set up ngrok

We'll need to install ngrok to expose our local server to the internet. Follow the instructions on their ngrok's website to install it. Once you have it installed run the following command in terminal:

    $ ngrok http 8080

You may find the above command not work, depending upon where you have ngrok installed on your drive. In such a case, try the following command instead:

    $ ./ngrok http 8080

If everything goes as expected, you should see the following (or similar) within terminal:

It means that ngrok has opened up your localhost port 8080 to the internet and provided you with both a HTTP and HTTPS URLs to reach it.

Conclusion

In this part of the tutorial we’ve learnt how to create our Vapor server, create and save a unique user and room to the database and also interact with the Pusher API directly. We’ve also setup push notifications in our Pusher dashboard. In the second part of this tutorial we will create the iOS application that interacts with this server and the Pusher Chatkit SDK.

The source code for this tutorial can be found here.

Clone the project repository
  • Chat
  • iOS
  • Swift
  • Chatkit
  • Beams

Products

  • Channels
  • Chatkit
  • Beams

© 2019 Pusher Ltd. All rights reserved.

Pusher Limited is a company registered in England and Wales (No. 07489873) whose registered office is at 160 Old Street, London, EC1V 9BW.