Skip to content

feat(messaging): add Appwrite Push adapter for MQTT 5 integration - #161

Open
ArnabChatterjee20k wants to merge 7 commits into
mainfrom
appwrite-mqtt-poc
Open

feat(messaging): add Appwrite Push adapter for MQTT 5 integration#161
ArnabChatterjee20k wants to merge 7 commits into
mainfrom
appwrite-mqtt-poc

Conversation

@ArnabChatterjee20k

@ArnabChatterjee20k ArnabChatterjee20k commented Aug 25, 2026

Copy link
Copy Markdown
Contributor

Appwrite Push (MQTT 5) adapter

Adds a Push adapter that talks to Appwrite's custom MQTT 5 broker, a minimal MQTT 5 control-packet codec (Helpers/MQTT), and a spawned mock broker for tests.

The adapter speaks the broker's enhanced-authentication CONNECT dialect: the credential rides in the Authentication Method/Data properties and the project in a projectId User Property. The broker scopes every topic by that project, so callers use bare device topics.

Publish — send()

use Utopia\Messaging\Adapter\Push\Appwrite;
use Utopia\Messaging\Messages\Push;

$adapter = new Appwrite(
    endpoint: 'broker.example.com:1883',
    projectId: '<projectId>',
    credential: $jwt,            // an Appwrite JWT (or a session secret)
    authMethod: 'appwrite-jwt',  // or 'appwrite-session'
    tls: true,
);

$response = $adapter->send(new Push(
    to: ['device-token-1', 'device-token-2'], // each published to appwrite/push/{token}
    title: 'Hello',
    body: 'World',
    data: ['key' => 'value'],
));

// $response['deliveredTo'], $response['results'][n]['status'] ...

Publishes are pipelined at QoS 1 (send a window of PUBLISHes, drain PUBACKs by packet id, refill), so throughput scales with socket bandwidth rather than round-trip latency.

Consume — consume()

The same enhanced-auth connection can subscribe. consume() subscribes to the given topics and invokes a callback for each message, until a message limit or timeout is reached (QoS 1 messages are acked):

$adapter->consume(
    topics: ['appwrite/push/device-token-1'],
    onMessage: function (array $message): void {
        // $message['topic'], $message['payload'], $message['qos']
    },
    limit: 1,
    timeout: 5.0,
);

This is what verifies broker fan-out end to end from the consumer side (e.g. a device receiving what a publisher sent).

Telemetry

An optional Telemetry adapter can be injected; it defaults to the base no-op counter:

new Appwrite(/* ... */, telemetry: $telemetry);

Codec — Helpers/MQTT

A transport-agnostic MQTT 5 codec: encode/parse CONNECT, CONNACK, PUBLISH, PUBACK, SUBSCRIBE, SUBACK, PINGREQ/RESP, DISCONNECT, and the v5 property block (User Properties, enhanced-auth, message expiry, …). Both the adapter and the tests build on it.

Tests

tests/Messaging/Adapter/Push/AppwriteTest.php drives the adapter against a spawned in-process mock broker (FakeBroker.php) that accepts the enhanced-auth CONNECT and acks QoS 1 publishes: device-topic publishes, pipelined PUBACK counting, expired-token reason codes, and the CONNECT property block (projectId + credential, not username/password).

ArnabChatterjee20k and others added 6 commits August 25, 2026 16:37
Align the ported MQTT 5 codec and Appwrite Push adapter with the monorepo's
stricter toolchain: pint (per preset, native_function_invocation, cast
spacing, trailing commas), rector 2.x (explicit bool compare, drop redundant
casts, empty()->=== [], readonly promoted props, instanceof over !== null),
and phpstan 2.x (cast unpack() result feeding the by-ref int $offset).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add a spawned in-process MQTT 5 broker (FakeBroker) that speaks just enough
protocol to accept the adapter's enhanced-auth CONNECT, ack QoS 1 PUBLISHes
(optionally rejecting tokens), and record what it saw. AppwriteTest drives the
publisher against it: device-topic publishes, pipelined PUBACK counting,
expired-token reason codes, and the enhanced-auth CONNECT (projectId +
credential in the property block, not username/password).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
encodeSubscribe() completes the subscriber role of the codec: one topic filter,
a configurable max QoS, and optional User Properties (e.g. the subId the broker
keys subscriptions on). Reserved fixed-header flags set to 0b0010 per spec.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…pter

The same enhanced-auth connection that publishes can now subscribe: consume()
connects, subscribes to the given topics (subId as a User Property), and invokes
a callback for each PUBLISH until a message limit or timeout is reached, acking
QoS 1. This is what verifies broker fan-out end to end from the consumer side.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The adapter overrode the constructor without chaining to the base Adapter, so the
send counter was never initialized and send() failed with "Typed property
$sendCounter must not be accessed before initialization". Call parent::__construct
and accept an optional Telemetry to inject, defaulting to the base no-op counter.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add MQTT::parseSuback() and have consume() inspect the SUBACK reason codes: a
code >= 0x80 (e.g. 0x87 Not Authorized from a broker ACL) now throws instead of
silently subscribing to nothing, so subscribe-side authorization is observable.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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.

1 participant