pex

Channels

Apex sends communications through three channels: email, in-app, and mobile push. Every communication declares which channels it can be sent on; every end-user has per-channel preferences; every audience can predicate on those preferences. This page maps the chain end-to-end so operators know what each surface controls.

The chain

The same system serves transactional sends, adaptive journeys, and one-off broadcasts — only the gating differs. Here's the pipeline a single send travels:

  1. End-user channel preference — set by the end-user via the public preferences page (/preferences/[token]) or by your code via the SDK. Stored as channelPreferences[channel]: boolean | undefined.
  2. Audience predicate — operator-authored. Optionally references the channel preference via channel_opted_in or channel_opted_out predicates. Filters the recipient list at audience-resolution time.
  3. Eligibility evaluator — runtime gate. Checks suppression, global opt-out, channel preference, journey-level overrides, quiet hours, and frequency caps. Runs once per (recipient, channel) pair, immediately before send.
  4. Send — the channel-specific delivery layer (SES for email, web/mobile push for push, in-app store for in-app). Records a CommunicationRecord keyed on messageId.

The audience predicate (step 2) is permissive by design — it surfaces who's eligible. The eligibility evaluator (step 3) is conservative — it blocks the send if anything's off. This split keeps the operator's audience-size estimate honest while preserving every regulatory and user-preference gate at the actual send moment.

Channel preferences

EndUserCommPreferences carries a per-channel boolean:

channelPreferences: {
  email: true,         // explicit opt-in
  in_app_push: false,  // explicit opt-out
  mobile_push: undefined, // not set yet
}

Three states matter:

  • true — explicit opt-in. The user actively chose this channel.
  • false — explicit opt-out. The user actively chose to NOT receive this channel.
  • undefined (or no record at all) — the user never set a preference. By default Apex treats this as opted-in for marketing — the same default the eligibility evaluator uses, so audience-size estimates match deliverable counts.

The "default opt-in" semantic is important: it means a freshly imported user can be included in an early-lifecycle audience without first being asked to set channel preferences. Jurisdictions that require express consent (CASL §6, GDPR Art. 7) should use the Strict consent checkbox in the audience builder to switch to explicit_optin mode (see below).

Audience predicates that touch channel preferences

The audience builder offers two channel-preference predicates:

Channel preference (channel_opted_in)

Includes users who CAN receive on a channel. Two modes:

  • any_not_optedout (default) — matches when the channel preference is true or undefined (no record). Mirrors the eligibility evaluator's default-opted-in semantics. Use this for general marketing audiences in jurisdictions where prior consent isn't required.
  • explicit_optin — matches ONLY when the channel preference is true. Required by CASL and GDPR for express-consent obligations. Toggled via the audience builder's "Strict consent (CASL/GDPR)" checkbox.

Re-engagement audience (channel_opted_out)

Includes users who EXPLICITLY opted out of a channel (channel preference is false). This is a regulatory red zone:

  • Workspace-admin only — the audience builder hides the entry for non-admins; the API gates the save.
  • Audit-logged — every save writes a JOURNEY_AUDIT entry with the operator's identity and the channel(s) targeted.
  • Right-to-object — GDPR Art. 21 and CASL §10 protect users who opted out from being targeted without a lawful re-engagement basis. Use this audience only with a regulator-defensible reason.

The predicate matches ONLY explicit false. Default-opted-in users (no record / undefined) are NOT included; otherwise an operator could re-target every user who never set a preference, which defeats consent.

Sends vs Recipients

When you send a comm to an audience, the SendMenu shows two numbers:

  • Sends — total (recipient × channel) units. Each one is a billable send and gets its own CommunicationRecord.
  • Recipients — unique humans who receive at least one channel.

These are the same number when you send on a single channel. They diverge as you add channels — a user who's on both email and push counts twice in Sends, once in Recipients. The gap line ("K recipients receive on multiple channels") makes this explicit.

The reach preview applies per-recipient channel-preference filtering at preview time, so the numbers match what the runtime will actually fan out to. Suppression, global opt-out, and quiet-hours gates run at SEND time and are not reflected in the preview — those are conditions that can change between preview and send (e.g. a fresh bounce flips the suppression bit).

Channel completeness

Every comm declares which channels it supports, but declaring a channel doesn't mean content is authored for it. The composer surfaces a per-channel Draft / Ready state:

  • Email is Ready when subject is set AND the body has at least one block beyond brand-header/brand-footer.
  • In-app is Ready when inAppTitle is set.
  • Push is Ready when pushTitle is set.

Anything else is Draft. Draft channels show as outline-style chips on template cards and are disabled in the SendMenu's channel picker (with a link back to the composer to author content). The runtime never sends Draft channels.

v1 limitations

  • Test sends are email-only. The composer's "Send test to me" / "Send test to specific address" run against the email channel regardless of the comm's declared channels. Other channels show as disabled in the test-channel picker; the audience tab supports all channels for real sends.
  • No k-anonymity floor on reach numbers. The Sends/Recipients preview shows the literal computed counts, even on small audiences. We chose this over flooring at, say, 20 recipients because operators told us obscured numbers eroded their trust faster than the privacy benefit warranted.
  • Channel preferences don't fan out. Setting email = false for a user disables email only — they may still receive in-app and push if those channel preferences are true or undefined. The global opt-out (separate field) disables every channel.