Documentation

Broadcasts multi-canaux

Envoyer une annonce à un segment d'utilisateurs sur email + push + in-app simultanément.

Les broadcasts permettent à un admin d'envoyer un message à un segment d'users sur plusieurs canaux en même temps : email (Resend), push (OneSignal), notification in-app (table Notification).

Page admin : /admin/broadcasts (raccourcie sous /broadcasts).

Workflow admin

  1. Composer — éditeur rich-text multi-locale (subject / body par locale dispo dans la base users), avec preview.
  2. Cibler une audience — voir tableau plus bas.
  3. Choisir les canauxinapp, email, push (au moins un).
  4. (Optionnel) Court titre/corps — override pour push (≤120 chars) et in-app (≤180 chars). Sinon le subject/body email est tronqué auto.
  5. Test — bouton "Envoyer un test" envoie le broadcast à l'admin uniquement (Gmail, iOS push truncation, etc.).
  6. Envoyer — POST avec sendNow=true ou save en draft (envoyer plus tard).

Audiences

AudienceFiltre
allTous les users vérifiés non-bloqués
paidSubscription.status["active", "trialing"]
freePas de subscription ou status != active/trialing
active30dAu moins une UserSession.lastActiveAt ≥ J-30
inactive60dAucune session active depuis 60 jours
countryUserSession.country = <code> (filtre audienceFilter.country)

Logique dans lib/broadcasts.ts → audienceWhere(). Étends-le pour ajouter des segments custom (par tag user, par cohorte, etc.).

Race & atomicité

executeBroadcast(broadcastId) claim atomique :

const claimed = await prisma.broadcast.updateMany({
  where: { id: broadcastId, status: "draft" },
  data: { status: "sending" },
});
if (claimed.count === 0) throw new Error("broadcast_not_draft");

Un double-clic ou une exécution cron concurrente ne déclenche pas deux envois — seul le premier appelant flip draft → sending, les autres bail out.

Sanitization HTML

Subject + body sont passés par sanitizeLocalized() à la création/PATCH — voir Sanitization. Pas de risque XSS dans les emails / in-app.

Email logging

Chaque envoi email d'un broadcast crée une row EmailLog avec templateKey="broadcast". Consultable dans Email logs. Si l'envoi throw (réseau Resend), on log quand même avec errorMsg.

Push (OneSignal)

Voir Push notifications. En résumé :

import { sendPushNotification } from "@/lib/onesignal";

await sendPushNotification({
  userIds: [u.id],
  title: shortTitle,
  message: pushBody,
}).catch((e) => console.error("[broadcast] push failed:", e));

Push échoué = log + on continue (pas de retry, pas de fail du broadcast). Configure ONESIGNAL_APP_ID + ONESIGNAL_REST_API_KEY en env pour activer.

In-app

Crée une row Notification(userId, title, message). Le composant <NotificationBell> (header) la lit en temps réel et affiche un badge.

Modèle de données

model Broadcast {
  id              String   @id @default(cuid())
  subject         Json    // Record<locale, string>
  body            Json    // Record<locale, string>  (rich text HTML)
  shortTitle      Json?   // Override push/in-app
  shortBody       Json?
  channels        String[]   // ["inapp"|"email"|"push"]
  audience        String     // "all" | "paid" | …
  audienceFilter  Json?
  status          String     @default("draft")  // draft | sending | sent | failed
  totalRecipients Int        @default(0)
  sentCount       Int        @default(0)
  errorCount      Int        @default(0)
  sentAt          DateTime?
  createdById     String
  createdAt       DateTime   @default(now())
}

Routes API

GET    /api/admin/broadcasts                → liste paginée
POST   /api/admin/broadcasts                → create + sendNow optionnel
       ?preview=1                           → renvoie juste le count audience
GET    /api/admin/broadcasts/[id]
PATCH  /api/admin/broadcasts/[id]           → édit draft + sendNow optionnel
DELETE /api/admin/broadcasts/[id]           → drop draft (sent broadcasts conservés)
POST   /api/admin/broadcasts/test           → test à l'admin only

Toutes admin-only (requireApiAuth({ admin: true })) avec audit (broadcast.create, broadcast.send_test, etc.).

Allez plus loin

On this page