Skip to content

Broadcast Lifecycle & AI Moderation Architecture

This document explains how the Listserv application unifies administrative announcements and user community posts into a single Broadcast entity, governed by an XState 5 lifecycle state machine and evaluated by an AI Moderation Agent.


1. The Consolidated Broadcast Entity

Earlier listserv implementations maintained separate data models for administrative Campaigns and user Submissions. The current architecture unifies both under a single Firestore collection (app/listserv/broadcast) and schema (BroadcastS).

Discriminators

  • origin: 'admin' (created manually in the admin dashboard) vs. 'user' (submitted via web or email command).
  • template: 'newsletter', 'alert', or 'community'.
  • state.lifecycle: Persisted XState machine context tracking lifecycle progress.

2. XState 5 Lifecycle Machine

The broadcast pipeline is governed by a strict state machine implemented in XState v5 on the client and mirrored on the server:

text
┌───────┐      SUBMIT      ┌───────────┐   AI Evaluation   ┌───────────┐
│ draft │ ───────────────▶ │ submitted │ ────────────────▶ │ moderated │
└───────┘                  └───────────┘                   └─────┬─────┘

                                                   ┌─────────────┴─────────────┐
                                                   │                           │
                                           APPROVE │                           │ REJECT
                                                   ▼                           ▼
                                             ┌──────────┐                ┌──────────┐
                                             │ approved │                │ rejected │
                                             └────┬─────┘                └──────────┘

                                             SEND │

                                             ┌─────────┐   SEND_SUCCESS  ┌──────┐
                                             │ sending │ ───────────────▶│ sent │
                                             └─────────┘                 └──────┘

Lifecycle States

StateRole TagDescription
drafteditorBroadcast under composition or initial ingestion.
submittedadminPost submitted by a community user; triggers AI moderation evaluation.
moderatedadminAI evaluation complete (moderation metadata attached); placed in Moderation Queue.
approvedadminApproved by AI or moderator; triggers translation into active languages.
rejectedadminSubmission rejected by moderator or AI; stores rejectionReason.
sendingadminBackground send actor executing Mailgun list dispatch.
sentadminDispatched to subscribers, archived, and tracked for webhooks.

3. AI Moderation Engine (effect/unstable/ai)

When a submission enters the submitted state, the server invokes the moderateBroadcast actor powered by effect/unstable/ai using gpt-4o-mini.

AI Moderation Schema (ModerationResult)

typescript
const ModerationResult = Schema.Struct({
  category: Schema.String,          // 'news' | 'event' | 'announcement' | 'question' | 'discussion' | 'spam'
  summary: Schema.String,           // Concise 1-2 sentence overview
  spamScore: Schema.Number,         // Floating point score from 0.0 to 1.0
  recommendedAction: Schema.String, // 'approve' | 'flag' | 'reject'
})

Auto-Moderation Decision Matrix

After LanguageModel.generateObject completes:

  • recommendedAction === 'approve' (Spam Score < 0.3): The lifecycle automatically transitions to approved (moderatedBy: '__ai__') and advances to dispatch without requiring manual human review.
  • recommendedAction === 'reject' (Spam Score > 0.8): The lifecycle automatically transitions to rejected with an automated rejection reason.
  • recommendedAction === 'flag': The broadcast pauses in the moderated state in the Admin Moderation Queue for human inspection.

4. Rate Limiting Safeguards

To prevent spam attacks or accidental flooding, user-origin submissions (origin: 'user') are rate-limited per sender email:

  • Enforcement: Max 5 submissions per sender per hour.
  • Verification: Checked inside the submit action handler prior to writing the submitted state. Exceeding the threshold returns a typed rate-limit error.