Inspiration

Every subreddit moderator knows the frustration: a user posts something that clearly breaks Rule 4, you remove it with a canned AutoMod reply, and within minutes you have a modmail thread that starts with "why was my post removed??" The user never read the sidebar. The bot's plain-text comment blends into the noise. Nothing was learned, and the user breaks the same rule again next week.

The core insight that inspired this project: a removal is not a punishment, it is an interruption. It creates a moment of attention — the user is right there, wondering what happened. That moment is wasted on a wall of legalese they immediately close. What if that moment was a lesson?

What it does

Educational Enforcer is a Devvit Blocks app that replaces the generic removal comment with an interactive Rule Card — a custom post type rendered natively inside the Reddit UI. The card shows the specific rule that was violated, explains why it exists, and requires the user to read it for at least 10 seconds before the "I've read this rule" button enables.

If the user ignores the card, up to three escalating reminder comments are posted. After the third, the case is automatically sent to the mod queue for human review — no more chasing users manually.

Repeat offenders are tracked. The same user breaking the same rule a second time gets a different, softer card. The third time, reminders are skipped and the case goes straight to mods. The system moves from education toward accountability.

Moderators get a live Analytics Dashboard — a second custom post type pinned to the subreddit — showing removals, acknowledgement rate, and escalations across All Time, 30 days, and 7 days, with per-rule breakdowns and unicode sparkline trends. It auto-refreshes every 60 seconds.

How we built it

The data model is layered into three repositories:

cardRepo — one record per removal event, keyed by content ID, with hash-based indexes for per-user and per-post lookups. userRuleRepo — tracks offense count per (userId, ruleId) pair within a 30-day rolling window, driving the repeat-offender logic. statsRepo — lightweight daily-bucketed counters.

Challenges we ran into

  1. Devvit Redis is not full Redis. The biggest surprise was discovering at runtime — not at compile time — that sAdd and sMembers (Redis Sets) simply do not exist in the Devvit production runtime. TypeScript happily accepted the calls; the app crashed silently. The fix was to replace every set-based membership pattern with a hash-based one: hSet(key, { [memberId]: '1' }) and Object.keys(await hGetAll(key)) to enumerate members. Every index in the codebase was migrated.

  2. TSX only in .tsx files. Devvit's build pipeline requires JSX syntax to live in .tsx files. One menu handler started as a .ts file with JSX in it — the compiler was silent but the output was wrong. The fix was either renaming to .tsx or using Devvit.createElement(...) directly, which is the underlying call JSX compiles to anyway.

  3. Blocks height constraints. Devvit custom posts have fixed height tiers (regular, tall, fullscreen). Getting the Analytics Dashboard to fit comfortably in regular height — with a global totals row, a scrollable per-rule table, filter buttons, a refresh toggle, and a live timestamp — required careful allocation of every pixel of vertical space, leaning on grow sparingly and keeping font sizes minimal.

  4. Scheduler fan-out at scale. Each removal schedules up to three reminder jobs, each of which re-reads card state, checks if the user already acknowledged, and either posts a comment or escalates. Getting the idempotency right — so a late job never double-posts after an acknowledgement — required checking card state at job execution time, not at schedule time.

Accomplishments that we're proud of

Shipping a complete, end-to-end moderation workflow entirely within Devvit. From the moment a mod clicks "Remove with Education" to the moment a user reads the rule, acknowledges it, and the event appears in the analytics dashboard — every step is handled by the app with no external services, no webhooks, no database outside Devvit Redis. The entire system is self-contained and installs in one command.

Turning a platform limitation into a better architecture. When we discovered that Devvit's production Redis silently drops sMembers and sAdd at runtime — with no compile-time error — we didn't just patch the crash. We redesigned every membership index to use hash-based patterns (hSet + hGetAll), which are actually more efficient for our access patterns than sets would have been. A bug became a better data model.

A live analytics dashboard that fits in a Reddit post. Getting a useful analytics surface — global totals, per-rule breakdowns, 3 time windows, unicode sparkline trends, auto-refresh, and a UTC timestamp — to render cleanly inside Devvit's regular-height post type, with no external charting library, felt like a genuine constraint-driven design win. The sparklines are computed with nothing but string indexing and integer math.

A repeat-offender system with genuinely different behavior at each tier. Most moderation bots treat every violation identically. Ours tracks (userId, ruleId) offense counts within a 30-day rolling window and changes both the card's tone and the escalation path depending on offense number. First offense: patient and educational. Second: softer, empathetic. Third: skips reminders entirely and goes straight to mods. The system models a human moderator's natural escalation instinct.

What we learned

Devvit Blocks is remarkably capable for read-heavy UIs once you embrace its constraints. The useAsync + useInterval pattern for live-updating dashboards works cleanly inside the platform's server-render model. Platform-specific runtime gaps matter more than type safety gaps. The Redis sMembers bug taught me to test against the actual target runtime early, not just against type signatures. The education framing genuinely changes user psychology. A card that explains a rule — and makes you wait 10 seconds — feels different from a bot comment. The friction is the feature.

What's next for Educational Enforcer

Near-term (polish & reliability) Quiz mode — instead of a passive read gate, present a 1-question multiple-choice quiz drawn from the rule's educational content. Acknowledgement only counts on a correct answer. This is the single biggest lift to actual learning. Rich rule authoring — a mod-facing settings UI with a markdown editor per rule, so educational content isn't just the raw rule text but a proper explanation with examples of what's allowed vs. not. AutoMod integration — parse incoming removal_reason fields from AutoMod so the bot works with zero manual mod intervention, not just via the "Remove with Education" menu item. Flair reward — grant a configurable flair (e.g. "Reformed Redditor") after a user acknowledges n n rules across their history, giving positive reinforcement for the pattern. Medium-term (analytics & moderation workflow) Recidivism rate trend — track the rate at which users who acknowledged a rule break it again within 30 days. This is the truest signal of whether the education is working. Modmail integration — when a case escalates, automatically open a modmail thread with the rule card's acknowledgement status attached, so mods have full context before responding. Bulk rule import — one-click sync from the subreddit's Reddit-native rule list into the app's rule config, eliminating the need to maintain two lists. Per-rule educational templates — a library of community-contributed rule explanation templates that mods can adopt and customise, lowering the barrier to good educational content. Longer-term (platform expansion) Cross-subreddit rule sharing — allow a mod network to share a common rule library and pool anonymised analytics, useful for topic-adjacent communities (e.g. all gaming subs). Devvit Web migration — move the Rule Card and Dashboard to a webview-based UI for richer formatting, embedded images, and video explanations inside the card. Appeal flow — after a third escalation, surface a structured appeal form directly in the Rule Card UI that routes to modmail with the full violation history pre-filled, replacing the chaotic "why was I banned" modmail thread entirely.

Built With

  • custom-post-types)-storage-devvit-redis-?-hash-counters-(hset-/-hincrby-/-hgetall)
  • daily-bucketed-stat-keys-for-analytics-aggregation-automation-devvit-triggers-?-postremove
  • platform-devvit-0.12.22-?-reddit's-developer-platform-for-native-app-experiences-language-typescript-5.4-(strict-mode)-ui-devvit-blocks-?-server-rendered-component-tree-with-tsx-syntax-(devvit.createelement
  • sorted-sets-(zadd-/-zrange)-for-time-windowed-queries
Share this project:

Updates