Expo Push Notifications with Supabase and Entrig

Entrig
Ib Entrig Team

Expo’s managed workflow keeps you out of Xcode and Gradle for almost everything. Push notifications are one of the few features that still need native configuration, and wiring them by hand (device tokens, an Edge Function, FCM and APNs credentials) is exactly the kind of backend work an Expo app is trying to avoid.

Entrig handles that part through a config plugin. You describe a notification once in the dashboard, and a PostgreSQL trigger on your Supabase database delivers it. This tutorial builds three real notifications for a group chat app, covering the three recipient patterns you actually hit in production: one-to-one, one-to-many through a join table, and broadcast. No Edge Functions, no custom backend.


The schema

A minimal group chat app with four tables:

TableColumns
usersid, name, created_at
groupsid, name, created_by → users.id, created_at
group_membersgroup_id → groups.id, user_id → users.id, created_at
messagesid, content, group_id → groups.id, user_id → users.id, created_at

The three notifications we’re building:

  1. Member joins a group → notify the group owner (one-to-one)
  2. New message in a group → notify all members except the sender (one-to-many)
  3. New group created → notify all users except the creator (broadcast)

Set up the SDK

Expo uses the same @entrig/react-native package, wired up through a config plugin instead of manual native setup. Install it with expo install so the version matches your SDK:

npx expo install @entrig/react-native

Set a real bundle identifier in app.json (not Expo’s default com.anonymous.*), add a URL scheme for deeplinks, and register the Entrig config plugin:

{
  "expo": {
    "scheme": "myapp",
    "ios": {
      "bundleIdentifier": "com.yourcompany.yourapp"
    },
    "android": {
      "package": "com.yourcompany.yourapp"
    },
    "plugins": ["@entrig/react-native"]
  }
}

Generate the native projects. The plugin runs during prebuild and configures the AppDelegate, background modes, and push entitlements for you, so there is nothing to edit by hand:

npx expo prebuild
npx expo run:ios      # or: npx expo run:android

For iOS there is a one-time Apple setup: open the Apple Developer portal, select the bundle ID from your app.json, and enable the Push Notifications capability. Then open ios/YourApp.xcworkspace in Xcode and pick your Team under Signing and Capabilities so Xcode generates a provisioning profile with push enabled. Android needs no extra steps.

Initialize Entrig and register devices with Supabase Auth:

import Entrig from '@entrig/react-native';
import { supabase } from './lib/supabase';

// Call once at app startup
await Entrig.init({ apiKey: 'YOUR_ENTRIG_API_KEY' });

// Register on sign-in, unregister on sign-out
supabase.auth.onAuthStateChange(async (event, session) => {
  if (event === 'SIGNED_IN' && session?.user) {
    await Entrig.register(session.user.id);
  } else if (event === 'SIGNED_OUT') {
    await Entrig.unregister();
  }
});

The userId passed to Entrig.register() must be the Supabase Auth user ID (session.user.id). This is the ID Entrig uses to resolve which device to send to. See the React Native SDK docs for manual configuration and troubleshooting.


Configure the notifications

Each notification is created in the Entrig dashboard in three steps: Trigger (when), Users (who), Message (what). The three tabs below walk through each notification type.

When a user joins a group, the person who created that group should know. This is a one-to-one notification: one join event, one recipient, found by navigating through two foreign keys to reach the owner.

Trigger

  • Table: group_members

  • Event: INSERT

  • Condition: group_members.user_idgroups.created_by (row value)

    Skip the notification when the joiner is the owner. Apps typically add the creator to group_members at the moment the group is created, so without this guard the owner would get a “someone joined your group” push about themselves.

Users

  • Users table: users

  • Connection type: Direct

    Direct means the trigger event maps to a single user. Entrig follows foreign keys to find them.

  • Path: group_members.group_id → groups → created_by → users.id

    Entrig reads your schema and auto-detects this path. Select it from the list. It follows group_id to the groups table, reads created_by, and resolves that to the users table.

  • Recipient filters: none

Message

Add payload fields so the title and body can reference them:

  • groups.name: name of the group
  • users.name: name of the user who joined

Set the title and body:

FieldValue
TitleNew member in {{groups.name}}
Body{{users.name}} just joined your group

Set the deeplink so a tap lands on the group. Placeholders are rendered at send time, so the URL arrives with the real values:

myapp://chat/{{group_members.group_id}}?name={{groups.name}}

When a message is sent, every member of that group should receive a notification, except the sender. This uses an indirect connection through the group_members join table, with a recipient filter to exclude the sender using a row value comparison.

Trigger

  • Table: messages
  • Event: INSERT
  • Conditions: none

Users

  • Users table: users

  • Connection type: Indirect via Join Table

    Indirect means the trigger event maps to multiple users through a relationship table.

  • Path: messages.group_idgroup_members (match on group_id) → user_idusers.id

    Entrig takes the group_id from the new message row, finds all matching rows in group_members, and from each of those resolves user_id to a registered device.

  • Recipient filter: group_members.user_idmessages.user_id (row value)

    This is the exclude-sender step. Set the filter value source to row value and select messages.user_id. Entrig will skip any group_members row where the user_id matches the sender.

