Skip to content

Appwrite push - #1909

Open
ArnabChatterjee20k wants to merge 118 commits into
mainfrom
appwrite-push
Open

ArnabChatterjee20k wants to merge 118 commits into
mainfrom
appwrite-push

Conversation

@ArnabChatterjee20k

@ArnabChatterjee20k ArnabChatterjee20k commented Sep 15, 2026

Copy link
Copy Markdown
Member

What this PR adds

A native Push service on the client SDKs. It is the Realtime analog delivered over the Appwrite MQTT broker (MQTT 5 with enhanced auth), with no FCM or APNS. Push is server-initiated: a client subscribes and receives, there is no client publish. The credential is read off the Client (set a JWT or session on it, like every other service), so Push has no setJWT/setSession of its own.

Per-SDK transport: Flutter uses mqtt5_client (raw TCP on mobile and desktop, MQTT over WebSocket on web), React Native uses MQTT.js over react-native-tcp-socket, Web uses MQTT.js over WebSocket, Android uses HiveMQ, Apple uses MQTTNIO (the SDK's existing SwiftNIO stack), Unity uses MQTTnet.

The API

The whole surface is: create the service, subscribe, receive, tear down. Nothing on the connection is tunable, so there are no connection setters to call in the right order.

Call What it does
Push(client) Create the service. Reads the JWT or session off client.
subscribe(topics, callback, { background?, title?, reDeliverIfMissed? }) Subscribe to one topic or a list. Resolves after SUBACK (foreground). Returns a subscription handle.
handle.unsubscribe() Drop that subscription. Closes the connection once the last one is gone.
handle.update({ background?, title?, reDeliverIfMissed? }) Change that subscription's options live, without resubscribing.
onOpen(cb) / onClose(cb) / onError(cb) Connection lifecycle hooks (foreground connection).
close() Tear down the connection and drop every subscription.
Client.setPushEndpoint(url) Point at a specific broker. mqtt(s)://host:port on native, ws(s)://host in the browser. Append ?tlsInsecure=true to skip TLS cert verification (self-signed brokers, testing; native only).
Client.setPushClientId(id) A stable client id, so the broker resumes this client's replay cursor across restarts.

Two things are per subscription rather than per connection, which is where MQTT itself puts them:

reDeliverIfMissed sets the subscription's QoS. true (the default) subscribes at QoS 1, so the broker holds that topic's messages and redelivers them on reconnect. false subscribes at QoS 0, at-most-once, so a message sent while you are disconnected is gone. The connection always keeps its session (clean start is always off), and the broker owns the replay window server-side, so reliability is a per-subscription choice and there is no connection-level replay flag.

background keeps a subscription delivering while the app is backgrounded or closed and posts a notification per message with title. The foreground service is reference-counted across subscriptions: it runs while any live subscription has background on, and stops once the last one goes. Each opted-in subscription posts notifications with its own title.

Duplicate messages

QoS 1 is at-least-once. After a reconnect the broker can redeliver a message you already handled, so a reDeliverIfMissed: true subscription may see the same message twice. If duplicates matter for your callback (for example it writes to a store or increments a counter), dedupe on the message: keep a set of recently seen message ids (or a hash of topic plus payload) and skip one you have already processed. A reDeliverIfMissed: false subscription does not redeliver, so it does not need this.

Usage

Flutter

// Auth lives on the Client; Push reads the JWT (or session) off it.
final client = Client()
    .setEndpoint('https://<region>.cloud.appwrite.io/v1')
    .setProject('<projectId>')
    .setJWT(jwt);

// Optional: point at a specific broker. Append ?tlsInsecure=true to skip cert checks.
client.setPushEndpoint('mqtts://<region>.cloud.appwrite.io:8883');
// Optional: a stable id so the broker resumes this client's replay cursor across restarts.
client.setPushClientId('device-42');

final push = Push(client);

// Connection lifecycle of the foreground (in-process) connection.
push.onOpen(() => print('connected'));
push.onClose(() => print('closed'));
push.onError((error) => print('error: $error'));

// Foreground subscription. Resolves after SUBACK. reDeliverIfMissed defaults to true (QoS 1).
final sub = await push.subscribe('user/123/#', (message) {
  print('${message.topic}: ${message.string}');
});

// Background subscription: keeps delivering while backgrounded/closed and posts a
// notification per message. Needs flutter_background_service + flutter_local_notifications.
final orders = await push.subscribe(
  'orders/#',
  (message) => print(message.string),
  background: true,
  title: 'Orders',
  reDeliverIfMissed: false, // QoS 0 for this one
);

// Change a subscription live, without resubscribing.
orders.update(background: false);        // stop background, keep receiving in foreground
orders.update(reDeliverIfMissed: true);  // switch this subscription to QoS 1

// Drop one subscription; the connection closes once the last one is gone.
sub.unsubscribe();

// Or tear everything down at once.
push.close();

React Native

import { Client, Push } from 'react-native-appwrite';

const client = new Client()
  .setEndpoint('https://<region>.cloud.appwrite.io/v1')
  .setProject('<projectId>')
  .setJWT(jwt);

client.setPushEndpoint('mqtts://<region>.cloud.appwrite.io:8883'); // ?tlsInsecure=true to skip cert checks
client.setPushClientId('device-42');

const push = new Push(client);

push.onOpen(() => console.log('connected'));
push.onClose(() => console.log('closed'));
push.onError((error) => console.log('error', error));

// Foreground subscription. Resolves after SUBACK. reDeliverIfMissed defaults to true.
const sub = await push.subscribe('user/123/#', (message) => {
  console.log(message.topic, message.payload.toString());
});

// Background subscription: Android foreground service + a notification per message.
// Needs the react-native-background-actions and expo-notifications peers, and the package
// added to your Expo plugins (it ships app.plugin.js for the Android 14+ manifest bits).
const orders = await push.subscribe('orders/#', (m) => console.log(m.topic), {
  background: true,
  title: 'Orders',
  reDeliverIfMissed: false,
});

orders.update({ background: false });
orders.update({ reDeliverIfMissed: true });

sub.unsubscribe();
push.close();

For background delivery, register the config plugin in app.json (or app.config.js) so Expo prebuild injects the Android 14+ foregroundServiceType and the FOREGROUND_SERVICE / FOREGROUND_SERVICE_DATA_SYNC / POST_NOTIFICATIONS permissions into the manifest. It is a no-op on iOS.

{
  "expo": {
    "plugins": ["react-native-appwrite"]
  }
}

Web

import { Client, Push } from 'appwrite';

const client = new Client()
  .setEndpoint('https://<region>.cloud.appwrite.io/v1')
  .setProject('<projectId>')
  .setJWT(jwt);

// Browser transport is MQTT over WebSocket; override with ws(s):// if needed.
client.setPushEndpoint('wss://<region>.cloud.appwrite.io');
client.setPushClientId('tab-42');

const push = new Push(client);

push.onOpen(() => console.log('connected'));
push.onClose(() => console.log('closed'));
push.onError((error) => console.log('error', error));

const sub = await push.subscribe('user/123/#', (message) => {
  console.log(message.topic, new TextDecoder().decode(message.payload));
});

// background shows a browser notification per message while the tab is open. A browser
// cannot hold a socket once the tab closes, so there is no true background mode.
const orders = await push.subscribe('orders/#', (m) => console.log(m.topic), {
  background: true,
  title: 'Orders',
  reDeliverIfMissed: false,
});

orders.update({ background: false });
orders.update({ reDeliverIfMissed: true });

sub.unsubscribe();
push.close();

Apple

let client = Client()
    .setEndpoint("https://<region>.cloud.appwrite.io/v1")
    .setProject("<projectId>")
    .setJWT(jwt)

client.setPushEndpoint("mqtts://<region>.cloud.appwrite.io:8883") // ?tlsInsecure=true to skip cert checks
client.setPushClientId("device-42")

let push = Push(client)

push.onOpen { print("connected") }
push.onClose { print("closed") }
push.onError { error in print("error: \(error)") }

// Suspends until SUBACK. reDeliverIfMissed defaults to true (QoS 1).
let sub = try await push.subscribe("user/123/#") { message in
    print("\(message.topic): \(message.string)")
}

// background posts a local notification per message via UserNotifications.
let orders = try await push.subscribe(
    "orders/#",
    background: true,
    title: "Orders",
    reDeliverIfMissed: false
) { message in
    print(message.string)
}

orders.update(background: false)
orders.update(reDeliverIfMissed: true)

sub.unsubscribe()
push.close()

Android

val client = Client(context)
    .setEndpoint("https://<region>.cloud.appwrite.io/v1")
    .setProject("<projectId>")
    .setJWT(jwt)

client.setPushEndpoint("mqtts://<region>.cloud.appwrite.io:8883") // ?tlsInsecure=true to skip cert checks
client.setPushClientId("device-42")

val push = Push(client)

push.onOpen { println("connected") }
push.onClose { println("closed") }
push.onError { error -> println("error: $error") }

// Blocks until SUBACK (foreground). reDeliverIfMissed defaults to true.
val sub = push.subscribe("user/123/#") { message ->
    println("${message.topic}: ${message.string}")
}

// Background runs a foreground Service. It needs POST_NOTIFICATIONS on Android 13+ and a
// Context to start the service the first time.
Push.requestNotificationPermission(activity)
val orders = push.subscribe(
    "orders/#",
    background = true,
    title = "Orders",
    reDeliverIfMissed = false,
    context = context,
) { message ->
    println(message.string)
}

orders.update(background = false)
orders.update(reDeliverIfMissed = true)

sub.unsubscribe()
push.close()

Unity

client.SetEndpoint("https://<region>.cloud.appwrite.io/v1")
      .SetProject("<projectId>")
      .SetJWT(jwt);

client.SetPushEndpoint("mqtts://<region>.cloud.appwrite.io:8883"); // ?tlsInsecure=true to skip cert checks
client.SetPushClientId("device-42");

// Push is a MonoBehaviour, created through the manager.
var push = AppwriteManager.Instance.Push;

push.OnOpen(() => Debug.Log("connected"));
push.OnClose(() => Debug.Log("closed"));
push.OnError(error => Debug.LogError(error));

// Awaits SUBACK. reDeliverIfMissed defaults to true (QoS 1).
var sub = await push.Subscribe("user/123/#", message => {
    Debug.Log($"{message.Topic}: {message.Text}");
});

// QoS 0 for this subscription. Unity has no background or notification mode, so it delivers
// on the main thread while the app runs and the handle exposes Unsubscribe and Update only.
var lossy = await push.Subscribe("orders/#", m => Debug.Log(m.Text), reDeliverIfMissed: false);

lossy.Update(reDeliverIfMissed: true);

sub.Unsubscribe();
push.Close();

Background delivery per platform

Android and Flutter host the connection in a foreground service (Flutter uses a background isolate) while any subscription wants background, so delivery survives backgrounding and swipe-away while the process is alive. The single connection relocates there when background is wanted and back in-process when none is, so the broker never sees two sessions with the same client id. A hard process kill is not recovered.

On iOS (Apple and React Native) a background subscription delivers only while the app runs or is briefly backgrounded. iOS suspends the app and its socket within seconds and kills it on swipe-away, and there is no API to keep a raw socket alive. Reliable background delivery on iOS needs APNs, which this MQTT channel does not use. This is a platform limit, not a bug, and worth stating plainly in the docs.

Web shows a browser notification while the tab is open. There is no delivery once the tab closes. Unity has no background or notification mode.

Dependencies

Each SDK pulls in one MQTT 5 client, and the optional background mode adds a few more. Nothing bumps a language or runtime floor.

SDK MQTT client Background (optional) Declared as
Flutter mqtt5_client, typed_data flutter_background_service, flutter_local_notifications, path_provider pubspec deps
React Native mqtt (+ buffer, readable-stream, process, expo-file-system); react-native-tcp-socket (optional peer) react-native-background-actions, expo-notifications, @expo/config-plugins (all optional peers) deps + optional peerDependencies
Web mqtt none (browser Notification API) package.json dep
Android com.hivemq:hivemq-mqtt-client none (foreground Service is built in) Gradle implementation
Apple swift-server-community/mqtt-nio (MQTTNIO) none (UserNotifications) SPM
Unity MQTTnet 4.3.7, bundled as a netstandard2.0 plugin DLL none vendored DLL + lockfile entry

React Native peers a consumer acts on: react-native-tcp-socket is the native socket the client runs over (optional so importing the SDK without it does not break Metro, required for Push to connect). react-native-background-actions and expo-notifications are only needed for a background subscription. @expo/config-plugins is an optional peer so the shipped app.plugin.js resolves at Expo prebuild against the consumer's Expo-managed copy, which fixes resolution under pnpm and Yarn PnP.

Downstream, not in this repo: MQTTNIO is SPM-only, so the Apple SDK ships through Swift Package Manager. If that repo still carries a CocoaPods podspec, that path needs a look with the Apple SDK maintainers, since MQTTNIO has no CocoaPods support.

Test plan

A minimal MQTT broker (utopia-php/mqtt) runs in the mock server as the mqtt service on the mockapi network, serving both transports the client uses, TCP on 1883 and WebSocket on 8083, and starts with the existing e2e setUp(). The broker is server-initiated: on SUBSCRIBE it delivers a message on the subscribed topic, mirroring production, so the e2e subscribes and awaits with no client publish.

Push e2e runs for Flutter (TCP), Flutter web (WebSocket), Apple, Android, Unity and Web (WebSocket), driven through Client.setPushEndpoint. Each asserts subscribe, receive, and that a default subscription is delivered at QoS 1. Not covered by the broker e2e: Kotlin (the server SDK has no Push, like Realtime) and React Native (the browser harness has no raw TCP socket); both need instrumented or device runners.

Note for the broker team: MQTT 5 clients that use a challenge/response enhanced-auth mechanism (HiveMQ, used by the Android SDK) require the CONNACK to echo the authenticationMethod. The mock does this; the production broker returns a bare CONNACK today, so it should echo the method for browser and HiveMQ clients.

@greptile-apps

greptile-apps Bot commented Sep 15, 2026

Copy link
Copy Markdown
Contributor

RetriggerConfidence Score: 5/5

The PR appears safe to merge; the only outstanding finding from the previous review is fixed by deriving Web Push from the regular API endpoint.

Summary

Adds native Push services across the supported client SDKs, backed by MQTT 5 transports and a dedicated mock broker for end-to-end coverage.

  • Implements subscription lifecycle, reconnect behavior, reliability options, and platform-specific background delivery.
  • Adds Push endpoint and stable client-ID configuration to generated clients.
  • Adds transport dependencies, platform packaging/configuration, and cross-SDK behavioral tests.
  • Corrects Web Push broker derivation to use the regular API endpoint rather than the independently configured Realtime endpoint.

Reviews (47) · Last reviewed commit: "fix(web): derive the default push host f..."

Comment thread templates/android/library/src/main/java/io/package/services/Push.kt.twig Outdated
Comment thread templates/web/src/services/push.ts.twig Outdated
Comment thread templates/android/library/src/main/java/io/package/services/PushService.kt.twig Outdated
Comment thread templates/react-native/src/lib/tcp-stream.ts.twig Outdated
Comment thread templates/react-native/src/services/push.ts.twig
Comment thread templates/apple/Sources/Services/Push.swift.twig Outdated
Comment thread templates/flutter/lib/src/mqtt_io.dart.twig
Comment thread templates/android/library/src/main/java/io/package/services/Push.kt.twig Outdated
Comment thread templates/android/library/src/main/java/io/package/services/PushService.kt.twig Outdated
Comment thread templates/android/example/build.gradle.kts.twig
# Conflicts:
#	templates/react-native/package-lock.json.twig
#	templates/web/package-lock.json.twig
#	templates/web/package.json.twig
A background subscription hands the connection to the foreground
service/isolate, which connects and subscribes asynchronously, so it
returns once the subscription is handed off rather than after SUBACK.
Document that the SUBACK guarantee holds for foreground subscriptions
and point at onOpen/onError for the background connection lifecycle
(Flutter interface doc + Android subscribe KDoc).
Comment thread templates/flutter/lib/src/mqtt.dart.twig Outdated
…setReplayMessages

Replace the connection-level setReplayMessages() (and its throw-while-
subscribed guard) with a per-subscription reDeliverIfMissed option: the
connection now always keeps its session (clean start off) and QoS is
chosen per subscription (true -> QoS1 default, false -> QoS0). This is
the MQTT-idiomatic shape (QoS is a per-SUBSCRIBE field), removes the
awkward connection-level setter, and is live-toggleable via the handle's
update() (re-subscribes the filter at the new QoS). A shared filter uses
the max QoS across its subscriptions so none is downgraded.
react-native-background-actions requires taskIcon for its ongoing
notification; omitting it made start() throw "Task icon not found",
which the catch then mislabeled as a missing peer dependency. Pass
taskIcon { name: ic_launcher, type: mipmap } (present in every RN/Expo
Android app) and foregroundServiceType [dataSync] to match the manifest
the config plugin injects. Also surface the real error from the catch
instead of always blaming the peer deps.
Comment thread templates/web/src/services/push.ts.twig Outdated
…Messages

Replace the connection-level setReplayMessages() with a per-subscription
reDeliverIfMissed option (true -> QoS1 default, false -> QoS0). The
connection always keeps its session (never startClean), so replay is a
per-subscription choice; a shared filter uses the max QoS across its
subs. The handle's update() re-subscribes the filter at the new QoS
(in-process) or re-syncs the isolate (background), which now carries a
per-topic QoS map and resubscribes a topic whose QoS changed. Covers the
interface, io, browser and the background isolate.
…ssages

Replace the connection-level setReplayMessages() (throwing setter) with a
per-subscription reDeliverIfMissed option (true -> QoS1 default, false ->
QoS0). cleanStart is always off so the broker keeps the session; a shared
filter uses the max QoS across its subs. The handle's update() now takes
reDeliverIfMissed and re-subscribes the affected filters at the new QoS.
Comment on lines +541 to +546
if (onConnected != null) {
builder = builder.addConnectedListener { onConnected() }
}
if (onDisconnected != null) {
builder = builder.addDisconnectedListener { context -> onDisconnected(context.cause) }
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P1] Reconnect and restore subscriptions after an unexpected disconnect

The disconnect listener only reports lifecycle events, and buildPushClient() never enables HiveMQ automatic reconnect. After a network interruption, the existing subscriptions therefore stop receiving indefinitely unless application code explicitly intervenes. Keeping the client ID stable does not help if no new connection is attempted. The same missing recovery exists in Apple (Push.swift.twig, addCloseListener) and Unity (Push.cs.twig, DisconnectedAsync): their handlers only notify callers.

I reproduced this with locally generated SDKs at dac7a5ffa: subscribe, successfully receive a QoS 1 message, force the broker's TCP connection closed, wait four seconds, then publish again. Android, Apple, and Unity each emitted only one CONNECT total and received no second message; a Web control reconnected and resumed delivery. Android was tested under Robolectric, Apple on macOS, and Unity using the generated Push.cs plus the bundled MQTTnet DLL with minimal engine/configuration stubs—not a Unity player. I rechecked these connection paths at the current head b39d41c9; automatic reconnect is still absent.

Please add reconnect/backoff and resubscription for unexpected disconnects in all three SDKs, while ensuring explicit close() disables recovery. A forced-disconnect regression test should verify that an existing subscription resumes delivery without another application-level subscribe() call.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 6981da2. Android now enables HiveMQ automaticReconnectWithDefaultConfig(); with clean start off and a stable client id the broker resumes the session, so delivery continues after a drop without resubscribing (resubscribing would stack a second per-subscription HiveMQ callback). Apple (MQTTNIO) and Unity (MQTTnet) have no built-in reconnect, so their close/disconnect handlers now run a backoff loop (1s..30s) that reconnects and resubscribes every active filter at its effective QoS. A deliberate close() disables recovery in all three via a closing flag (and HiveMQ does not reconnect after a user-initiated disconnect).

Not yet added: an in-repo forced-disconnect regression test. The mock broker (utopia-php/mqtt Swoole server) can't drop a client's socket on command, so a real forced-disconnect e2e needs broker support or a per-SDK instrumented harness like the one you used locally. Happy to follow up on that separately — flagging so it isn't assumed covered.

…Messages

Replace the connection-level setReplayMessages() with a per-subscription
reDeliverIfMissed option (true -> QoS1 default, false -> QoS0). cleanStart
is always off so the broker keeps the session; a shared filter uses the
max QoS across its subs. PushForeground now tracks per-topic QoS and, on
sync, unsubscribes+resubscribes a topic whose QoS changed (avoiding a
stacked HiveMQ callback); PushService subscribes each topic at that QoS.
In-process, update() re-subscribes only the filters whose QoS changed
(unsubscribe-then-resubscribe), and reconcile() no longer resubscribes an
unchanged in-process host. Drop the now-unused PushConfig.replay.
…ssages

Replace the connection-level SetReplayMessages() with a per-subscription
reDeliverIfMissed option (true -> QoS1 default, false -> QoS0). cleanStart
is always off so the broker keeps the session; a shared filter uses the
max QoS across its subs. The PushSubscription handle gains Update(
reDeliverIfMissed) which resubscribes the affected filters at the new QoS
(MQTTnet's single central receive handler means no callback duplication).
Comment thread templates/apple/Sources/Services/Push.swift.twig
…cted

update({ reDeliverIfMissed }) sent a fire-and-forget re-SUBSCRIBE and
reported success even if the broker rejected it, leaving the local QoS
out of sync with the broker. Handle the SUBACK callback: on error,
restore the pre-update QoS (the old subscription still stands) and
surface it via onError.
…n/onError

The Flutter isolate and Android foreground service forward messages but
not their connection lifecycle, and retry failures internally, so a
background subscription's connect/SUBACK failures never reach onOpen/
onError. Correct the Flutter and Android docs to scope those hooks to the
foreground (in-process) connection.
Unsubscribing the last reDeliverIfMissed=true subscription from a filter
that still had QoS-0 subscribers left the broker subscription at QoS 1, so
the remaining at-most-once subscribers kept getting replayed messages they
opted out of. On unsubscribe, re-SUBSCRIBE such a filter at its new (lower)
effective QoS across all SDKs (Web, React Native, Apple, Android in-process
via the safe unsubscribe+resubscribe, Flutter io+browser, Unity); the
Android/Flutter background hosts already apply it through their re-sync.
…android, apple, unity)

Web/RN (mqtt.js reconnectPeriod) and Flutter (autoReconnect +
resubscribeOnAutoReconnect) already recover, but Android, Apple and Unity
did not, so subscriptions stopped receiving after a network drop.

- Android: enable HiveMQ automaticReconnectWithDefaultConfig(). With clean
  start off and a stable client id the broker resumes the session, so
  delivery continues without resubscribing (which would stack a callback).
- Apple: MQTTNIO has no built-in reconnect; the close listener now starts a
  backoff loop (1s..30s) that reconnects the client and resubscribes every
  active filter at its effective QoS.
- Unity: MQTTnet's plain client likewise gets a backoff reconnect loop in
  DisconnectedAsync that reconnects and resubscribes.

A deliberate close() disables recovery in all three (a closing flag; HiveMQ
does not reconnect after a user disconnect).
Comment thread templates/apple/Sources/Services/Push.swift.twig Outdated
Comment thread templates/unity/Assets/Runtime/Push.cs.twig Outdated
…idden resub failure, unity double onOpen)

