Logo
Toggle menu

Push Notifications

Updated Jul 6, 2026

Circle's push notification campaign feature lets you deliver real-time notifications directly to your users on the Web, iOS, and Android — even when they aren't actively using your site or app. All three are channels of the same underlying setup: you connect one Firebase project to Circle, then install the SDK for whichever platform(s) you support.

The full flow has three parts:

  1. Create a Firebase project and register your Web, iOS, and Android apps.
  2. Upload the Firebase credentials to your Circle dashboard.
  3. Install the SDK for your platform — @circlehq/push-web for the browser, @circlehq/push-react-native for iOS and Android.

Prerequisites

Before you begin, make sure you have:

  • Node.js 18+ installed
  • A Circle account with an API key — get or create one from your dashboard API Keys settings
  • A Google account to create a Firebase project
  • For Web: a web app served over HTTPS or localhost — web push does not work on plain HTTP
  • For iOS/Android: a React Native project (Expo SDK 51+ or bare React Native CLI 0.71+) and a physical device — push notifications do not work on simulators or emulators

Sample Apps

Want to see a working integration before building your own? Clone a sample app, add your API key, and run it:

Each repo is a minimal working app with the SDK already wired up.

Step 1 — Create a Firebase Project and Register Your Apps

  1. Go to the Firebase console and click Add project. Follow the prompts to create a new project (or reuse an existing one).

  2. Add a Web app: click the Web icon (</>) and register it.

  3. Add an Android app: click the Android icon, enter your app's package name (this must match applicationId in android/app/build.gradle), and register the app.

  4. Add an iOS app: click the iOS icon, enter your app's bundle ID (this must match the Bundle Identifier in your Xcode project), and register the app.

    (Only register the platforms you actually support — you don't need all three.)

  5. Generate a Web Push certificate: go to Project Settings → Cloud Messaging → Web configuration, and click Generate key pair under "Web Push certificates". Copy the resulting VAPID key — you'll paste it into Circle in the next step.

  6. Generate a Service Account key: go to Project Settings → Service Accounts, and click Generate new private key. This downloads a JSON file — this is the master credential Circle needs first, since it's what lets Circle send messages through your Firebase project on your behalf.

Step 2 — Upload Your Firebase Credentials to Circle

  1. In your Circle dashboard, go to Settings → Integrations → Add Integration → Firebase (/settings/integrations/new/firebase). You'll need the Manage Integrations permission to access this page.
  2. Upload the Service Account JSON first — this unlocks the Android, iOS, and Web upload slots.
  3. Upload the credentials for each platform you registered:
    • Web — the Firebase SDK config JSON for your Web app, plus the VAPID key from Step 1 (Firebase console → Project Settings → General → Your apps → Web app → SDK setup and configuration)
    • Android — the google-services.json for your Android app (Firebase console → Project Settings → General → Your apps → Android app)
    • iOS — the GoogleService-Info.plist for your iOS app (Firebase console → Project Settings → General → Your apps → iOS app)

Each file is validated on upload:

  • Service Account JSON must contain type: "service_account", project_id, client_email, and private_key.
  • Web config JSON must contain apiKey, authDomain, projectId, appId, and messagingSenderId.
  • google-services.json must contain a matching project_info.project_id and an android_client_info entry.
  • GoogleService-Info.plist must contain BUNDLE_ID, API_KEY, and GOOGLE_APP_ID.

Replacing the Service Account later invalidates every device token currently registered for push — devices will stop receiving pushes until they re-register, so only rotate it when you mean to.

Step 3 — Set Up the SDK for Your Platform

Choose your platform below — Web, iOS, or Android. For iOS and Android, then pick the framework you're building with. Each tab shows only the setup steps relevant to that combination.

Web push works in any modern browser over HTTPS (or localhost). Install the web SDK, ship the service worker, then initialize on load — the framework-specific notes below cover where the service worker lands.

Install the package

npm install @circlehq/push-web

The package automatically copies a service worker file (firebase-messaging-sw.js) into your project during install. This service worker handles background notifications when your tab is not in focus.

If your install runs with --ignore-scripts (common in CI), run the copy step once manually:

npx circle-push install-sw

The installer detects your framework from package.json and places the file in the right location:

  • Next.jspublic/
  • Vite (React / Vue / Svelte)public/
  • Create React Apppublic/
  • SvelteKitstatic/
  • Angularsrc/ (also add one entry to angular.json — see below)

Angular only: add this to the assets array in your angular.json:

{ "glob": "firebase-messaging-sw.js", "input": "src", "output": "/" }

Add your API key

Store it in an environment variable — never hard-code it.

# Vite / Create React App — .env or .env.local
VITE_CIRCLE_API_KEY=cir_live_xxx
# Next.js — .env.local
NEXT_PUBLIC_CIRCLE_API_KEY=cir_live_xxx

Initialize the SDK on app load

Call CirclePush.init() once, as early as possible. It is safe to call more than once.

// React (Vite / CRA)
import { useEffect } from 'react';
import CirclePush from '@circlehq/push-web';

export default function App() {
  useEffect(() => {
    CirclePush.init({
      apiKey: import.meta.env.VITE_CIRCLE_API_KEY,
    }).catch(console.error);
  }, []);

  return <YourApp />;
}
// app/_components/CirclePushProvider.tsx — Next.js 14+ (App Router)
'use client';
import { useEffect } from 'react';
import CirclePush from '@circlehq/push-web';

export function CirclePushProvider({ children }: { children: React.ReactNode }) {
  useEffect(() => {
    CirclePush.init({
      apiKey: process.env.NEXT_PUBLIC_CIRCLE_API_KEY!,
    }).catch(console.error);
  }, []);
  return <>{children}</>;
}

Identify users after sign-in

Call identify() to register the browser as a push device in Circle. You can identify by email, phone, or both.

import CirclePush from '@circlehq/push-web';

async function enablePushForUser(user) {
  const device = await CirclePush.identify({
    email: user.email,
    firstName: user.firstName,
  });
  console.log('Push registered:', device.deviceId);
}

By default, identify() automatically shows the browser's notification permission dialog. To show your own soft-ask prompt first, disable the auto-prompt at init:

await CirclePush.init({
  apiKey: import.meta.env.VITE_CIRCLE_API_KEY,
  autoRequestPermission: false,
});

// After the user accepts your custom prompt:
const state = await CirclePush.requestPermission();
if (state === 'granted') {
  await CirclePush.identify({ email: currentUser.email });
}

Handle notification events (optional)

Subscribe with on() and remove listeners with off() when you no longer need them.

import CirclePush from '@circlehq/push-web';

const onClicked = ({ link, campaignId }) => {
  console.log('Notification clicked — campaign:', campaignId);
};
const onReceived = (payload) => {
  console.log('Notification received in foreground:', payload);
};

CirclePush.on('notificationClicked', onClicked);
CirclePush.on('notificationReceived', onReceived);

// Later, e.g. on component unmount:
CirclePush.off('notificationClicked', onClicked);
CirclePush.off('notificationReceived', onReceived);

Unregister on sign-out

Always unregister the device when a user signs out.

async function handleSignOut() {
  await CirclePush.unregister();
  await yourAuthProvider.signOut();
}

Available Events

Both SDKs expose the same event names:

  • notificationReceived — a notification arrives while the app/tab is in the foreground
  • notificationClicked — the user taps a notification
  • notificationDismissed — the user dismisses a notification (React Native only)
  • permissionChange — the OS/browser permission status changes
  • tokenRefresh — the push token is rotated
  • error — an internal SDK error occurs

Subscribe with CirclePush.on(event, handler). On web, unsubscribe with CirclePush.off(event, handler); on React Native, on() returns an unsubscribe function directly.

API Reference

The Web and React Native SDKs share the same core API:

CirclePush.init(config)

Initialize the SDK. Call once on app load. apiKey is the only required field.

CirclePush.identify({ email?, phone?, firstName?, lastName? })

Register the device/browser as a push target for this contact. Requires at least one of email or phone.

CirclePush.requestPermission()

Trigger the permission prompt. Returns 'granted', 'denied', 'default', or 'unsupported'.

CirclePush.getPermissionState()

Synchronous read of the current permission state — does not trigger a prompt.

CirclePush.getToken()

Returns the current push token, or null if not yet registered.

CirclePush.refresh()React Native only

Re-registers the current identity, returning the updated device record.

CirclePush.unregister()

Unregister the device and clear all local state. Call this on sign-out.

CirclePush.on(event, handler) / CirclePush.off(event, handler)web

Subscribe or unsubscribe to SDK events. On React Native, on() returns an unsubscribe function directly.

CirclePush.setDebug(true)

Enable verbose console logging. Tokens and emails are always redacted.

Sending Push Notifications

Once devices are registered — on Web, iOS, or Android — you send the actual notifications from Circle, not from the SDK. Use whichever fits your use case:

  • Campaigns — one-off or broadcast pushes to a segment of contacts.
  • Workflow Automations — triggered pushes as part of an automated flow (e.g. after a user signs up or completes an action).

Browser Support (Web)

  • Chrome, Edge, Brave, Opera (desktop & Android) — full support
  • Firefox 90+ — full support
  • Safari macOS 16.4+ — full support
  • Safari iOS 16.4+ — supported only when the app is installed via "Add to Home Screen"
  • In-app browsers (Facebook, Instagram, etc.) — not supported; the SDK silently no-ops

Troubleshooting

Permission stays default after calling requestPermission()

The user dismissed the prompt without making a choice. Browsers/OSes throttle repeated prompts — only call requestPermission() in response to a direct user action like a button click.

Service worker not found (Web)

Confirm firebase-messaging-sw.js is reachable at https://yourdomain.com/firebase-messaging-sw.js. It must be same-origin and cannot be served from a CDN subdomain.

iOS Safari shows no notifications (Web)

Web Push on iOS 16.4+ requires the user to first add the app to their Home Screen. The SDK returns unsupported until then.

Notifications never arrive on a simulator/emulator (React Native)

Push notifications require APNs and Firebase Cloud Messaging, neither of which work on simulators or emulators. Always test on a physical device.

Android 13+ shows no permission prompt (React Native)

Android 13 (API 33) introduced a runtime POST_NOTIFICATIONS permission. Make sure it's declared in AndroidManifest.xml (the setup script/config plugin adds this automatically) and that you've called requestPermission().

iOS notifications work in development but not after release to TestFlight/App Store (React Native)

Check the aps-environment entitlement in your iOS project — it defaults to development. Set it to production before submitting a release build.

Expo build fails resolving notifee during expo prebuild (React Native)

expo prebuild doesn't automatically wire up @notifee/react-native's local Maven repository. Add a config plugin mod (e.g. a withNotifeeMavenRepo-style plugin) that adds node_modules/@notifee/react-native/android/libs as a Maven repo in your root build.gradle.

config/already_initialized error

You called init() more than once with a different config. Call init() exactly once with a consistent apiKey.