Skip to content

Messaging

Appwrite Push_

Deliver push notifications from Appwrite to apps built with the Web, Flutter, React Native, Android, and Apple SDKs, without Firebase or device tokens.

17 min read

Raw

Appwrite Push is Appwrite's own push provider. With the Appwrite SDK, your app connects to Appwrite's push broker, an MQTT server that relays messages to connected devices. Messages you send with Appwrite Messaging arrive over that connection, so you can reach a signed-in user without setting up Firebase or collecting device tokens.

Appwrite Push works with the Web, Flutter, React Native, Android, and Apple SDKs. On Android, it keeps delivering after your users leave the app. On iOS, apps built with the Apple SDK use APNs to show messages while the app is suspended.

How it works

  • Every user has their own topic. A topic is a named channel that devices subscribe to. A signed-in user can subscribe to users/<USER_ID> without creating a topic first. A message sent with users: ['<USER_ID>'] reaches all of that user's connected devices.
  • You need targets only for some features. A target is a record of one device that Appwrite delivers to. You don't need one to send to a user. Register an Appwrite push target to use topics, iOS background delivery, or Appwrite Push alongside FCM or APNs.
  • The SDK holds the connection. Push connects with the session the app signed in with, so you don't manage a separate token. When the user is blocked or deleted, Appwrite closes their connections. Signing out doesn't close an open connection, so call push.close() when the user signs out.
  • Missed messages are replayed. With the default delivery guarantee, Appwrite keeps each message for up to 7 days and replays the ones a device missed when it reconnects.
  • Clients only receive. Devices can't publish messages. Everything is sent from your server or the Console through Messaging.

1. Add provider

The Add provider page, with Appwrite listed under Push
The Add provider page, with Appwrite listed under Push

Navigate to Messaging > Providers > Create provider and choose Appwrite under Push.

The Configure Appwrite form with the provider name filled in
The Configure Appwrite form with the provider name filled in

Enter a name for the provider. Appwrite Push doesn't take any credentials. Optionally, change the two delivery settings below, then click Create provider:

  • Quality of service: the delivery guarantee for devices that are connected when you send. At most once (QoS 0) sends each message once, and the device doesn't confirm it. At least once (QoS 1) waits for the device to confirm each message, so a message can arrive twice but isn't lost on the way. Let the subscriber choose sends at least once, and an app's subscription can switch to at most once with retry: false. Messages a device missed while offline are replayed according to the topic's and the subscription's setting. See Offline replay.
  • Message expiry (seconds): how long, in seconds, messages are kept for offline devices, up to 7 days (604800). Topics without their own expiry use the lowest expiry across your enabled Appwrite providers, or 7 days if none sets one. 0 keeps nothing for replay.

You can also create the provider with a Server SDK.

const sdk = require('node-appwrite');
const client = new sdk.Client()
.setEndpoint('https://<REGION>.cloud.appwrite.io/v1') // Your API Endpoint
.setProject('<PROJECT_ID>') // Your project ID
.setKey('<API_KEY>'); // Your secret API key
const messaging = new sdk.Messaging(client);
const provider = await messaging.createAppwriteProvider({
providerId: sdk.ID.unique(),
name: 'Appwrite Push',
qos: 1, // optional
expiry: 86400 // optional
});

2. Install the SDK

Install the SDK for your platform. Some platforms need extra setup:

On Android, Push needs Android 7.0 (API 24) or later and core library desugaring. Add both to android/app/build.gradle.kts:

Kotlin
android {
defaultConfig {
minSdk = 24
}
compileOptions {
isCoreLibraryDesugaringEnabled = true
}
}
dependencies {
coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:2.1.4")
}

The appwrite package needs Dart 3.10 and Flutter 3.38.1 or later.

3. Subscribe

Sign the user in. Create a Push instance from the same client, then call subscribe. If you pass no topic, subscribe listens on the user's own topic, users/<USER_ID>. Messages you send to that user arrive on this topic.