- Android: connect() now reuses the existing client even while it is mid
  automatic-reconnect, instead of building a second client with the same
  stable id that would fight over the broker session.
- Apple: the reconnect loop no longer swallows a resubscription failure with
  try?; it lets the error fall through to the backoff retry so recovery is
  not reported while handles have no broker routing.
- Unity: drop the duplicate _onOpen from the reconnect loop; the client's
  ConnectedAsync handler already fires it once per reconnect.
Comment thread templates/apple/Sources/Services/Push.swift.twig Outdated
… live client

If connect() succeeds but the resubscribe is rejected, the retry must not
call connect() again on the already-connected client (a no-op that never
restores routing, looping forever). Guard the (re)connect on the client's
connected state (Apple client.isActive(), Unity client.IsConnected) so a
resubscribe-only failure just retries the subscribe.
Comment thread templates/unity/Assets/Runtime/Push.cs.twig
MQTTnet reports rejected filters in the SubscribeAsync result rather than
throwing, so a completed call was wrongly treated as success. Add an
AllGranted() reason-code check: the reconnect loop keeps retrying on a
rejected resubscription instead of ending recovery, and the initial
subscribe throws (rolling back) instead of silently leaking a
never-routed callback.
react-native-tcp-socket is a CommonJS module (module.exports = { ... })
with no .default, so require('react-native-tcp-socket').default was
undefined and TcpSocket.connectTLS crashed at connect time. Read
mod.default ?? mod so both the CJS default-export and ESM interop work.
Every SDK fabricated a fallback client id 'appwrite-<project>-<random8>'
when the app did not call Client.setPushClientId(). The random suffix
changed every run, so the broker saw a new session each time and the
QoS-1 replay cursor never resumed. Send an empty client id instead: the
Appwrite broker derives a stable id server-side (keyed on the
credential/project), so replay resumes without the app setting one; an
explicit setPushClientId(id) still wins. Android omits the HiveMQ
identifier when empty; mqtt.js (Web/RN) respects an explicit empty
string, so no client-side id is generated.
Part 1 sent an empty client id for every SDK, but mqtt.js (Web + RN)
rejects an empty id with clean start off (MQTT requires a non-empty id
for a persistent session), so the CONNECT looped and WebChromium's push
e2e went red. The native SDKs tolerate an empty id (the broker derives a
stable one), so they keep it. For the mqtt.js SDKs, default the client id
to the userId decoded from the JWT, falling back to the session secret
(then the raw token) when there is no decodable JWT. That is stable per
user, so the broker's replay cursor resumes; Client.setPushClientId(id)
still overrides it.
Comment thread templates/web/src/services/push.ts.twig
@ArnabChatterjee20k

Copy link
Copy Markdown
Member Author

@greptile I think the bugs are fixed now and you can give a 5/5

Shorter, clearer name for the same per-subscription option (true -> QoS1
at-least-once, the default; false -> QoS0). Renamed across all six SDKs:
the subscribe option, the PushSubscription.update parameter, the internal
field, and the docs.
When the app has not called Client.setPushEndpoint(), derive the broker
from the regular endpoint on a 'push.' subdomain (push.<host>), mirroring
how realtime derives its endpoint from the client endpoint. An explicit
setPushEndpoint(url) still wins. Applied across all six SDKs (host prefix
for the native TCP transports, scheme://push.<host> for the browser WS
transports).
Comment thread templates/web/src/services/push.ts.twig Outdated
… realtime

endpointUrl() fell back to the independently-configurable realtime
endpoint before the regular endpoint, so an app with a custom realtime
endpoint (e.g. wss://realtime.example.com) would connect push to
push.realtime.example.com. Derive the push. default from the regular API
endpoint only; endpointPush still overrides.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants