Configuration
The product notification/ folder
Section titled “The product notification/ folder”Delivered through meta:
notification/ providers/providers.yaml # which provider backs each channel templates/ # one YAML per template (flat — subfolders are ignored) plugins/ extend/ # entity overlaysTemplate and provider changes are picked up on the tenant’s next meta refresh; duplicate template names are rejected at load time.
Templates
Section titled “Templates”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:
| Channel | Keys |
|---|---|
email | subject, html / html_file, text / text_file, sender_name, sender_email |
sms | body, sender_id |
voice | ssml, text, voice_id |
whatsapp | template_id, language, components[].{type, sub_type, index, params} |
push | title, body, template_id, sound, image_url, data, badge, actions[], apns_override, fcm_override |
inapp | title, message |
webhook | body |
Values are Liquid — {{firstname}} resolves against the request’s data object.
A real template from a product repo:
name: magictemplatecontent: text: "Please log in using this link: {{magic_link}}" subject: Login Linktemplatetype: text/htmlcategories: - emailrender: trueSet render: false to send a template’s content verbatim without Liquid evaluation.
Which provider backs each channel
Section titled “Which provider backs each channel”notification: email: { type: sendgrid } sms: { type: twilio } androidpush: { type: onesignal } iospush: { type: onesignal } webpush: { type: onesignal } webhook: { type: rest }Tenant configuration
Section titled “Tenant configuration”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 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:
| Provider | Required | Optional |
|---|---|---|
sendgrid | apikey, from_name, from_email | reply_name, reply_email |
twilio | account_sid, auth_token, from_number (E.164) | |
textlocal | apikey | sender (default TXTLCL) |
onesignal | app_id, api_key | |
rest | — | accepts 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 sweepsService (cluster) configuration
Section titled “Service (cluster) configuration”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.
Boot configuration
Section titled “Boot configuration”Generate a starting file with notify.svc generate:
port: 8080log: stdoutname: notifydbpools: - name: mainpool dbserver: db.internal:5432 database: appdb dbuser: appuser password: vault:notify-dsn maxconnections: 10timeouts: { 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 bootProvider credentials deliberately do not belong here — they are per-tenant.