Inspiration

Every moderator knows the feeling. You open the mod queue and see the same post removed for the fifth time today — no flair, no required info, wrong format. You remove it, leave the same comment you've left a hundred times, and wait for the user to resubmit. They usually don't. The post dies, the user leaves frustrated, and you've wasted ten minutes of your day on a problem that should solve itself.

We moderated communities on Reddit and lived this loop daily. AutoModerator can remove posts, but it cannot teach. It cannot tell a user exactly what to fix, and it absolutely cannot restore a post the moment a user corrects it. That gap — between removal and reinstatement — is where good content dies and good users give up.

FlairEnforcer exists to close that gap.


What it does

FlairEnforcer is a formatting enforcement tool that gives users a second chance — automatically.

When a user submits a post that violates the subreddit's formatting rules (missing flair, missing required keywords, body too short, wrong title format), FlairEnforcer:

  1. Temporarily removes the post — not permanently, just holds it
  2. Posts a sticky checklist comment — telling the user exactly what to fix, item by item
  3. Sends the author a modmail — with the same checklist and a direct link to edit their post
  4. Watches for edits — the moment the user updates their post to comply, FlairEnforcer automatically approves it, deletes the checklist comment, and sends a confirmation

Zero mod intervention. The post goes from held to live the moment it meets the rules.

Mods configure everything through a visual dashboard — a pinned custom post in their subreddit. They can:

  • Build rules per flair (or globally) with a drag-and-drop editor
  • Set required keywords, minimum body length, and title format requirements
  • Test regex patterns with a live preview before saving
  • Monitor the held queue with force-restore and permanent-remove controls
  • Track 7-day stats: posts held, auto-restored percentage, average fix time

If a held post isn't fixed within 24 hours, a one-off scheduler job fires exactly at expiry, sends the user a final modmail, and cleans up the queue.


How we built it

FlairEnforcer is built entirely on Reddit's Developer Platform (Devvit) using the Classic SDK.

Backend: TypeScript with @devvit/public-api. The core is two triggers:

  • onPostSubmit — evaluates every new post against the ruleset stored in Redis, removes and holds failing posts, schedules a precise 24-hour expiry job via context.scheduler.runJob({ runAt: new Date(expiresAt) })
  • onPostUpdate — fires on every edit, re-evaluates the current post content, and auto-approves if it now passes

The evaluation engine (evaluate.ts) is a pure function with zero SDK dependencies — fully unit-testable with Vitest.

Storage: Devvit Redis, scoped per installation. We use a Sorted Set (held_index) for the held post queue — scored by expiry timestamp — so concurrent submissions from multiple users never corrupt each other. Stats use Redis Hashes with hIncrBy for atomic increments, and a 30-day TTL prevents zombie key accumulation.

Dashboard: A Vite-bundled React app served as a WebView custom post. The frontend communicates with the backend via window.parent.postMessage, and the backend responds through useWebView's postMessage hook. No external APIs, no external databases — everything stays inside Devvit's sandbox.


Challenges we ran into

The concurrency trap. Our first instinct was to store the held post queue as a JSON array in Redis. On high-traffic subreddits, two simultaneous submissions would both read the array, both append their post ID, and one write would silently overwrite the other — leaving posts trapped in limbo forever. Switching to Redis Sorted Sets with zAdd made concurrent writes atomic and eliminated the problem entirely.

The expiry scheduler design. Our original design used an hourly cron job that looped through all held posts checking for expiries. On a large subreddit with hundreds of held posts, this would hit Devvit's execution time limit and crash mid-loop, leaving posts in a corrupted state. We replaced it with one-off runAt jobs — each held post schedules its own precise expiry job at creation time. No loops, no timeouts, no corruption.

The onPostUpdate race condition. On very fast edits immediately after submission, onPostUpdate would occasionally fire before onPostSubmit had finished writing the HeldPost to Redis. Our first instinct was a retry loop with a 1-second delay. We realized this burns serverless compute time unnecessarily — the correct fix is a graceful exit. The user's next edit fires a fresh onPostUpdate, and by then the data is always present.

The WebView iframe constraints. Reddit's custom post iframe has an opaque container, which means CSS backdrop-filter (glassmorphism) renders as a plain gray box. We caught this during testing and replaced all blur effects with flat dark cards — cleaner, faster, and consistent across Reddit clients.


Accomplishments that we're proud of

The auto-restore loop is the thing we're most proud of. onPostUpdate firing to automatically approve a corrected post — with the sticky comment deleted and a confirmation modmail sent — is something AutoModerator fundamentally cannot do. It requires event-driven triggers that only exist inside Devvit. We built the one mod tool that is architecturally impossible to replicate outside this platform.

We're also proud of the zero-corruption data layer. The combination of atomic Sorted Set operations, one-off scheduler jobs, and stats TTLs means FlairEnforcer will not accumulate data debt or corrupt its own queue under any traffic pattern.


What we learned

We learned to think in Devvit's constraints rather than around them. Every time we reached for an external database or a polling loop, we asked: can Redis Sorted Sets solve this atomically? Almost always, the answer was yes, and the solution was simpler than what we'd planned.

We also learned that the evaluation engine being a pure function — no SDK dependencies, fully unit-testable — is not just a clean code preference. It's what let us iterate on the rule logic quickly without uploading to Devvit after every change. Test fast locally, upload slow to production.


What's next for FlairEnforcer

  • Multi-condition rules — AND/OR logic between multiple rule groups, not just per-flair matching
  • Mod analytics export — downloadable CSV of held/restored stats for subreddit health reports
  • User strike tracking — users who repeatedly submit non-compliant posts get flagged in the dashboard with a warning count
  • Flair-based auto-removal messages — different sticky comment templates per flair, so r/personalfinance gets a different checklist than r/legaladvice
  • Appeal flow — a button in the sticky comment that lets users request manual mod review if they believe the hold was incorrect

Built With

Share this project:

Updates