import { Account, Client, Push } from 'appwrite';
const client = new Client()
.setEndpoint('https://<REGION>.cloud.appwrite.io/v1')
.setProject('<PROJECT_ID>');
await new Account(client).createEmailPasswordSession({ email, password });
const push = new Push(client);
const subscription = await push.subscribe((message) => {
const { notification, data } = JSON.parse(message.data);
console.log(notification?.title, notification?.body, data);
});

subscribe returns once the subscription is active. On Android, a background subscription returns once the SDK's background service has taken it over.

Each message has four fields:

  • topic: the topic the message arrived on.
  • data: the payload as text.
  • payload: the payload as raw bytes.
  • qos: the delivery guarantee the message arrived with, 0 or 1.

Push signs in with the same credential as the client you created it from:

  • the session your app created through that client, or
  • a JWT or session you set on the client.

Background delivery reconnects later with that credential. A JWT expires, so use a session where you can.

To know when the connection opens, closes, or fails, register these callbacks. Calling one again replaces the earlier callback.

TypeScript
push
.onOpen(() => console.log('connected'))
.onClose(() => console.log('disconnected'))
.onError((error) => console.warn(error.message));

To change a subscription's options later, call subscription.update(...). To stop it, call subscription.unsubscribe().

4. Send a message

Send a push notification to the user from the Console or a Server SDK, exactly as you would with FCM or APNs. Navigate to Messaging > Messages > Create message > Push. Add a title and a body, and select the user as the recipient.

const message = await messaging.createPush({
messageId: sdk.ID.unique(),
title: 'Your order has shipped',
body: 'Track it from the Orders tab.',
users: ['<USER_ID>'],
data: { orderId: '<ORDER_ID>' }
});

While the app is on screen, the message reaches your subscribe callback, and mobile apps show no system notification. Once the app is in the background, the SDK shows one. See Notifications and taps.

A message to a user needs no target or topic. Appwrite creates the user's topic the first time you send to it.

Appwrite push target

A push target on the Appwrite provider tells Appwrite that a device listens over Appwrite Push. You need one to:

  • Receive messages sent to topics.
  • Show messages while an iOS app is suspended. See Background delivery on iOS.
  • Receive messages over Appwrite Push when the user also has FCM or APNs targets. FCM still gets the full notification, so an Android app that registers both can show a message twice. Skip duplicates by messageId, or register only one of them.

Create the target after each sign-in. Appwrite Push doesn't use identifier, so pass any unique value. Always set providerId to your Appwrite provider's ID. Without providerId, Appwrite assigns the target to one of your enabled push providers, which may not be the Appwrite provider.

import { ID } from 'appwrite';
const target = await account.createPushTarget({
targetId: ID.unique(),
identifier: ID.unique(),
providerId: '<APPWRITE_PROVIDER_ID>'
});

Targets belong to the session that created them. When the user signs out with deleteSession, Appwrite deletes that session's targets for you.

Message format

Your callback receives the message as JSON in message.data. A message sent with a title, a body, an image, and data looks like this:

JSON
{
"messageId": "6720c5e7...",
"notification": {
"title": "Your order has shipped",
"body": "Track it from the Orders tab.",
"image": "https://example.com/parcel.png"
},
"data": {
"orderId": "<ORDER_ID>"
},
"priority": "high"
}

The fields are:

  • messageId: the message's ID. With at least once delivery, the default, a message can arrive twice, for example when it's replayed after a reconnect. Use this ID to skip duplicates.
  • notification: the title and body. It also holds any of these createPush options you set: icon, color, sound, tag, channelId, badge, action, contentAvailable, and critical.
  • notification.image: a URL to the image. When you send, pass the image as a Storage file in the form <BUCKET_ID>:<FILE_ID>.
  • data: the custom data you sent.
  • priority: high, unless you set another priority when you sent the message.

Topics

To send the same message to many users, subscribe their devices to a Messaging topic by its ID.

