Notify guides
03 · Notification types as code

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
// 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.

preview.ts
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.

promote.ts
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]);
}