Keep notification types in your repo.
Review template changes in pull requests, apply them on deploy, and promote them from a staging workspace to production.
The key needs notify.type.manage. The console’s type editor and these calls write the same records, with a version history per template you can revert.
1
Declare and apply
PUT /notification-types/by-key/{key} (replaceByKey) sets a type and its complete template set in one transaction: templates you leave out are deleted, unchanged ones keep their version. Applying the same body again changes nothing, so it is safe in every deploy. Categories must exist first.
// notify/types.ts: reviewed in pull requests like any other code.
import { Notify } from "@productcraft/notify";
const notify = new Notify();
type TypeBody = Parameters<typeof notify.notificationTypes.replaceByKey>[1];
const categories = [
{ key: "security", name: "Security", transactional: true, defaults: { inbox: "mandatory", email: "mandatory" } },
{ key: "billing", name: "Billing", transactional: true },
{ key: "product", name: "Product updates", defaults: { email: "off" } },
] as const;
const types: Record<string, TypeBody> = {
"security.new_sign_in": {
name: "New sign-in",
category_key: "security",
critical: true, // ignores opt-outs and quiet hours (never bounces or complaints)
ttl_seconds: 3600,
routing: { always: ["inbox"], fallback: [{ channel: "push" }, { channel: "email" }] },
templates: [
{ channel: "inbox", locale: "en", body: { title: "New sign-in from {{city}}", body: "{{device}} at {{time}}. Not you? Secure your account." } },
{ channel: "inbox", locale: "pt", body: { title: "Novo início de sessão em {{city}}", body: "{{device}} às {{time}}. Não foi você? Proteja a sua conta." } },
{ channel: "push", locale: "en", body: { title: "New sign-in", body: "{{device}} in {{city}}", url: "https://app.example.com/security" } },
{ channel: "email", locale: "en", body: { mail_template_name: "security-new-sign-in" } },
],
},
};
// Apply on deploy. Both calls are upserts: running them twice changes nothing.
const existing = new Set((await notify.categories.list().toArray({ maxItems: 200 })).map((c) => c.key));
for (const c of categories) {
if (!existing.has(c.key)) await notify.categories.create({ ...c });
}
for (const [key, body] of Object.entries(types)) {
await notify.notificationTypes.replaceByKey(key, body);
}2
Check a template before shipping it
Preview renders every channel as a real recipient (their locale and profile) with your sample data, and reports missing variables and variables used on one channel but not another. Nothing is sent.
const type = (await notify.notificationTypes.list().toArray({ maxItems: 200 })).find((t) => t.key === "security.new_sign_in");
if (type) {
const preview = await notify.notificationTypes.preview(type.id, {
recipient: "user_123", // render as this recipient (locale, profile)
data: { city: "Lisbon", device: "Firefox on macOS", time: "10:42" },
});
console.log(preview); // rendered text per channel, plus missing-variable warnings
}3
Export and promote
GET /export returns every category, type and template, and the workspace settings, in the shape PUT …/by-key accepts: a backup, a diff in review, or a promotion from staging.
import { Notify } from "@productcraft/notify";
import { writeFileSync } from "node:fs";
const staging = new Notify({ workspaceId: process.env.STAGING_WORKSPACE_ID });
const production = new Notify({ workspaceId: process.env.PRODUCTION_WORKSPACE_ID });
const config = await staging.export.retrieve();
writeFileSync("notify-export.json", JSON.stringify(config, null, 2));
// Promote: every type in the shape PUT …/by-key accepts.
for (const t of config.notification_types) {
if (t.archived_at) continue;
const { key, archived_at: _archived, ...body } = t;
// Export enums are open (new channels may appear), so name the body type.
await production.notificationTypes.replaceByKey(key, body as Parameters<typeof production.notificationTypes.replaceByKey>[1]);
}