Topic messages are delivered through Appwrite push targets. Add the device's Appwrite push target as a subscriber of the topic, from your server or from the app when the topic's permissions allow it:

TypeScript
await messaging.createSubscriber({
topicId: 'news',
subscriberId: ID.unique(),
targetId: target.$id
});

Then subscribe to the topic in the app:

TypeScript
const news = await push.subscribe(['news'], (message) => {
showNews(JSON.parse(message.data));
}, {
background: true, // also notify when the app isn't on screen
title: 'News' // notification title when a message has none
});

Topic IDs must match exactly. Wildcards like + and # are refused, and of the users/ topics, a user can subscribe only to their own. When the user is no longer a subscriber of the topic through any of their targets, Appwrite drops the device's live subscription to it.

Topics have their own delivery settings, the qos and expiry parameters of createTopic and updateTopic:

  • Quality of service: a message is delivered at the lowest setting of the provider, the topic, and the subscription.
  • Expiry: a topic without its own expiry uses the lowest expiry of your enabled Appwrite providers, or 7 days if none sets one.

Subscription options

OptionDescriptionDefault
backgroundShow a notification for each message while the app isn't on screen, and keep delivering in the background where the platform allows.true when you call subscribe without a topic, false when you pass topics
titleNotification title to use when a message has none.The topic name
retryAsk for at-least-once delivery (QoS 1), so messages missed while offline are replayed.true
notifyInForegroundAlso show a notification while the app is on screen. Android, Flutter, and React Native only.false

Notifications and taps

A subscription with background posts a notification for each message while the app isn't on screen, using the title, body, and image you sent.

A message with only data reaches your callback without a notification. The exception is a suspended iOS app: Appwrite wakes it through APNs for every message, so a message with only data shows as "New message".

  • Android 13 and later: the first background subscription asks for the notification permission. If the user declines, messages still reach your callback.
  • Android icon: add a meta-data entry to your <application> with your icon. The key is io.appwrite.push.notification_icon for the Android SDK, io.appwrite.flutter.push.notification_icon for Flutter, and io.appwrite.reactnative.push.notification_icon for React Native.
  • Web: the Web SDK shows a notification for each message on a background subscription while the tab is open, also while the tab is visible. Subscribe from a click handler, because some browsers only show the notification permission prompt after a user action. Mobile browsers don't show these notifications.

To open the right screen when the user taps a notification, read the tap that launched the app at startup and listen for later taps. data is the data you sent with the message.

TypeScript
// The tap that launched the app, reported once
const opened = await push.getInitialNotification();
if (opened) openOrder(opened.data.orderId);
// Taps while the app is running
const stop = push.onNotificationOpened(({ topic, data }) => openOrder(data.orderId));

On the web, getInitialNotification() always returns null, because a page only receives taps while it is open.

Delivery while the app is closed

How long a device keeps receiving messages after the user leaves your app depends on the platform.

Android

On Android, Flutter, and React Native apps, the SDK saves background subscriptions and wakes the app on a schedule to reconnect. The broker replays what was sent in between, and a notification is posted.

Device stateHow often the app reconnects
Android 11 and older, or exact alarms allowedAbout every 60 seconds, on time
Exempt from battery optimization, no exact alarmsAbout every 60 seconds, but Android can defer it
Android 12 and later, neither of the aboveAbout every 15 seconds, but Android can defer it, for example in battery saver
Doze (screen off, idle)About every 9 minutes
Foreground service onImmediately, also in Doze
Force-stoppedWhen the app is opened again

A message arrives at the next reconnect, so a deferred reconnect delays it.

For punctual wake-ups, declare these permissions in your manifest. On Android 14 and later, your users allow exact alarms under Settings > Apps > Special app access > Alarms & reminders. Google Play asks you to declare SCHEDULE_EXACT_ALARM.

Markup
<uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM" />
<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS" />

