push sends a notification to every row of a table whose deviceToken column matches a query.
AppConfig.push must be configured before anything is sent; without it the call throws. See
Push Configuration.
final job = await push(
PushMessage(
title: 'New reply',
body: 'Someone replied to your post',
collapseKey: 'post:${after.id}',
data: {'postId': '${after.id}'},
),
table: 'device_tokens',
column: 'token',
platformColumn: 'platform',
where: In('user_id', recipientIds),
);
platformColumn names the column holding each row's platform, and it decides which transport carries the recipient: FCM, or APNs directly when
AppConfig.push.apns is set. Omit it only if FCM is configured — it is the route Zonai picks for a recipient with no platform, so an APNs-only app that omits it can route nobody. See
Device Tokens.
It is unlike the other side effects, in three ways#
It is awaited. mutate queues a write and returns nothing; push
returns a PushJobId. The wait is for the job row to be committed, not for the fan-out — a few milliseconds whether the recipient set is ten rows or a hundred thousand.
The id is not a delivery receipt. It says the job was written down. Nothing here, and nothing either push service offers, tells you a notification reached a phone. See What the job id means.
It outlives the request. A queued mutate is committed with the request. A fan-out keeps running long after the response was sent, and nothing about the request can stop it once it starts.
It needs no particular identity, either: a hook pushes the same for an admin, an ordinary user or an unauthenticated request, because the code deciding to send is yours. Which means a hook on a publicly-reachable operation is a publicly-reachable fan-out — see Who can send.
Call it from after* hooks, never before*#
A before hook runs prior to the write. A notification announcing something that may not happen cannot be recalled — there is no unsend.
Throwing from an after hook fails the request after the write has committed, and a job already enqueued still runs.
Recipients are a query, not a list#
where is a predicate Zonai runs and pages, so a large recipient set is never materialized in memory. The column must be declared with
$.deviceToken(...): the fan-out reads only the primary key, that column, and the platform column when you name one — and refuses a token column that is not a
deviceToken. See Device Tokens.
Dead tokens are Zonai's job, not yours#
When the transport reports a token permanently rejected, Zonai clears it and calls onPushRejected
first — a hook on the extension for the table the token lives on, not something you request here. A timeout is
not a dead token and never prunes. See Dead Tokens.