Inspiration

I was inspired by two existing tools I rely on as a moderator: Modmail-to-Discord, which streams modmail conversations into Discord for real-time team awareness, and Subreddit Statistics, which surfaces community growth metrics. I also drew from Reddit's built-in Insights page, whose Reports & Removals tab is currently capped at a 7-day window, leaving moderators blind to longer trends. The gap was clear: no Devvit app that I've found gives moderators a well-organized view of their own mod actions across time. I wanted to fill that gap with just clean data, surfaced well.

What it does

mod-stats watches your subreddit's moderation log and transforms raw actions into:

  • A monthly insights dashboard: a wiki page with ranked tables for top moderators, action breakdown, daily activity, and most-actioned users — each paired with an inline bar graph. A Community Health Score shows the ratio of constructive to destructive actions at a glance.
  • A 3-month activity heatmap: three monthly calendar grids with 5-level color-coded intensity. Spot brigades, quiet weekends, or unusual spikes instantly.
  • Real-time Discord & Slack webhooks: mod actions stream to your team chat as rich embeds (Discord) or plain text (Slack), color-coded by action severity. Fully configurable per community — enable/disable toggle, action filters, user ignore list, and optional role pings.
  • Weekly modmail digests: every Monday, your mod team gets a formatted report covering top moderators, busiest day, and action breakdown. Triggerable manually from the subreddit menu, too.
  • A self-updating monthly stats wiki page: top moderators, most-actioned users, and daily breakdowns — all timezone-aware, refreshed automatically.
  • 90 days of backfilled history on install: reports are useful from day one, not after a month of waiting.
  • 22 mod action types tracked: posts, comments, bans, mutes, locks, wiki changes, flair edits, team changes. All deduplicated. All timezone-aware.

Pure data-in, markdown-out — every report renders natively in Reddit's wiki, modmail, and webhook endpoints. No external dashboards, no separate accounts.

How we built it

mod-stats is built on Reddit's Developer Platform using TypeScript.

mod-stats is built on Reddit's Developer Platform using TypeScript. Architecture: Every ModAction trigger fires a handler that validates the action, deduplicates by Reddit's action ID, then atomically increments Redis sorted sets keyed by {year}-{month}. This pre-aggregation model means every read — wiki generation, dashboard rendering, digest composition, heatmap data — is O(log N) at read time. No per-request aggregation.

Backfill: On install, a one-shot scheduler job pulls the last 90 days of getModerationLog entries and writes them into the same sorted sets, so dashboards and digests are populated from day one rather than starting empty.

Dashboard & Heatmap: Native Reddit-markdown reports, published to subreddit wiki pages so they render correctly across old.reddit, new.reddit, and the mobile apps with no client-side dependencies. The dashboard pairs a KPI summary with ranked tables (top mods, action breakdown, daily activity, most-actioned users), each row paired with a █/░ bar-graph column. The heatmap renders three stacked monthly grids with weeks as rows (Mon–Sun across the top, ISO week numbers down the side) and five-level emoji color intensity.

Webhooks: Configurable per community via Devvit's native addSettings(). Each subreddit gets its own Installation Settings panel with URL input, enable/disable toggle, action filter, ignore list, and role ping. Supports both Discord (rich embeds) and Slack (plain text). Host validation before every POST.

Cron jobs & menu items: Devvit's scheduler runs wiki updates daily and a weekly modmail digest on Mondays. Subreddit menu items let mods manually trigger any of these on demand — update the stats wiki, regenerate the dashboard, regenerate the heatmap, or send the digest immediately. Dependencies: only date-fns and the Devvit SDK.

