Push Notifications
Configure notification permissions, the local inbox, Expo push tokens, and optional server sending.
The Expo template includes the client-side notification experience: permission priming, Android channels, Expo push token retrieval, foreground handling, a small in-app inbox, and tap-to-open deep links. It does not ship a complete push-sending backend.
Real remote push testing needs a native development, preview, or production
build. Expo documents that expo-notifications remote push support is not
available in Expo Go on Android from SDK 53 onward, and this template uses
Expo SDK 54.
What it provides
| Surface | Status | Notes |
|---|---|---|
NotificationsProvider | Integrated | Lazy-loads expo-notifications, survives web or builds where the module is missing, and exposes useNotifications() |
app/permissions.tsx | Integrated | Primes notification, photo, camera, and tracking permissions from one screen |
app/notifications.tsx | Integrated | Shows permission status, the current Expo push token, cached notification inbox entries, and a clear action |
| Android channels | Integrated | Registers default and alerts channels before token requests |
| Notification tap routing | Integrated | Reads data.url from the notification response and forwards it through openDeepLink() |
| Server sending | Not wired | The client does not persist tokens; the optional send-test-push function expects a missing device_tokens table |
lib/notifications.tsx wraps the native module in a lazy require. If
expo-notifications cannot load, isNativePushAvailable is false and the
request action returns a friendly error instead of crashing the app.
Client flow
Register handlers
On mount, NotificationsProvider installs a foreground handler that shows the
alert, does not play a sound, and does not set a badge. It also subscribes to
incoming notifications and notification responses.
Create Android channels
On Android, the provider creates:
default -> General, default importance
alerts -> Alerts, high importanceAndroid 13+ requires notification permission, and the system prompt depends on channels existing before a push token request.
Request permission and token
requestPermission() calls requestPermissionsAsync(). When permission is
granted, it calls getExpoPushTokenAsync() and stores the returned token in
React state for the current session.
Keep a local inbox
Foreground notifications are normalized to { id, title, body, url, createdAt }, prepended to the inbox, capped at 50 entries, and persisted with
lib/cache.ts under cache.notifications.inbox.v1.
Forward taps to routes
If a notification response includes data.url, the provider calls:
openDeepLink(url, "/notifications");Only routes allowed by lib/deep-links.ts are opened; otherwise the fallback
notifications screen is used.
What is not wired
The client retrieves an Expo push token, but it never writes that token to
Supabase. The shipped Supabase function send-test-push is an optional APNs
debug artifact copied from the native stack. It queries:
public.device_tokensNo migration creates that table, and the Expo client never registers native APNs tokens. Treat that function as a starting point, not as a working Expo push backend.
If you keep the APNs function, update its fallback deep link too:
send-test-push currently falls back to soar:///notifications, while this
template's default schemes are soar-starter-expo-dev://,
soar-starter-expo-preview://, and soar-starter-expo://.
To actually send pushes
For the Expo push service path:
Add a token table
Create an owner-scoped table for Expo push tokens:
create table public.device_push_tokens (
id uuid primary key default gen_random_uuid(),
user_id uuid not null references auth.users(id) on delete cascade,
platform text not null,
expo_push_token text not null,
created_at timestamptz not null default now(),
updated_at timestamptz not null default now(),
unique (user_id, expo_push_token)
);
alter table public.device_push_tokens enable row level security;Add insert/select/delete policies scoped to auth.uid() = user_id.
Persist tokens after permission
After getExpoPushTokenAsync() succeeds, upsert the token for the signed-in
user. Delete stale rows on sign-out if you do not want a shared device to keep
receiving account-specific messages.
Send through Expo
Create a Supabase Edge Function or your own API route that reads
device_push_tokens, calls Expo's push API, and removes invalid receipts. Keep
server auth and any provider secrets outside the app bundle.
If you prefer direct APNs/FCM, extend the existing send-test-push artifact:
add a real device_tokens migration, collect native device tokens instead of
Expo push tokens, and add the Android FCM path before relying on it in a shipped
app.
Verify
- Install a native development build on a physical device.
- Open Permissions and request Notifications.
- Open Notifications and confirm the status changes to
granted. - Confirm an Expo push token appears. If it remains blank, check native credentials and the runtime logs.
- Send a test push with
data.urlset to an allowed route such as/notificationsor/refer, then confirm tapping the notification opens that route.