Background delivery is on for every subscription with background set, with nothing else to configure. Push.backgroundStatus() reports how punctual it can be. Its bestEffort flag is read-only and is true when messages can arrive late, or only once the app is opened:

  • Exact alarms aren't allowed, and no foreground service is running. A foreground service doesn't count while background usage is restricted.
  • The user restricted the app's background usage, and the app isn't exempt from battery optimization.

To make delivery more punctual, tell your users why you need the permission, then request it when they tap a button:

Kotlin
val status = Push.backgroundStatus(context)
if (status.bestEffort) {
if (!status.exactAlarms) {
Push.requestExactAlarms(context)
}
if (status.backgroundRestricted && !status.ignoringBatteryOptimizations) {
Push.requestIgnoreBatteryOptimizations(context)
}
}

requestExactAlarms and requestIgnoreBatteryOptimizations return false when:

  • the permission is already granted or isn't needed, or
  • the matching permission isn't declared in your manifest.

Flutter and React Native have the same methods on push, without the context argument, and they're async there. Outside Android, backgroundStatus() returns null, and both request methods resolve to false.

For immediate delivery, call push.setForeground(true) while the app is on screen. It runs a foreground service that shows a quiet ongoing notification, and the setting is kept across restarts.

The service is a remoteMessaging foreground service, which Google Play asks you to declare. If you never call setForeground, remove these from your merged manifest with tools:node="remove":

  • the push service: io.appwrite.services.PushService on Android, io.appwrite.flutter.services.PushService on Flutter, or io.appwrite.reactnative.services.PushService on React Native
  • the FOREGROUND_SERVICE_REMOTE_MESSAGING permission

iOS

These steps apply to apps built with the Apple SDK. Flutter and React Native apps on iOS receive messages only while the app runs and for a short time after it moves to the background. Once iOS suspends the app, messages wait until it opens again, and the broker then replays what was missed.

iOS suspends a backgrounded app within seconds, so the connection can't stay open. With the Apple SDK, Appwrite Push uses APNs to reach a suspended app:

  1. When you send to a user who has an Appwrite push target, Appwrite sends their iOS devices a short APNs wake-up instead of the full notification.
  2. A Notification Service Extension in your app opens its own connection, fetches the real message, and replaces the wake-up with its title, body, and image before iOS shows it.

To set it up:

  1. In Xcode, add the Push Notifications capability and the Background Modes capability with Remote notifications to your app target.

  2. Add an APNs provider. The quickest way is one-click APNs setup: run appwrite init apns in your iOS project. The CLI creates the sandbox and production APNs providers and adds the Apple platform.

  3. Add a Notification Service Extension target to your app, with this class:

    Swift
    import Appwrite
    class NotificationService: AppwriteNotificationService {}
  4. Give the app and the extension the App Groups capability with the same group ID, and set that group ID as AppwritePushAppGroup in the Info.plist of both targets. The extension reads the app's session from this group.

  5. Set a client ID that you save per install with client.setPushClientId(...). The default ID is shared by all of the user's iOS devices, so without your own ID, one device's extension can fetch the messages another device was woken for.

  6. Register an Appwrite push target after sign-in. Without it, Appwrite sends the full APNs notification instead of the wake-up.

  7. Subscribe with background: true. The SDK registers the device with APNs and creates its APNs push target for you.

The SDK registers for remote notifications on its own. To handle registration yourself, set AppwritePushAutoRegistration to NO in Info.plist, call UIApplication.shared.registerForRemoteNotifications(), and pass the device token to Push.setDeviceToken(_:) from your app delegate. Push.handleRemoteNotification(_:) and Push.isDelivered(_:) are optional helpers.

Web

Messages arrive while the page is open. To replay messages sent while the tab was closed, give each browser a stable client ID. See Offline replay.

Offline replay

Appwrite stores each message until it expires. When a device subscribes again, Appwrite replays up to the 5 newest messages it missed on each topic, oldest first. Replay needs at-least-once delivery on both the topic and the subscription.