Challenges we ran into

  1. HTML doesn't render in Reddit wikis. The first dashboard was a self-contained HTML page with Chart.js loaded from a CDN — every metric would render as a real chart. Reddit's wiki engine displays it as escaped raw HTML. We rewrote the dashboard as native markdown: ranked tables with / bar-graph columns, KPI summary tables, calendar-grid heatmaps with emoji intensity. The new version renders consistently across old.reddit, new.reddit, and the mobile apps with no client-side dependencies.

  2. A silent cross-file write-format mismatch. The live ModAction handler and the historical backfill were both writing to the same Redis sorted set for daily counts — but with different member formats. The handler used "24" (just the day), the backfill used "2026-05-24" (full date). Everything typechecked. The bug only surfaced when the heatmap reader assumed one format and rendered every active day as "0." Fix: extract member-encoding into a shared helper so the format lives in one place.

  3. AppInstall fires before the subreddit context is wired. Calling reddit.getCurrentSubreddit() from inside the install trigger throws a gRPC error that fails the entire install. The fix is to read the subreddit name off event.subreddit.name and schedule heavy work (backfill) as a one-shot job that fires ~30 seconds later, after install completes.

  4. SDK field names changed between Devvit versions. Our backfill assumed entry.action, entry.createdUtc, entry.mod.name, and entry.target.name — none of which exist on the current ModAction type. The actual fields are entry.type, entry.createdAt (already a Date, no epoch math), entry.moderatorName, and entry.target.author. Easy fix once we read the type definitions; expensive without typechecking.

  5. Timezone awareness across all surfaces. Date bucketing, wiki headers, heatmap cells, and digest periods all had to agree on which day a given action belonged to. Intl.DateTimeFormat made this clean, but every new Date() had to route through a centralized timezone resolver to prevent UTC midnight from splitting a single day across two buckets.


Accomplishments we're proud of

  1. One-click install with day-one data. A 90-day backfill on install means new subreddits see meaningful dashboards within a minute, not after a month of accumulated logs.

  2. A simple Community Health Score. A single percentage comparing constructive actions (approvals, unbans, unmutes) to destructive ones (removals, bans, mutes) — at-a-glance context for whether moderation is trending corrective or punitive.

  3. Calendar-grid activity heatmap from sorted sets. Because the data model pre-aggregates by day, generating three months of color-coded cells is three Redis reads. Same query pattern would require per-request aggregation in a hash-bucket model.

  4. Per-community webhook control with no shared infrastructure. Each subreddit configures its own webhook (URL, enable/disable, action filter, ignore list, role pings) through Devvit's native settings panel. No shared configs, no code changes per community.

  5. Proper deduplication out of the box. Every mod action is keyed on Reddit's action ID with a 7-day Redis TTL, so re-installs and overlapping cron ticks can't double-count actions.


What we learned

  • Devvit's settings API is per-installation by default. addSettings() stores values at the community level, which makes per-community configuration trivial. We didn't need a custom storage layer for subreddit-specific webhook configs.
  • Sorted sets are the right data model for analytics. Pre-aggregating into Redis sorted sets (leaderboards, action counts, daily totals) makes every read surface instant, regardless of data volume. Hash-bucket approaches require per-request aggregation that scales poorly.
  • Intl.DateTimeFormat works reliably in Devvit's runtime and is the cleanest way to do timezone-aware date formatting without pulling in heavy libraries like luxon or temporal-polyfill.
  • Reddit wiki pages are markdown-only — no HTML, no JS, no CSS. Our first dashboard was a self-contained Chart.js page; it rendered as escaped raw HTML in the wiki. We rewrote it as native markdown with table-based bar graphs and emoji heatmap grids, which renders consistently across old.reddit, new.reddit, and the mobile apps with zero client dependencies.
  • AppInstall runs before the subreddit context is fully wired. Calling reddit.getCurrentSubreddit() inside the install trigger throws a gRPC error that fails the entire install. The fix is to read the subreddit name off the event payload (event.subreddit.name) and schedule heavy work — like the backfill — as a one-shot job that fires ~30 seconds later, after install completes.
  • Listing.all() already paginates internally. Our initial backfill had a hand-rolled for (page < 10) { ... after = lastId } loop, which both fights the SDK and produces inconsistent results. Devvit's listings are lazy iterators — one .all() call handles the full window cleanly.
  • Cross-file write consistency is invisible to the type system. Two different code paths (live action handler and historical backfill) were writing to the same Redis sorted set with different member formats — one used "24", the other "2026-05-24". Everything typechecked. The bug only surfaced when the heatmap reader assumed one format and got nothing useful back. Lesson: when multiple writers touch the same key, factor the member-encoding into a single helper so the format lives in one place.

What's next for mod-stats

  • Per-mod breakdown: show each moderator's individual action mix (bans vs. removals vs. approvals) so teams can see who handles what.
  • Anomaly detection: since we track daily counts in sorted sets, comparing today against the 7-day average to flag "unusual spike in removals" is straightforward.

Built With

Share this project:

Updates