Norq

Providers

Norq compiles a template into a channel payload, then a provider turns that payload into an HTTP request that the host (CLI or SDK) executes. Ten providers ship in the box — covering email, SMS, push, and the SuprSend hub — and any other delivery service plugs in as a custom HTTP-adapter provider defined in norq.config.yaml.

How providers fit in

Every send() call follows the same three steps:

  1. WASM compile. The template is rendered against runtime data into a channel-specific payload (html, text, body, blocks, …).
  2. Provider prepare. The provider’s prepare_send builds a ProviderRequest — method, URL, headers, JSON body, and a typed Auth value.
  3. Host execute. The CLI or SDK signs the auth, performs the HTTP call, and reads the response per the request’s response_hints (success status codes, message_id JSONPath, retryable status codes).

Providers never execute HTTP themselves. That split keeps secret-bearing crypto (FCM OAuth2 token exchange, APNs JWT signing, VAPID Web Push encryption) inside language-specific SDKs, while the request shape lives in WASM and is shared across CLI, Node, Python, Go, Java, and Ruby.

Routing

Map channels to providers in norq.config.yaml:

routing:
  email: resend
  sms: twilio
  push: fcm

Push additionally accepts per-platform keys, useful when different platforms route to different services:

routing:
  push: fcm                    # fallback / single-key shorthand
  push.ios: apns               # iOS tokens go to APNs
  push.android: fcm            # Android tokens go to FCM
  push.web: webpush            # Web subscriptions go to VAPID Web Push

Resolution order

For each send Norq walks this priority list and picks the first match:

  1. Per-token override. A recipient push token may set provider: <name> to bypass routing entirely (handy for multi-app workspaces with two FCM projects).
  2. routing.push.<platform> — platform-scoped entry, only consulted for push.
  3. routing.<channel> — channel-wide default.
  4. Single-provider auto-fallback. If exactly one configured provider supports the channel, Norq uses it. Good first-run UX — you don’t have to write routing: to send a single email.
  5. Error. provider/no-route-for-<channel> (or provider/no-route-for-push.<platform> for push).

norq doctor validates routing at config-load time and prints a per-channel table showing which provider was selected and how.

Built-in providers

Provider Channels Auth Source
Resend email API key (Bearer) resend.com
SendGrid email API key (Bearer) sendgrid.com
Twilio sms HTTP Basic twilio.com
Bird sms AccessKey scheme bird.com
FCM push (ios, android, web) Google service-account OAuth2 Firebase
APNs push (ios) Apple .p8 ES256 JWT Apple
Expo push (ios, android) optional Bearer Expo Push Service
Web Push push (web) VAPID (P-256 ECDSA + ECDH) RFC 8030 / 8291 / 8292
SuprSend all + render workspace key/secret (Bearer) SuprSend hub
console all none — debug output to stdout built-in

norq provider list shows each as configured (config), configured (env), credentials missing, or not configured so you can tell at a glance whether a provider was loaded from norq.config.yaml, from NORQ_<NAME>_* env-var fallbacks, or not loaded at all.

Credential resolution

Sensitive config values — API keys, FCM service-account JSON, APNs .p8 PEM, VAPID private keys, custom-provider secret: true fields — accept three forms:

  • Env-var reference. ${VAR_NAME} is replaced with process.env.VAR_NAME at YAML load time. Unset variables resolve to the empty string and are treated as “not configured” (the provider returns Ok(None) instead of failing), so you can keep a config block in source and only populate it in environments that should send.
  • File path. Recognised by leading ./, ../, /, or ~/, or by a .json, .p8, .pem, or .key extension. The file is read at load time and its contents become the config value. ~/ expands against $HOME. A missing file is a hard error — Norq never silently falls through to treating the path as a literal string.
  • Inline literal. Anything else is the value as-is — typical for short tokens like Expo’s accessToken.

The resolver lives in crates/core/src/provider/secret.rs and is shared across every built-in and custom provider. See Secret values for the canonical reference.

Idempotency

Every send() call carries an idempotency_key. When the caller does not supply one, the SDK or CLI auto-generates a UUID v4 and applies the same key to every channel in a multi-channel send. Provider behaviour varies:

Provider Idempotency surface Real dedup?
Resend Idempotency-Key: header yes
SuprSend Idempotency-Key: header yes
APNs apns-id header (when key is a UUID) yes (within a short window)
SendGrid custom_args.idempotency_key body field no — correlation only
Twilio not surfaced no
Bird not surfaced no
FCM X-Norq-Idempotency-Key header no — observability only
Expo not surfaced no
Web Push not surfaced no
Console printed envelope no

Supplying a stable user-derived key (order-shipped-ORD-123) extends real dedup across retries and processes on the providers that support it; on the others it stays useful as a log/webhook correlation token.

Batch send

batch() prepares a template once and renders it per recipient via WASM. Most providers don’t expose a batch endpoint — the SDK iterates and calls prepare_send per recipient. The exceptions:

Provider Native batch Max items per request
Resend POST /emails/batch 100
SuprSend POST /v1/notification/trigger/bulk 1000
All others

The provider’s max_batch_size() declares the limit; the SDK auto-flushes at that boundary, so a 250-recipient batch() against Resend turns into three HTTP calls (100 + 100 + 50) without you having to chunk manually.

Multi-channel send

Most providers handle exactly one channel. SuprSend is the exception — its single /v1/notification/trigger endpoint accepts a channels array covering email + SMS + push + Slack + WhatsApp + MS Teams in one request. The SDK detects this via supports_multi_channel_send() and skips the per-channel iteration loop.

Render operations

norq preview and norq screenshot build images of compiled email output. These are render operations, not send operations, and require a render-capable provider. Today only SuprSend declares RenderCapability (Preview and Screenshot, email channel only).

Error codes

A short reference for the most common provider errors. The full catalogue with causes and fixes lives at Error reference.

Code Meaning
provider/no-route-for-<channel> No provider configured for the channel — add routing.<channel>: <name>.
provider/no-route-for-push.<platform> No push provider for this platform — add routing.push.<platform> or stamp the recipient token with provider:.
provider/cli-http2-required CLI tried to deliver via APNs — APNs requires HTTP/2; use an SDK.
provider/cli-oauth2-not-supported CLI tried to deliver via FCM — FCM requires the OAuth2 token exchange; use an SDK.
provider/cli-vapid-not-supported CLI tried to deliver Web Push — VAPID encryption isn’t bundled in the CLI; use an SDK.
provider/missing-vapid-keys A Web Push recipient token is missing keys.p256dh and keys.auth (RFC 8291).
provider/fcm-legacy-server-key Configured FCM with the retired serverKey field (Google sunset 2024-06-20) — migrate to serviceAccount.

Per-provider error codes (e.g. push/invalid-priority, push/missing-platform) are documented on each provider’s page.

See also

  • Custom providers — define HTTP-adapter providers in YAML for any service the built-ins don’t cover.
  • Project configuration — the full norq.config.yaml shape including providers:, routing:, and credential resolution.
  • Error reference — every coded error with cause and fix, including the runtime push and provider codes.
  • CLI referencenorq provider list, norq provider routing, norq doctor.