convex-expo-push-notifications
SKILL.md
Expo Push Notifications with Convex
Complete guide for implementing push notifications in Expo React Native apps using @convex-dev/expo-push-notifications.
Setup Checklist
Before writing any code, verify:
- Physical device available (simulators do NOT support push notifications)
- Expo account with EAS project configured (
eas build:configure) - Convex project initialized
- For Android: Firebase project with
google-services.json - For iOS: Apple Developer account (EAS handles APNs credentials automatically)
SDK 53+ Breaking Change: Push notifications are NOT available in Expo Go on Android. Use a development build.
Installation
# Expo client packages
npx expo install expo-notifications expo-device expo-constants
# Convex component
npm install @convex-dev/expo-push-notifications
Convex Backend Setup
1. Register Component — convex/convex.config.ts
import { defineApp } from "convex/server";
import pushNotifications from "@convex-dev/expo-push-notifications/convex.config";
const app = defineApp();
app.use(pushNotifications);
export default app;
2. Initialize Component in Functions
import { PushNotifications } from "@convex-dev/expo-push-notifications";
import { components } from "./_generated/api";
const push = new PushNotifications(components.pushNotifications);
3. Core API Methods
| Method | Context | Purpose |
|---|---|---|
push.recordToken(ctx, { userId, pushToken }) |
mutation | Store device push token |
push.sendPushNotification(ctx, { userId, notification }) |
mutation | Send to one user/device |
push.sendPushNotificationBatch(ctx, { notifications }) |
mutation | Send to multiple users |
push.getStatusForUser(ctx, { userId }) |
query | Check token/notification status |
push.getNotification(ctx, { id }) |
query | Get delivery status |
push.getNotificationsForUser(ctx, { userId, limit }) |
query | List user's notifications |
push.removePushNotificationToken(ctx, { userId }) |
mutation | Remove token (logout) |
push.pauseNotificationsForUser(ctx, { userId }) |
mutation | Pause without removing token |
push.unpauseNotificationsForUser(ctx, { userId }) |
mutation | Resume paused notifications |
4. Notification Object Shape
{
title: "Message title",
body: "Message body",
data: { screen: "Chat", chatId: "123" }, // custom payload for deep linking
sound: "default",
priority: "high",
}
Critical Rules
- Always register component in
convex.config.ts— notconvex.json. Useapp.use(pushNotifications). - Always validate push tokens — check format starts with
ExponentPushToken[orExpoPushToken[. - Always set up Android notification channel — call
setNotificationChannelAsyncbefore requesting tokens. - Always use
v.id("users")orv.id("devices")for userId — never raw strings for document references. - Always handle
DeviceNotRegisterederrors — remove invalid tokens from the database. - Always clean up notification listeners — return removal functions from
useEffect. - Never request permissions on first app launch — use a soft prompt pattern first.
- Never log push tokens in production — wrap with
__DEV__checks. - Never send notifications in queries — only in mutations or actions.
- Never use
.filter()on device/token queries — use.withIndex().
DO / DON'T Quick Reference
| DO | DON'T |
|---|---|
app.use(pushNotifications) in convex.config.ts |
Put component config in convex.json |
Index fields you query (pushToken, deviceId) |
Use .filter() to find tokens |
Remove tokens on DeviceNotRegistered error |
Ignore failed delivery errors |
| Set Android channel before getting token | Skip channel setup on Android |
| Use soft prompt before system permission dialog | Call requestPermissionsAsync() on launch |
Store projectId in EAS config |
Hardcode projectId in source |
Use allowUnregisteredTokens: true for device-based |
Assume all tokens are user-authenticated |
| Batch notifications for multiple recipients | Loop sendPushNotification one-by-one for bulk |
Add returns validator to all functions |
Skip return type validation |
| Clean up listeners in useEffect return | Leave dangling notification subscriptions |
Implementation Patterns
For complete implementation code, read the appropriate reference file:
- Auth-based pattern (user-scoped tokens): See references/auth-pattern.md
- Device-based pattern (no auth, device tokens): See references/device-pattern.md
- Client-side hook: See references/client-hook.md
- Troubleshooting & debugging: See references/troubleshooting.md
Schema Patterns
Auth-based (user has account)
export default defineSchema({
users: defineTable({
name: v.string(),
email: v.optional(v.string()),
}),
});
// Token stored via push.recordToken keyed to user._id
Device-based (no auth)
export default defineSchema({
devices: defineTable({
pushToken: v.string(),
deviceId: v.optional(v.string()),
platform: v.optional(v.string()),
lastSeen: v.number(),
isActive: v.optional(v.boolean()),
})
.index("by_pushToken", ["pushToken"])
.index("by_deviceId", ["deviceId"]),
});
App Configuration
app.json / app.config.js
{
"expo": {
"ios": {
"bundleIdentifier": "com.yourcompany.yourapp",
"infoPlist": {
"UIBackgroundModes": ["remote-notification"]
}
},
"android": {
"package": "com.yourcompany.yourapp",
"googleServicesFile": "./google-services.json"
},
"plugins": [
["expo-notifications", {
"icon": "./assets/images/notification-icon.png",
"color": "#ffffff",
"defaultChannel": "default"
}]
],
"extra": {
"eas": { "projectId": "your-eas-project-id" }
}
}
}
eas.json
{
"build": {
"development": { "developmentClient": true, "distribution": "internal" },
"preview": { "distribution": "internal" },
"production": { "autoIncrement": true }
}
}
Permission Strategy
| App Type | When to Ask |
|---|---|
| Messaging | After first message sent |
| E-commerce | After first purchase |
| Social | After following someone |
| News | After subscribing to a topic |
Never ask on first app launch. Show a custom soft prompt explaining the value first, then call requestPermissionsAsync() only if user taps "Allow".
Security Rules
- Wrap token logging with
if (__DEV__)— never expose tokens in production - Validate token format server-side before storing
- Authenticate send-notification mutations — use
ctx.auth.getUserIdentity()for auth-based apps - Use HTTPS for all API calls in production
- Rate-limit notification sending — consider
@convex-dev/rate-limiter
Weekly Installs
1
Repository
imfa-solutions/skillsGitHub Stars
1
First Seen
1 day ago
Security Audits
Installed on
windsurf1
amp1
cline1
openclaw1
adal1
opencode1