Message

Add payload fields:

  • groups.name: group name for the notification title
  • users.name: sender’s name
  • messages.content: the message text

Set the title and body:

FieldValue
Title{{groups.name}}
Body{{users.name}}: {{messages.content}}

Set the deeplink so a tap opens the group the message was sent in:

myapp://chat/{{messages.group_id}}?name={{groups.name}}

When a new group is created, every other user in the app should be notified so they can choose to join. This is a broadcast: the trigger table has no join path to users, so Entrig notifies everyone registered, filtered to exclude the creator.

Trigger

  • Table: groups
  • Event: INSERT
  • Conditions: none

Users

  • Users table: users

  • Connection type: Broadcast

    Broadcast notifies all users in the users table when the event fires. No join table needed.

  • Recipient filter: users.idgroups.created_by (row value)

    Set the filter value source to row value and select groups.created_by. This excludes the creator from the broadcast. They don’t need to be told about a group they just made.

Message

Add payload fields:

  • groups.name: the new group’s name
  • users.name: the creator’s name

Set the title and body:

FieldValue
TitleNew group: {{groups.name}}
Body{{users.name}} created a new group. Tap to join

Set the deeplink so a tap opens the new group:

myapp://chat/{{groups.id}}?name={{groups.name}}

Handle notification taps

All three notifications carry a deeplink of the form myapp://chat/<group-id>?name=<group-name>. Because you set scheme in app.json, Expo registers that URL scheme on both platforms during prebuild, so there is no Info.plist or AndroidManifest.xml editing to do.

Handle the tap with the useEntrigEvent hook and expo-router’s router. Put it in your root app/_layout.tsx so it is mounted for the whole app:

import { Stack, router } from 'expo-router';
import { useEntrigEvent } from '@entrig/react-native';

export default function RootLayout() {
  useEntrigEvent('opened', (event) => {
    if (!event.deeplink) return;
    const uri = new URL(event.deeplink);        // myapp://chat/<id>?name=<name>
    router.push({
      pathname: '/chat',
      params: {
        groupId: uri.pathname.replace('/', ''),
        groupName: uri.searchParams.get('name') ?? '',
      },
    });
  });

  return (
    <Stack>
      <Stack.Screen name="index" options={{ headerShown: false }} />
      <Stack.Screen name="home" options={{ headerShown: false }} />
      <Stack.Screen name="chat" options={{ headerShown: false }} />
    </Stack>
  );
}

new URL(...) works because the Supabase client already pulls in react-native-url-polyfill. The /chat route reads those params with useLocalSearchParams, the same values the app passes when opening a group from the list:

import { useLocalSearchParams } from 'expo-router';

const { groupId, groupName } = useLocalSearchParams<{ groupId: string; groupName: string }>();

If the app was fully closed when the notification was tapped, the tap does not fire opened. Read the launch notification once on startup with getInitialNotification():

import { useEffect } from 'react';
import Entrig from '@entrig/react-native';
import { router } from 'expo-router';

useEffect(() => {
  Entrig.getInitialNotification().then((notification) => {
    if (!notification?.deeplink) return;
    const uri = new URL(notification.deeplink);
    router.push({
      pathname: '/chat',
      params: {
        groupId: uri.pathname.replace('/', ''),
        groupName: uri.searchParams.get('name') ?? '',
      },
    });
  });
}, []);

Prefer to skip the parsing? Initialize with Entrig.init({ apiKey, autoOpenDeeplink: true }) and the SDK opens the deeplink URL for you on tap. With scheme set in app.json, expo-router receives that URL through its own linking and routes to the matching screen. The manual approach above gives you explicit control over navigation, which is why we use it here.


Test it

Push notifications require a real device, and a development build rather than Expo Go, because Expo Go cannot load the native push module. Build and install with npx expo run:ios or npx expo run:android, then trigger each notification by inserting a row from the Supabase dashboard or SQL editor:

-- Notification 1: member joins a group
INSERT INTO group_members (group_id, user_id)
VALUES ('your-group-id', 'joining-user-id');

-- Notification 2: new message
INSERT INTO messages (content, group_id, user_id)
VALUES ('Hey everyone!', 'your-group-id', 'sender-user-id');

-- Notification 3: new group created
INSERT INTO groups (name, created_by)
VALUES ('Weekend Plans', 'creator-user-id');

Each insert should deliver the notification to the right device within a few seconds.


The three notifications in this tutorial cover the full range of what Entrig supports: Direct for targeted one-to-one, Indirect for group-aware delivery, and Broadcast for app-wide announcements. Because the recipient logic lives in the dashboard, the same Expo app works for any use case: e-commerce orders, task assignments, social follows.

Full SDK reference at entrig.com/docs.


Read the Push Notifications for Supabase overview for a comparison of the manual setup versus Entrig.


Start free with Entrig and get this running on your own Supabase project in minutes. More at entrig.com.