Appwrite tracks replay per client ID, the identifier a device connects to the broker with. A client ID the broker hasn't seen before gets no backlog.

  • Android: the SDK derives a stable ID per user and install, so leave setPushClientId unset. This includes Flutter and React Native apps on Android.
  • iOS and the web: all of a user's iOS devices and Flutter web sessions share the default ID. With the Web SDK, the default ID belongs to one browser tab. To replay missed messages, save a client ID per device, for example in the Keychain or localStorage, and set it with setPushClientId.
TypeScript
let clientId = localStorage.getItem('pushClientId');
if (!clientId) {
clientId = crypto.randomUUID();
localStorage.setItem('pushClientId', clientId);
}
client.setPushClientId(clientId);

All tabs of the browser share this ID, so a missed message is replayed to the first tab that reconnects.

A random client ID on every launch loses the backlog.

Sign out

Close Push when the user signs out. It closes the connection and stops background delivery, saved subscriptions included. Deleting the session doesn't close a connection that is already open.

TypeScript
push.close();
await account.deleteSession({ sessionId: 'current' });

Self-hosting

A self-hosted Appwrite runs the broker in the appwrite-mqtt container. Traefik serves MQTT over TLS on port 8883 and MQTT over secure WebSocket on port 8084. See the MQTT environment variables to change the ports, the replay depth, and the maximum packet size.

Where each SDK connects by default decides what you set up:

  • Android, Apple, React Native, and Flutter on mobile connect to push.<YOUR_API_HOST> on port 8883. A push. DNS record that points at your server is enough.
  • Web, and Flutter on the web, connect to wss://push.<YOUR_API_HOST> on port 443, which Traefik doesn't serve. Set the broker address on these clients.

You can also set the broker address on any client instead of adding the DNS record:

TypeScript
client.setPushEndpoint('mqtts://appwrite.example.com:8883'); // Android, Apple, React Native, Flutter on mobile
client.setPushEndpoint('wss://appwrite.example.com:8084'); // Web, Flutter on the web

A Flutter app that runs on both picks the endpoint with kIsWeb.

Limits

LimitValue
Message retentionUp to 7 days
Replay per topicNewest 5 missed messages
Title and body256 characters for the title and 64,230 for the body, the limits of createPush
Connections128 new connections per user per minute. On self-hosted Appwrite, this applies when _APP_OPTIONS_ABUSE is enabled.

Troubleshooting

SymptomCause and fix
subscribe() throws "needs a signed-in user"The user isn't signed in through this client, and no JWT or session is set on it.
A topic subscription is refusedThe user isn't a subscriber of the topic, or the topic ID has a wildcard.
Topic messages never arriveThe device has no Appwrite push target subscribed to the topic.
A user with FCM or APNs targets gets nothing over Appwrite PushRegister an Appwrite push target for the device.
Messages sent while offline never arriveA client ID shared by several devices, which is the default on iOS, or tied to one tab, which is the default with the Web SDK. Also check that retry isn't false and that the message hasn't expired. See Offline replay.
No notification while the app is openExpected. Set notifyInForeground to true on Android, Flutter, or React Native.
Nothing arrives while the Android app is closedNotification permission declined, the app was force-stopped, or exact alarms aren't allowed. Check backgroundStatus().bestEffort.
iOS devices get no APNs notifications, or debug builds use the production providerNo enabled APNs provider matches the app's bundle ID and build: sandbox for debug and Simulator builds, production for TestFlight and App Store builds. Rerun appwrite init apns to create both, or set the bundle ID on the provider in the Console. Devices pick up the change the next time the app launches.
iOS shows "New message" instead of the real messageThe Notification Service Extension is missing, the extension target doesn't include the Appwrite library, or AppwritePushAppGroup isn't set to a group both targets share in both Info.plist files.
The UI freezes or the app stops responding on AndroidCall subscribe off the main thread.
React Native fails with "background delivery needs the SDK's native Android module"The app runs in Expo Go, or wasn't rebuilt after installing.

Was this page helpful?

Share what worked or what we should fix. Once approved, our agents automatically apply suggested updates to the docs.