skills/imfa-solutions/skills/convex-expo-push-notifications

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:

  1. Physical device available (simulators do NOT support push notifications)
  2. Expo account with EAS project configured (eas build:configure)
  3. Convex project initialized
  4. For Android: Firebase project with google-services.json
  5. 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

  1. Always register component in convex.config.ts — not convex.json. Use app.use(pushNotifications).
  2. Always validate push tokens — check format starts with ExponentPushToken[ or ExpoPushToken[.
  3. Always set up Android notification channel — call setNotificationChannelAsync before requesting tokens.
  4. Always use v.id("users") or v.id("devices") for userId — never raw strings for document references.
  5. Always handle DeviceNotRegistered errors — remove invalid tokens from the database.
  6. Always clean up notification listeners — return removal functions from useEffect.
  7. Never request permissions on first app launch — use a soft prompt pattern first.
  8. Never log push tokens in production — wrap with __DEV__ checks.
  9. Never send notifications in queries — only in mutations or actions.
  10. 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:

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

  1. Wrap token logging with if (__DEV__) — never expose tokens in production
  2. Validate token format server-side before storing
  3. Authenticate send-notification mutations — use ctx.auth.getUserIdentity() for auth-based apps
  4. Use HTTPS for all API calls in production
  5. Rate-limit notification sending — consider @convex-dev/rate-limiter
Weekly Installs
1
GitHub Stars
1
First Seen
1 day ago
Installed on
windsurf1
amp1
cline1
openclaw1
adal1
opencode1