BackCore concepts

Fallback channels

Retry a failed delivery, then try another channel, so an important message is not silently lost.

Retries First

A failed email, SMS, or webhook delivery is first retried under the retry policy (3 attempts by default; see Delivery and retries). A fallback only comes into play once the channel has given up, or could not be used at all. Without a fallback, a delivery that gives up is marked failed, the delivery.failed hook fires, and that is the end of it.

Inbox Fallback

The simplest form is a single inbox item, written when any email, SMS, or webhook delivery fails for good:

notification({
	id: "invoice_ready",
	payload: { invoiceUrl: "string" },
	channels: [email({ subject: "Your invoice is ready", body: "{{invoiceUrl}}" })],
	fallback: inbox({ title: "Your invoice is ready", actionUrl: "{{invoiceUrl}}" }),
})

It only reacts to failed deliveries, not to missing addresses or preferences, and it is written only if the recipient allows inbox for this notification.

Rules

For more control, pass a list of rules. Each says when it applies and which channel to use:

notification({
	id: "security_alert",
	payload: { message: "string" },
	channels: [email({ subject: "Security alert", body: "{{message}}" })],
	fallback: [
		{
			if: "missing_address",
			from: "email",
			then: sms({ body: "{{message}}" }),
		},
		{
			if: "channel.failed",
			from: "email",
			then: inbox({ title: "Security alert" }),
		},
	],
})
FieldWhat it is
ifThe trigger, below
thenA channel config, built with the same factories as the notification's channels
fromOptional. Only apply when this channel type triggered it
TriggerFires when
channel.failedA delivery failed after all its retries
missing_addressThe recipient has no email for an email channel, or no phone for SMS
skippedThe recipient's preferences or your defaults turned the channel off

Rules are checked in order and the first match wins. A rule only matches if its then channel type is not one the notification already has, or one an earlier fallback already used, so a fallback always reaches the recipient somewhere new. If every channel is turned off but a skipped or missing_address rule matches, the send goes ahead for the fallback instead of being suppressed.

How Fallbacks Behave

  • One step only. If the fallback delivery itself fails, no further fallback is tried. There is no email, then SMS, then inbox cascade; put your most reliable channel in then.
  • Preferences still apply. The fallback channel is checked like any other: if the recipient turned that channel off, or has no address for it, nothing is sent. A required notification overrides preferences here too.
  • Quiet hours do not. A fallback email, SMS, or webhook is sent right away, even inside the recipient’s quiet hours.
  • Retries apply. A fallback delivery gets the same retry policy.
  • Timing depends on the trigger. missing_address and skipped fallbacks run inside send(). A channel.failed fallback runs after the last retry, which with an asynchronous queue is after send() has returned.

Startup Checks

Fallbacks are validated when createNotifyKit() runs. It throws if a fallback targets a channel with no provider configured, uses an unknown trigger or channel type, or references a template variable missing from the payload. It warns if the list is empty, if from names a channel the notification does not have (the rule can never match), or if from and then are the same type.

Writing Fallback Content

  • Keep the action. If the email had a link, give the inbox item the same actionUrl, so the recipient can still act.
  • Fit the channel. An SMS fallback for a long email should be a short line and a link, not the whole body.
  • Do not mention the failure. “We couldn’t email you” is noise; say what the message was going to say.

Debugging

A fallback that ran leaves a fallback.triggered event in the timeline, naming the trigger and the channel it came from. If one did not fire, check in this order:

  1. Did the primary channel actually fail, or was it held, deduplicated, or rate-limited?
  2. Does a rule match that trigger, and its from, if set?
  3. Is the then channel type already one of the notification’s channels?
  4. Does the recipient allow that channel, and have an address for it?

In tests, the fake providers can fail on demand: fakeEmailProvider({ failOnNext: true }), or setFailOnNext(true) on an existing one. Set retry: { maxAttempts: 1 } so the failure is final. See Testing and dev mode.