Skip to content
On this page

Channels

Email, in-app and push, web, iOS, macOS, Android and Windows. Each channel is set up from the dashboard, and is on once it has settings.

Every channel starts off. It turns on when you save its settings on the Channels page of the dashboard, and each type’s template turns it on for that type. Removing a channel’s settings turns it off again: its sends skip it as channel_not_configured, and the deliveries it still had queued fail with that reason. A pause is something else: it holds a channel’s deliveries until you resume it; see Pause a channel.

Settings are saved only through the dashboard, so a new environment sends nothing, not even in-app, until Cloudflare Access covers its URL and someone saves its channels; under wrangler dev, the identity under access.dev signs you in. Each environment’s Chaski keeps settings of its own, and the push credentials among them are encrypted with CHASKI_SECRET. A setting that stops working, such as credentials saved under another CHASKI_SECRET, shows its channel as Failing until you save it again. Configuration lists every field.

Email

Email goes out through Cloudflare Email Service, from a domain you onboarded to it, through the CHASKI_EMAIL binding. Set its sender address, and optionally a sender name and a reply-to address. The address must be one the binding allows. The page says when the binding is missing.

A send can override the sender name and the reply-to address, and attach up to 10 files when CHASKI_ATTACHMENTS holds an R2 bucket. The HTML of every email carries a pixel, served through your app’s userRoutes(), which marks the delivery opened when the message is read; set CHASKI_PUBLIC_URL to that mount, and without it emails carry no pixel.

A contact without an email address skips the channel with missing_email.

In-app

The in-app channel keeps a feed per contact in the ChaskiFeed Durable Object, and pushes each new item to their open pages over a WebSocket. It has no settings: turn it on, with CHASKI_FEED bound. Your frontend reads the feed through your app’s userRoutes(), as Browser clients shows; on the server, env.CHASKI.feed() lists it, counts the unread items and marks them read.

Push

Push reaches browsers through web push, Apple devices through APNs, Android devices through FCM and Windows devices through WNS. Set up any of them; push is on once one is.

  • Web push takes a contact, a mailto: or https: address the push services can reach you at, and a key pair: Generate keys makes one, and Use existing keys takes the pair you already send with.
  • APNs takes your team id, the key’s id and the .p8 file, which send to every app of your team. The iOS app and the macOS app each take their bundle ID and the APNs environment of their tokens: Sandbox for development builds, Production for TestFlight and App Store builds.
  • FCM takes a service account’s JSON key file, from Project settings → Service accounts in the Firebase console. The project it names must be the one your Android app registers its tokens with.
  • WNS takes the Microsoft Entra app registration your Windows app creates its push channel with, through the Windows App SDK’s PushNotificationManager: its tenant id, its client id and one of its client secrets.

A contact receives push on the devices registered to them. Browsers register with client.push.enable() from chaskijs/client; your app can register an iOS or macOS device token, an Android FCM token or a Windows channel URI from the server:

TypeScript
await env.CHASKI.push().register({
  recipient: { id: user.id },
  platform: 'android', // or 'ios', 'macos', 'windows'
  deviceToken,
});

A mobile app can also register its own token through your app’s userRoutes(), with client.push.registerDevice(), as the signed-in user.

An Apple token belongs to the environment of the build that made it: a development build’s token only works in sandbox. Pass environment: 'sandbox' when that build registers, and the app’s environment in the settings applies to every other device. When APNs refuses a token as BadDeviceToken or DeviceTokenNotForTopic, Chaski forgets the device and logs the reason to Workers Logs: the token may be fine and the environment or the bundle id wrong.

An Electron app on macOS gets its token from pushNotifications.registerForAPNSNotifications() in the main process, and registers it with client.push.registerDevice({ platform: 'macos', deviceToken }), as Browser clients shows.

On Windows, the device token is the channel URI WNS hands the app. Chaski only accepts one on notify.windows.com, since it sends its access token there. The push arrives as a toast whose launch arguments carry the href and tag, which the Windows App SDK hands the app as AppNotificationActivatedEventArgs.Arguments when the user clicks it. WNS channels expire after 30 days, so the app registers its channel again on every launch; Chaski forgets one WNS answers 404 or 410 for.

A send with push: { silent: true } shows its alert without a sound, on Apple devices, on Windows and in browsers. On Android, the notification channel your app creates decides the sound.

Every push carries the title, the body, the link (href) and a tag: the send’s push.collapseId, or the notification id, which the feed item also carries as notificationId. A push with the tag of one already shown replaces it, on Apple devices, Android and in browsers. The send’s data comes along as is:

PlatformWhere href, tag and data arrive
Web pushThe push’s JSON: { title, body, href, tag, silent, data }.
iOS and macOSCustom keys next to aps, one per data key.
AndroidThe message’s data, one per data key.
WindowsThe toast’s launch arguments, one per data key.

Give your app’s own notification of the same event the same id, collapseId or the notification id, and the system shows one of the two. An app that shows the feed’s items itself while it runs can also take its device out of push while its feed is open; see Browser clients.

On Android the notification shows on its own when the app is in the background; the href and tag of the push arrive in the intent’s extras when the user taps it.

A contact without a registered device skips the channel with no_device_tokens.

Updated Oct 30, 2026

MIT License · chaski.sh