Skip to content

12 — Moderation Settings (Screen 9, `/app/testimonials/moderation`)

Cursor-ready plan: shop-level auto-moderation rules, blocklist, video length limits, team notifications, and where to enforce them in the pipeline.

Source: 02-Implementation-Blueprint.mdScreen 9 - Moderation Settings (/app/testimonials/moderation).

Product alignment: 01-Post-Purchase-Video-Testimonial-Collector-Plan.md§6 (moderation), §7 (Phase 2+ AI signals — only placeholder UI here for blur detection).

This document is a build spec only. No code changes are implied until a task references this file.

Related: 06-public-submission-page-screen-13.md (apply rules on submit), 09-submissions-inbox-screen-5.md and 10-submission-detail-screen-6.md (human review), 08-security-compliance-and-privacy.md (PII in blocklist / emails).


Merchants configure automation and guardrails (auto-approve photos, minimum rating, blocklist, video duration bounds, team notification recipients, and a disabled future toggle for AI blur) in one admin screen; the backend enforces these rules at submission time and/or when new pending items arrive.


ItemValue
Suggested fileapp/routes/app.testimonial-moderation.jsx (per blueprint §6).
URL/app/testimonials/moderation.

Auth: authenticate.admin(request); read/write only the current shop’s settings.

Nav: add under testimonial group, e.g. “Moderation” or “Moderation rules”.


Option A (simplest): add nullable columns to Shop (or a dedicated TestimonialModerationSettings model with shopId @unique if you want a slimmer shops table).

Blueprint fieldSuggested column / typeNotes
Auto-approve photos onlytestimonialAutoApprovePhotos Boolean @default(false)Already named in blueprint §5.1reuse or align.
Require minimum ratingtestimonialMinRating Int?null = no minimum; 1–5 when set.
Blocklist wordstestimonialBlocklistWords String? @db.TextStore as newline-separated or JSON array; normalize to lowercase for matching.
Minimum video length (seconds)testimonialMinVideoLengthSec Int?null = no minimum.
Maximum video length (seconds)testimonialMaxVideoLengthSec Int?null = use hard platform cap elsewhere.
Reject blurry (future AI)testimonialBlurRejectionEnabled Boolean @default(false)UI: disabled with “Coming soon” — no worker in v1.
Team notification emailstestimonialTeamNotifyEmails String? @db.TextComma- or newline-separated list; validate on save.

Migration: one Prisma migration; backfill is not required (nulls = “not configured”).


  • Page title="Moderation settings"
  • Layout: FormLayout or BlockStack with Cards:
    1. Automation — auto-approve photos, min rating
    2. Content safety — blocklist
    3. Video rules — min/max length; blur toggle (disabled)
    4. Notifications — team emails
  • Auto-approve photos onlyCheckbox with help text: “Photo submissions skip the pending queue and are marked approved. Video always requires review unless you add more rules later.”
  • Require minimum ratingSelect 1–5 or “No minimum” (null).
  • BlocklistTextField multiline={5}; help: “One word or phrase per line. Submissions containing these (case-insensitive) are rejected or held—define behavior in §4.”
  • Min / max video lengthTextField type="number" with min={0}; show error if min > max when both set.
  • Blur rejectionCheckbox disabled + Tooltip or Banner “Planned: AI quality checks.”
  • Team notification emailsTextField multiline; validate each token as email; max N addresses (e.g. 10) to avoid abuse.
  • Primary action “Save” → action with intent save_moderation_settings.
  • Success / error toasts; use useSubmit + useActionData or Remix patterns.

Document this in code comments so 06 and any async workers stay consistent.

4.1 On public submit (api.testimonial-submit06)

Section titled “4.1 On public submit (api.testimonial-submit — 06)”

After creating TestimonialSubmission in pending (or before final commit), run validation:

  1. Blocklist — if headline or reviewText contains any blocklist entry (word boundary or substring — choose one; substring is simpler but more false positives):

    • Option A: set status=rejected with system reason + TestimonialModerationLog actorType=system.
    • Option B: keep pending but set flagged / hold for merchant — pick A or B and document (A reduces inbox noise; B is safer for false positives).
  2. Minimum rating — if testimonialMinRating is set and rating < min (or rating null when required): 400 with clear message or create rejected row per product policy.

  3. Auto-approve photos — if mediaType === 'photo' and testimonialAutoApprovePhotos === true:

    • Set status=approved and optionally published=false (publish still from 10 / 11 if you separate concerns) or approved + published=true for fastest go-live — pick one; recommended: approved only, publish via 11.
  4. Video length — compare TestimonialMediaAsset.durationSec (must be populated after upload metadata extraction — if still null at submit, queue validation in media worker or reject with “processing” state). If duration out of bounds: reject or hold per same policy as blocklist.

  • No-op. If toggle is disabled in DB, skip. When implemented later, hook in media callback worker.

v1 minimum: on new TestimonialSubmission with status=pending (and not auto-rejected), send one email to testimonialTeamNotifyEmails using app/lib/email.server.js (Brevo), subject line includes shop name + submission id.

Idempotency: do not email on duplicate submit (409).

Optional: digest mode — out of scope for Screen 9 v1.


  • Auto-rejected rows may not appear in Pending tab if you set status=rejected immediately — they should appear under Rejected with reason “Blocklist” or “Below minimum rating.”
  • Auto-approved photos appear under Approved (and optionally Published after merchant action).

  • Blocklist should not log matched substrings if they contain PII — log submissionId + rule id only.
  • Team emails are merchant-only; never expose on public API.

7) Testing checklist (acceptance criteria)

Section titled “7) Testing checklist (acceptance criteria)”
  • Save invalid email list → validation error, no partial save.
  • min > max video bounds → validation error.
  • Submit with blocklisted word → expected reject or hold per §4.1.
  • Photo + auto-approve on → submission is approved (and log says system if you log auto-approve).
  • Video length out of range → rejected or held.
  • Team email sends (or is skipped if list empty) when a new pending submission is created.
  • Blur toggle remains disabled in UI; no server path depends on it.

  1. Prisma: add/merge columns on Shop (or new 1:1 settings model).
  2. app.testimonial-moderation.jsx loader + action + Polaris form.
  3. Extract applyModerationRulesToSubmission({ shop, submission, text, media }) in app/lib/testimonial-moderation.server.js (name flexible).
  4. Call from api.testimonial-submit after create (or inside transaction).
  5. Add team notify email in same request or deferred job (queue table optional for v1).
  6. Update 09 empty copy if all auto-handled.

  • 02-Implementation-Blueprint.mdScreen 9, §5.1 (testimonialAutoApprovePhotos, testimonialConsentVersion lives on Shop; moderation fields can live alongside).
  • 01-Post-Purchase-Video-Testimonial-Collector-Plan.md§6–7 moderation and future AI.
  • 06-public-submission-page-screen-13.md — submit API.
  • 08-security-compliance-and-privacy.md — logging and email handling.

This folder already includes 0511 numbered plans. This file is 12-… (not 05-…).