Notify guides
07 · A preferences screen

Let users choose what reaches them.

Categories with sensible defaults, a preference centre in your settings page, quiet hours, and an unsubscribe link that does what it says.


1

Design the categories

Users choose per category, not per type, so keep categories few and named the way a user thinks (“Billing”, not invoice.paid). Up to 50 per workspace.

categories.ts
// A channel left out of defaults is "on". "mandatory" cannot be turned off
// by the recipient (the switch shows locked); use it sparingly.
await notify.categories.create({
  key: "security",
  name: "Security",
  description: "Sign-ins and password changes",
  transactional: true, // no List-Unsubscribe header on its emails
  defaults: { inbox: "mandatory", email: "mandatory", push: "on", webpush: "on" },
  ordinal: 0,
});
await notify.categories.create({
  key: "digest",
  name: "Weekly digest",
  defaults: { email: "on", push: "off", webpush: "off" },
  ordinal: 10,
});

2

The drop-in preference centre

A table with a switch per category and channel. Each switch saves only its own cell, at once, and rolls back with a message if the save fails. Mandatory cells are locked with the reason. It also offers “turn off all optional notifications”, the time zone and quiet hours.

NotificationSettings.tsx
"use client";
import { NotifyPreferences, NotifyProvider } from "@productcraft/notify-react";
import "@productcraft/notify-react/styles.css";

export function NotificationSettings({ userId }: { userId: string }) {
  return (
    <NotifyProvider key={userId} getToken={() => fetch("/api/notify-token").then((r) => r.text())}>
      {/* A switch per category × channel, mandatory cells locked,
          "turn off all optional notifications", time zone and quiet hours. */}
      <NotifyPreferences channels={["inbox", "email", "push", "webpush"]} />
    </NotifyProvider>
  );
}

3

Or your own UI

preferences.ts
import { createNotifyClient, resolvePreference } from "@productcraft/notify-js";

const notify = createNotifyClient({ getToken: () => fetch("/api/notify-token").then((r) => r.text()) });

const prefs = await notify.preferences.get();
for (const category of prefs.categories) {
  const email = resolvePreference(prefs, category.key, "email");
  console.log(category.name, email.enabled, email.mandatory ? "(required)" : "");
}

// Only this cell changes, so two open tabs never overwrite each other.
await notify.preferences.update([{ scope: "category", key: "digest", channel: "email", enabled: false }], {
  timezone: "Europe/Lisbon",
  quiet_hours: { start: "22:00", end: "08:00" },
});

4

From your backend

Support can change a user’s preferences with the API key. Every change, from any source (the user, your backend, an unsubscribe link, a spam complaint), is kept as consent evidence.

support.ts
// Each change is recorded with its source, as consent evidence.
await notify.recipients.preferences.update("user_123", {
  preferences: [{ scope: "category", key: "digest", channel: "email", enabled: false }],
});
const current = await notify.recipients.preferences.retrieve("user_123");
console.log(current.preferences, current.unsubscribed_all);

5

Email unsubscribe

Every email of a non-transactional, non-critical category carries a one-click List-Unsubscribe header, as Gmail and Yahoo require of bulk senders. Using it turns that category’s email off for that recipient, nothing more: security mail still arrives.