Install
$ agentstack add skill-ampli-group-agentic-mobile-blueprint-push-notifications ✓ scanned · ✓ verified, works with Claude Code, Cursor, and more.
Security review
✓ PassedNo issues found. Passed automated security review. · v0.1.0 How review works →
- ✓ Prompt-injection patterns
- ✓ Secret / credential exfiltration
- ✓ Dangerous shell & filesystem operations
- ✓ Untrusted network calls
- ✓ Known-malicious package signatures
What it can access
- ● Network access Used
- ✓ Filesystem access No
- ✓ Shell / process execution No
- ● Environment & secrets Used
- ✓ Dynamic code execution No
From automated source analysis of v0.1.0. “Used” means the capability is present in the source — more access means more to trust, not that it’s unsafe.
Verified badge
Passed review? Show it. Paste this badge into your README, it links to the public security report.
Reliability & compatibility
Declared compatibility
Compatibility is declared by the source manifest. End-to-end runtime verification is coming, see below.
We're building live execution health for every listing: tool-call success rate, median latency, uptime, and last-checked timestamps, measured, not self-reported. It isn't live yet, so we don't show numbers we can't stand behind.
How agent discovery & health will work →About
Push Notifications
Stack
- expo-notifications — handles token registration, foreground/background receipt on device
- Expo Push Notification Service (EPN) — proxies to APNs (Apple) and FCM (Google); free, no quota
- Supabase Edge Function — sends notifications server-side via Expo Push API
- APNs key — required for iOS production builds (configured in Expo dashboard)
- FCM key — required for Android (configured in Expo dashboard)
You do NOT call APNs or FCM directly. Expo's push service handles the routing.
Step 1: Install
cd mobile
npx expo install expo-notifications expo-device expo-constants
Add to app.json:
{
"expo": {
"plugins": [
[
"expo-notifications",
{
"icon": "./assets/notification-icon.png",
"color": "#ffffff",
"sounds": ["./assets/notification-sound.wav"]
}
]
]
}
}
Step 2: Register for Push Token
Add to your app initialization (e.g. app/_layout.tsx):
import * as Notifications from "expo-notifications";
import * as Device from "expo-device";
import Constants from "expo-constants";
// Handle notifications when app is in foreground
Notifications.setNotificationHandler({
handleNotification: async () => ({
shouldShowAlert: true,
shouldPlaySound: true,
shouldSetBadge: false,
}),
});
export async function registerForPushNotifications(): Promise {
if (!Device.isDevice) {
console.warn("Push notifications require a physical device");
return null;
}
const { status: existingStatus } = await Notifications.getPermissionsAsync();
let finalStatus = existingStatus;
if (existingStatus !== "granted") {
const { status } = await Notifications.requestPermissionsAsync();
finalStatus = status;
}
if (finalStatus !== "granted") {
return null;
}
const projectId = Constants.expoConfig?.extra?.eas?.projectId;
const token = (await Notifications.getExpoPushTokenAsync({ projectId })).data;
return token; // looks like: ExponentPushToken[xxxxx]
}
projectId comes from eas.json or app.json → expo.extra.eas.projectId. Find it in expo.dev under your project settings.
Step 3: Store Token in Supabase
After registration, save the token to your database:
// lib/notifications.ts
import { supabase } from "./supabase";
import { registerForPushNotifications } from "./pushNotifications";
export async function syncPushToken(userId: string) {
const token = await registerForPushNotifications();
if (!token) return;
await supabase.from("push_tokens").upsert(
{ user_id: userId, token, platform: Platform.OS },
{ onConflict: "token" }
);
}
Database schema:
create table push_tokens (
id uuid primary key default gen_random_uuid(),
user_id uuid references auth.users not null,
token text unique not null,
platform text not null, -- ios | android
created_at timestamptz default now()
);
alter table push_tokens enable row level security;
create policy "users manage own tokens" on push_tokens
using (auth.uid() = user_id)
with check (auth.uid() = user_id);
Call syncPushToken after login:
supabase.auth.onAuthStateChange((event, session) => {
if (event === "SIGNED_IN" && session?.user) {
syncPushToken(session.user.id);
}
});
Step 4: Send Notifications from Edge Function
// supabase/functions/send-notification/index.ts
import { createClient } from "npm:@supabase/supabase-js@2";
const EXPO_PUSH_URL = "https://exp.host/--/api/v2/push/send";
interface PushMessage {
to: string;
title: string;
body: string;
data?: Record;
sound?: "default" | null;
badge?: number;
}
export async function sendPushNotification(
userIds: string[],
notification: Omit
) {
const supabase = createClient(
Deno.env.get("PUBLIC_SUPABASE_URL")!,
Deno.env.get("SUPABASE_SERVICE_ROLE_KEY")!
);
const { data: tokens } = await supabase
.from("push_tokens")
.select("token")
.in("user_id", userIds);
if (!tokens?.length) return;
const messages: PushMessage[] = tokens.map(({ token }) => ({
to: token,
sound: "default",
...notification,
}));
// Expo accepts up to 100 messages per request
const chunks = chunk(messages, 100);
for (const batch of chunks) {
await fetch(EXPO_PUSH_URL, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(batch),
});
}
}
function chunk(arr: T[], size: number): T[][] {
return Array.from({ length: Math.ceil(arr.length / size) }, (_, i) =>
arr.slice(i * size, i * size + size)
);
}
Step 5: Configure APNs Key (iOS Production)
Required for iOS push notifications in production builds.
- developer.apple.com/account/resources/authkeys → +
- Name:
Expo Push, enable Apple Push Notifications service (APNs) - Download the
.p8file — download once only - Note the Key ID and your Team ID (top right of developer.apple.com)
Upload to Expo:
- expo.dev → Your project → Credentials → iOS
- Push Notifications → Upload the
.p8key, enter Key ID and Team ID
Or via CLI:
eas credentials --platform ios
# Select: Push Notifications → Upload APNs key
Step 6: Configure FCM (Android Production)
- Go to console.firebase.google.com
- Create project (or use existing) → Add app → Android
- Enter your package name (
com.yourorg.appname) - Download
google-services.json→ place inmobile/google-services.json - Project settings → Cloud Messaging → copy Server key
Add to app.json:
{
"expo": {
"android": {
"googleServicesFile": "./google-services.json"
}
}
}
Upload FCM key to Expo:
eas credentials --platform android
# Select: Push Notifications → Upload FCM key
Handle Notification Taps
import { useEffect, useRef } from "react";
import * as Notifications from "expo-notifications";
import { router } from "expo-router";
export function useNotificationNavigation() {
const responseListener = useRef();
useEffect(() => {
// App opened from notification
responseListener.current = Notifications.addNotificationResponseReceivedListener(
(response) => {
const data = response.notification.request.content.data;
if (data?.screen) {
router.push(data.screen as string);
}
}
);
return () => {
responseListener.current?.remove();
};
}, []);
}
Pass data: { screen: "/orders/123" } when sending to deep-link on tap.
Testing
# Test with Expo CLI (dev build or Expo Go)
npx expo push:send --to "ExponentPushToken[xxx]" --title "Test" --body "Hello"
# Or use the Expo push tool
open https://expo.dev/notifications
Physical device required for testing — simulator/emulator does not receive push notifications.
Gotchas
Notifications work in Expo Go but not in production build — APNs key not uploaded to Expo. Run eas credentials --platform ios and verify Push Notifications key is configured.
Android notifications not delivered — google-services.json missing or wrong package name. Verify package in app.json matches Firebase project.
Token is null on simulator — Expected. Push tokens only work on physical devices. Check with Device.isDevice.
"DeviceNotRegistered" error — Token is stale (user uninstalled/reinstalled app). Delete the token from push_tokens table when this error is returned by Expo Push API.
Notification not shown when app is in foreground — Set shouldShowAlert: true in setNotificationHandler. By default foreground notifications are silent.
iOS notification permission denied — You can only ask for permission once. If denied, user must go to Settings manually. Always explain why before requesting.
Badge count not clearing — Call Notifications.setBadgeCountAsync(0) on app foreground:
AppState.addEventListener("change", (state) => {
if (state === "active") Notifications.setBadgeCountAsync(0);
});
Source & license
This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.
- Author: Ampli-Group
- Source: Ampli-Group/agentic-mobile-blueprint
- License: MIT
Install and usage instructions live in the source repository linked above.
Reviews
No reviews yet, be the first.
Write a review
Versions
- v0.1.0 Imported from the upstream source.