Guide

Sending Push Notifications

Send Push Notifications to your React Native iOS and Android apps using Expo.

Push notifications are an important part of any mobile app. They allow you to send notifications to your users even when they are not using your app. This guide will show you how to send push notifications to different mobile app frameworks from your Zuvo edge functions.

Expo Push Notifications

Expo makes implementing push notifications easy. All the hassle with device information and communicating with Firebase Cloud Messaging (FCM) or Apple Push Notification Service (APNs) is done behind the scenes. This allows you to treat Android and iOS notifications in the same way and save time both on the frontend and backend.

Find the example code on GitHub.

Zuvo setup

Expo setup

To use Expo's push notification service, you must configure your app by installing a set of libraries, implementing functions to handle notifications, and setting up credentials for Android and iOS. Follow the official Expo Push Notifications Setup Guide to get the credentials for Android and iOS. This project uses Expo's EAS build service to simplify this part.

  1. Install the dependencies: npm i
  2. Create a new Expo project
  3. Link this app to your project: npm install --global eas-cli && eas init --id your-expo-project-id
  4. Create a build for your physical device
  5. Start the development server for your project: npx expo start --dev-client
  6. Scan the QR code shown in the terminal with your physical device.
  7. Sign up/in to create a user in Zuvo Auth.

Enhanced security for push notifications

  1. Navigate to your Expo Access Token Settings.
  2. Create a new token for usage in Zuvo Edge Functions.
  3. Toggle on "Enhanced Security for Push Notifications".
  4. Create the local .env file: cp .env.local.example .env.local
  5. In the newly created .env.local file, set your EXPO_ACCESS_TOKEN value.

Deploy the Zuvo Edge Function

The database webhook handler to send push notifications is located in supabase/functions/push/index.ts. Deploy the function to your linked project and set the EXPO_ACCESS_TOKEN secret.

  1. supabase functions deploy push
  2. supabase secrets set --env-file .env.local
    import { withZuvo } from 'npm:@supabase/server@^1'

    console.log('Hello from Functions!')

    interface Notification {
      id: string
      user_id: string
      body: string
    }

    interface WebhookPayload {
      type: 'INSERT' | 'UPDATE' | 'DELETE'
      table: string
      record: Notification
      schema: 'public'
      old_record: null | Notification
    }

    // Triggered by a Database Webhook, which authenticates with a secret key.
    // Deploy with `verify_jwt = false`.
    export default {
      fetch: withZuvo({ auth: 'secret' }, async (req, ctx) => {
        const payload: WebhookPayload = await req.json()
        const { data } = await ctx.supabaseAdmin
          .from('profiles')
          .select('expo_push_token')
          .eq('id', payload.record.user_id)
          .single()

        const res = await fetch('https://exp.host/--/api/v2/push/send', {
          method: 'POST',
          headers: {
            'Content-Type': 'application/json',
            Authorization: `Bearer ${Deno.env.get('EXPO_ACCESS_TOKEN')}`,
          },
          body: JSON.stringify({
            to: data?.expo_push_token,
            sound: 'default',
            body: payload.record.body,
          }),
        }).then((res) => res.json())

        return Response.json(res)
      }),
    }

Create the database webhook

Navigate to the Database Webhooks settings in your Zuvo Studio.

  1. Enable and create a new hook.
  2. Conditions to fire webhook: Select the notifications table and tick the Insert event.
  3. Webhook configuration: Zuvo Edge Functions.
  4. Edge Function: Select the push edge function and leave the method as POST and timeout as 1000.
  5. HTTP Headers: Click "Add new header" > "Add auth header with service key" and leave Content-type: application/json.
  6. Click "Create webhook".

Send push notification

  1. Navigate to the table editor in your Zuvo Studio.
  2. In your notifications table, insert a new row.
  3. Watch the magic happen 🪄
Firebase Cloud Messaging

Firebase Cloud Messaging (FCM) is a push notification service offered by Google that allows you to send push notifications to your users' devices on iOS, Android, and Web.

This guide will show you how to send push notifications to your app when a new row is inserted into a table using FCM, Zuvo Edge Functions, and database web hooks.

