Today, appealing a ban or removal on Reddit means writing into modmail and hoping. Mods get an unstructured wall of text with no link to the action, no history, and no audit trail. Inconsistent decisions, mod burnout, and users who feel unheard — even when their appeal is fair.

Appeal-Desk is a dedicated, fair appeals desk built into the subreddit.

What it does, end to end

  1. Action snapshot. When a mod bans a user or removes content, Appeal-Desk atomically snapshots the action context (action type, target ID, original removal reason) keyed by the target. The user can never edit this; the mod always sees the original reason at decision time.
  2. Civil invite. The banned user is invited (via modmail, on bans) to file an appeal through a structured intake form. The form pre-fills the action context (read-only) and asks for a reason and explicit acknowledgement.
  3. Submission — hardened. Validation, sanitisation, rate-limiting, and atomic action-lock (WATCH/MULTI/EXEC CAS on Redis) all run at the boundary. Two users cannot open duplicate appeals for the same action even if they submit in the same millisecond. Deterministic Jaccard-based dedup against the user's prior appeals fires a near-duplicate flag for the mod — not a block.
  4. Mod dashboard. A custom post renders a queue of open appeals with full context, history count, and near-duplicate score. Each appeal opens a detail panel with three one-tap buttons: Uphold, Overturn, Ask for more info. AI hints (if enabled) appear clearly marked, never load-bearing.
  5. Reply-confirm gate. Every one-tap button opens a reply-confirm form with a templated civil reply pre-filled. Nothing is sent until the mod confirms. AI never blocks, AI never decides, AI never sends.
  6. Record-then-send. The decision is persisted before the reply is dispatched. If modmail send fails transiently, the decision still stands and the UI offers a resend — the recorded verdict is the source of truth.
  7. Lifecycle. SLA-nudge scheduler. Daily retention purge of resolved appeals past the configured window. GDPR-style erasure scrubs free text but keeps an auditable tombstone (idempotent).

The AI stance

AI assists. Mods decide. The audit trail proves it.

Three rules, enforced structurally:

  • AI never blocks intake. submitAppeal() calls the optional triage provider best-effort; failure returns null and the appeal is created either way.
  • AI output is clamped. Empty / too-long replies fall back to the deterministic template. Malformed triage JSON yields null, never throws.
  • NoopAiProvider is the default. selectProvider(aiEnabled, backend) returns a no-op whenever AI is off or no backend is wired. The full happy path works without an API key — verified by tests.

Why this isn't slideware — reproducible numbers

  • 288 / 288 tests pass across 18 test files
  • 99.97 % statements / 99.06 % branches / 100 % functions / 99.97 % lines coverage on the platform-free core (gates pass; the two non-100 % branches are documented defensive guards)
  • 0 TypeScript errors (npx tsc --noEmit clean across .ts + .tsx)
  • 0 ESLint issues
  • 4 BUILD commits on main showing the project's evolution from initial scaffold through the code-review fixes to the final renamed app
  • An honest defect log: every reviewer-found bug (broken lint, same-ms cursor tie-skip, unbounded queue read, smuggled lock key, dead retention code, stray metric field) was found and fixed, with new tests landing alongside each fix.

Architecture in one sentence

A platform-free TypeScript core (core/ + ai/, zero Devvit imports, 100 % testable without the runtime) behind a thin Devvit shell that wires triggers, custom posts, forms, menu items, and the scheduler.

That is the single architectural decision the project most rewards: the core speaks plain domain objects and small injected interfaces (RedisLike, RedditGateway, AiProvider, Telemetry), which is why both 100 % coverage without the Devvit runtime, and gradual deprecation of the AI layer if the team ever wants to, are tractable.

Try it now

  • Install from the App Directory: https://developers.reddit.com/apps/appeal-desk
  • Reproduce the numbers: git clone https://github.com/vsenthil7/appeal-desk && cd appeal-desk && npm ci && npx tsc --noEmit && npm run lint && npx vitest run --coverage

See submission_media/JUDGE_TEST_SCRIPT.md for a step-by-step deterministic verification walkthrough (~10 minutes, every step has a concrete pass criterion).

License

MIT.

Built With

  • concurrency
  • devvit
  • moderation
  • modmail
  • property-based-testing
  • reddit-developer-platform
  • redis
  • retention
  • typescript
Share this project:

Updates