Norq

Push Channel

Norq compiles push templates to a structured JSON payload with title, body, image, and action URL. The payload works with FCM, APNs, and web push providers.

Pipeline: Markdown -> AST (parse tree) -> { title, body, image, action_url, platforms } JSON

Template files

  • push.md – Markdown mode
  • push.json – Native JSON mode (for custom payload structures)

Frontmatter

---
title: "Order shipped!"
enabled: true
ios:
  sound: "default"
  badge: 1
android:
  channel_id: "orders"
web:
  icon: "https://cdn.example.com/icon.png"
---
Field Required Description
title No Notification title. Fallback: :::header content, then first heading. (lowest priority – see Title resolution priority)
enabled No true (default) or false.
ios No iOS overrides. Allowed keys: sound, badge, category, thread-id, mutable-content, content-available, interruption-level. Unknown keys fire push/unknown-ios-key; invalid interruption-level values fire push/invalid-interruption-level.
android No Android overrides. Allowed keys: channel/channel_id, priority (normal|high), color, icon, sound, tag, ttl. Unknown keys fire push/unknown-android-key; invalid priority values fire push/invalid-priority.
web No Web Push overrides. Allowed keys: icon, badge, image, requireInteraction, actions, tag, renotify. Unknown keys fire push/unknown-web-key.

Directive compilation

Directive Push output
:::header Title source (highest priority – see Title resolution priority)
:::footer Ignored
:::action action_url extracted from the first link
:::callout Body text with warning symbol prefix
:::hero image URL extracted for the push image field
:::fields Body text lines (“Key: Value”)
:::media Rendered as text (graceful degradation)
:::columns Stacked as text
:::list Text lines
:::highlight Body text with star prefix
:::centered Plain text (no alignment in push)
:::raw Content extracted as text (graceful degradation)

Example

---
title: "Order #{{order.id}} shipped"
---
 
::: hero
![](https://cdn.example.com/shipped-banner.png)
:::
 
Hey {{user.first_name}}, your order is on its way!
 
::: action
[Track Order]({{tracking_url}}){primary}
:::

Compiled output

Norq compiles the above template into a structured push payload:

{
  "title": "Order #ORD-123 shipped",
  "body": "Hey Gaurav, your order is on its way!",
  "image": "https://cdn.example.com/shipped-banner.png",
  "action_url": "https://track.example.com/123",
  "platforms": {
    "ios": { "sound": "default", "badge": 1 },
    "android": { "channel_id": "orders" },
    "web": { "icon": "https://cdn.example.com/icon.png" }
  }
}

Title resolution priority

The push title is resolved in this order:

  1. Content of :::header directive
  2. First heading (#, ##, etc.) in the body
  3. title field in frontmatter
  4. Empty string (linter warns)

Sending push notifications

Token shape

A recipient’s push.tokens is a list. Each entry must declare platform. Web Push tokens additionally require encryption keys.

{
  "push": {
    "tokens": [
      { "token": "abc123", "platform": "ios" },
      { "token": "def456", "platform": "android" },
      {
        "token": "https://updates.push.services.mozilla.com/wpush/v2/...",
        "platform": "web",
        "keys": { "p256dh": "...", "auth": "..." }
      },
      { "token": "ExponentPushToken[xxx]", "platform": "ios", "provider": "expo" }
    ]
  }
}
Field Required Notes
token yes Provider-specific token; for Web Push it is the subscription endpoint URL.
platform yes One of ios | android | web. Missing or invalid values fire push/missing-platform / push/invalid-platform.
provider no Overrides channel-level routing for this token.
keys iff web { p256dh, auth } from the browser’s PushSubscription. Missing keys fire push/missing-vapid-keys.
environment no APNs only. sandbox or production — overrides the provider-level production: flag for this token (useful for routing TestFlight builds at sandbox while the provider defaults to production). Invalid values fire push/invalid-environment.

Routing precedence

For each token, the provider is resolved highest-specificity first:

  1. token.provider — explicit per-token override.
  2. routing[push.<platform>] — e.g. push.ios: apns.
  3. routing[push] — channel-wide default.
  4. Single-provider fallback — if exactly one push-capable provider is registered, use it automatically (good first-run UX for projects that only have FCM configured).
  5. otherwise: error provider/no-route-for-push.<platform>.

Built-in providers

Provider Auth Platforms Notes
fcm Google service account ios, android, web (Chrome/Edge) One endpoint, three platforms via apns/android/webpush overrides.
apns Apple p8 JWT ios HTTP/2 only — not callable from CLI.
expo Bearer (optional) ios, android Single endpoint; Expo proxies to FCM/APNs.
webpush VAPID web All browsers (Firefox, Safari, Chrome, Edge).

Testing

tests:
  - name: "Push payload under 4KB"
    channel: push
    sample: "New user"
    assert:
      payload_bytes: { lte: 4096 }
 
  - name: "Push has title"
    channel: push
    sample: "New user"
    assert:
      body: { not_contains: "" }

Best practices

  • Keep push body under 100 characters for good display on all platforms
  • Use :::hero for the notification image (extracted automatically)
  • The first :::action link becomes the tap-through URL
  • Use platform overrides in frontmatter for platform-specific behavior (sounds, badges)
  • Push content is plain text – bold/italic formatting is stripped