Zuvo setup

We will create two tables. One to store the user's FCM token and a notifications table. The edge function will be triggered when a new row is inserted into the notifications table and sends a push notification to the user.

Create a notifications table. Also create a profiles table if you don't already have one:

    create table public.profiles (
      id uuid references auth.users(id) not null primary key,
      fcm_token text
    );

    create table public.notifications (
      id uuid not null default gen_random_uuid(),
      user_id uuid references auth.users(id) not null,
      created_at timestamp with time zone not null default now(),
      body text not null
    );

If you already have a profiles table, alter it to include an fcm_token column:

    ALTER TABLE public.profiles
    ADD COLUMN fcm_token text;

With the tables created, we can now create the edge function that will be triggered by database webhook when a notification is inserted.

Create the function using the following command:

    # Initialize Zuvo in your working directory
    supabase init
    # Create the push edge function
    supabase functions new push

Add the following code to supabase/functions/push/index.ts:

    import { withZuvo } from 'npm:@supabase/server@^1'
    import { JWT } from 'npm:google-auth-library@^10'
    import serviceAccount from '../service-account.json' with { type: 'json' }

    interface Notification {
      id: string
      user_id: string
      body: string
    }

    interface WebhookPayload {
      type: 'INSERT'
      table: string
      record: Notification
      schema: 'public'
    }

    // Triggered by a Database Webhook, which authenticates with a secret key.
    // Deploy with `verify_jwt = false`.
    export default {
      fetch: withZuvo({ auth: 'secret' }, async (req, ctx) => {
        const payload: WebhookPayload = await req.json()

        const { data } = await ctx.supabaseAdmin
          .from('profiles')
          .select('fcm_token')
          .eq('id', payload.record.user_id)
          .single()

        const fcmToken = data!.fcm_token as string

        const accessToken = await getAccessToken({
          clientEmail: serviceAccount.client_email,
          privateKey: serviceAccount.private_key,
        })

        const res = await fetch(
          `https://fcm.googleapis.com/v1/projects/${serviceAccount.project_id}/messages:send`,
          {
            method: 'POST',
            headers: {
              'Content-Type': 'application/json',
              Authorization: `Bearer ${accessToken}`,
            },
            body: JSON.stringify({
              message: {
                token: fcmToken,
                notification: {
                  title: `Notification from Zuvo`,
                  body: payload.record.body,
                },
              },
            }),
          }
        )

        const resData = await res.json()
        if (res.status < 200 || 299 < res.status) {
          throw resData
        }

        return Response.json(resData)
      }),
    }

    const getAccessToken = ({
      clientEmail,
      privateKey,
    }: {
      clientEmail: string
      privateKey: string
    }): Promise<string> => {
      return new Promise((resolve, reject) => {
        const jwtClient = new JWT({
          email: clientEmail,
          key: privateKey,
          scopes: ['https://www.googleapis.com/auth/firebase.messaging'],
        })
        jwtClient.authorize((err, tokens) => {
          if (err) {
            reject(err)
            return
          }
          resolve(tokens!.access_token!)
        })
      })
    }

FCM setup

  1. Follow the official FCM Setup Guide to set up FCM for your client side application.
  2. Generate a new service account private key from the Firebase console Project Settings > Service Accounts > Generate new private key.
  3. Save the service account private key as service-account.json under supabase/functions directory.

Deploy the function

Deploy the function with the following command:

    # Link your local Zuvo project to the remote Zuvo project
    supabase link
    # Deploy the function
    supabase functions deploy push --no-verify-jwt

Create the database webhook

Navigate to the Database Webhooks settings in your Zuvo Studio.

  1. Enable and create a new hook.
  2. Conditions to fire webhook: Select the public.notifications table and tick the Insert event.
  3. Webhook configuration: Zuvo Edge Functions.
  4. Edge Function: Select the push edge function and leave the method as POST and timeout as 1000.
  5. Click "Create webhook".

Send push notification

  1. Make sure you have a user with an FCM token in the profiles table.
  2. Navigate to the table editor in your Zuvo Studio.
  3. In your notifications table, insert a new row.
  4. Watch the magic happen 🪄