Skip to content
Talk to our solutions team

Configuration

Delivered through meta:

notification/
providers/providers.yaml # which provider backs each channel
templates/ # one YAML per template (flat — subfolders are ignored)
plugins/
extend/ # entity overlays

Template and provider changes are picked up on the tenant’s next meta refresh; duplicate template names are rejected at load time.

A template declares per-channel variants, so one name covers every channel it may be delivered on:

templates:
- name: welcome-onboarding
categories: ["transactional"]
variants:
email:
subject: "Welcome, {{firstname}}!"
html_file: templates/welcome.html # loaded via meta, not inlined
text: "Welcome {{firstname}} — get started at {{url}}"
sms:
body: "Welcome {{firstname}}!"
sender_id: KISAI
inapp:
title: "Welcome!"
message: "Tap to get started"

Per-channel keys:

ChannelKeys
emailsubject, html / html_file, text / text_file, sender_name, sender_email
smsbody, sender_id
voicessml, text, voice_id
whatsapptemplate_id, language, components[].{type, sub_type, index, params}
pushtitle, body, template_id, sound, image_url, data, badge, actions[], apns_override, fcm_override
inapptitle, message
webhookbody

Values are Liquid — {{firstname}} resolves against the request’s data object.

A real template from a product repo:

notification/templates/magiclink_email.yaml
name: magictemplate
content:
text: "Please log in using this link: {{magic_link}}"
subject: Login Link
templatetype: text/html
categories:
- email
render: true

Set render: false to send a template’s content verbatim without Liquid evaluation.

notification/providers/providers.yaml
notification:
email: { type: sendgrid }
sms: { type: twilio }
androidpush: { type: onesignal }
iospush: { type: onesignal }
webpush: { type: onesignal }
webhook: { type: rest }

Provider credentials and routing live in tenant config, never in the boot file.

notify:
default_country: "US" # used to normalize phone numbers
providers:
sendgrid:
apikey: vault:sendgrid-apikey
from_name: Acme
from_email: [email protected]
twilio:
account_sid: vault:twilio-sid
auth_token: vault:twilio-token
from_number: "+14155551234"
rate_limit:
per_second: 200 # per-tenant override of the provider default
routing:
email:
rules:
- default: { primary: sendgrid }
sms:
rules:
- match: { country: ["IN", "NP", "BD"] }
primary: textlocal
fallback: [twilio]
- default: { primary: twilio }

Routing rules are evaluated in order. A rule may match on country, platform, or template_categories; the first match wins and supplies primary plus an ordered fallback list. A default rule (map or bare string) catches the rest. With no rules configured at all, the first registered provider for the channel is used.

Required credential fields per provider:

ProviderRequiredOptional
sendgridapikey, from_name, from_emailreply_name, reply_email
twilioaccount_sid, auth_token, from_number (E.164)
textlocalapikeysender (default TXTLCL)
onesignalapp_id, api_key
restaccepts arbitrary extra keys

Fields marked secret are redacted in logs.

Queue sweep intervals are read from the tenant config root:

daemon:
pendingdelay: 10 # seconds between pending sweeps
faileddelay: 100 # seconds between retry sweeps
notify:
inbound: # inbound HTTP rate limits — opt-in per class
notify_send: { per_second: 100 }
notify_send_fanout: { per_second: 10 }
notify_inapp_read: { per_second: 500 }
ratelimit_store:
kind: tiered # memory (default) | distrib | tiered
distrib: { backend: dragonfly, dsn: "redis://cache.internal:6379" }
tiered: { local_capacity_ratio: 0.1, sync_interval_ms: 1000, on_store_failure: fail_open }

Rate-limit classes: notify_send, notify_send_fanout, notify_inapp_read, notify_inapp_write, notify_metadata_read, notify_metadata_write. A class with no configuration is not limited — there are no implicit defaults.

Generate a starting file with notify.svc generate:

port: 8080
log: stdout
name: notify
dbpools:
- name: mainpool
dbserver: db.internal:5432
database: appdb
dbuser: appuser
password: vault:notify-dsn
maxconnections: 10
timeouts: { read: 15, write: 15, readheader: 5 }
default:
dbpool: mainpool
dbschema: "[{name}],public"
domain: ["[{name}].example.com"]
daemon: { pendingdelay: 10, faileddelay: 100 }
loadtenants: [] # tenants to warm at boot

Provider credentials deliberately do not belong here — they are per-tenant.