Inspiration
Academic research from CHI 2026 confirms what moderators have been saying for years: 74.5% of mods experience queue collisions, where multiple moderators unknowingly process the same content. Moderators juggle fragmented tools — mod log for history, third-party dashboards for stats, and manual spreadsheets for trend tracking — but none of these provide an automated, unified view of subreddit health.
Scouring r/modhelp and r/ModSupport reveals a recurring theme: moderators want to know "how is our subreddit doing this week?" without manually stitching together data from five different sources. Yet Reddit's native mod log is capped at 90 days, and no existing Devvit app delivers a scheduled, enriched, multi-dimensional health digest.
Existing Devvit apps each cover one dimension:
- modlogstats shows raw mod log counts but no trends, no schedules, no enrichment
- modqueue-tools helps clear queues faster but doesn't measure team throughput
- sub-stats-bot provides on-demand stats but has no scheduler, no anomaly detection, no per-mod breakdown
No one has combined all these signals into a single automated report. ModVitals was born from that gap — a complete, zero-config health monitor that tells your mod team exactly what's happening, every reporting period, without anyone lifting a finger.
What it does
ModVitals is a Devvit app that generates automated periodic health reports for subreddit moderation teams. It runs entirely on Reddit's platform — no external servers, no dashboards, no browser extensions required.
Core capabilities:
6 reporting presets — Hourly, 4-hourly, 12-hourly, daily, weekly (Monday), or custom cron expression. Each preset resolves to its effective cron at runtime, with timezone offset applied from the settings panel (34 timezone options, UTC-12 through UTC+14).
Heartbeat scheduler — Devvit's static cron (
* * * * *) fires every minute, but ModVitals checks runtime settings on each tick.shouldGenerateReport()gates execution based on configured frequency, timezone-adjusted wall clock, and dedup guards (daily/weekly skip if already generated today; sub-hourly skip if last report was under 60s ago). This heartbeat pattern unlocks flexible scheduling despite platform constraints.3 event triggers —
onPostSubmit,onCommentCreate, andonModActionfire on every subreddit activity, writing incremental metrics to Redis immediately. Per-mod action counts, per-rule violation tallies, and repeat-offender scores are all tracked in real time.Redis-powered data layer — Daily hash keys store numeric counters (
metrics:YYYYMMDD,mods:YYYYMMDD,modActions:YYYYMMDD,rules:YYYYMMDD). A sorted set (offenders) tracks repeat offenders across all time, persisting beyond Reddit's 90-day mod log window. Snapshot hashes isolate on-demand reports from production schedules.8+ section enriched report — Each report post contains:
- Overview — Top-level totals with trend arrows (▲/▼/➡) and percentage change vs. previous period, gated by per-metric visibility toggles
- Activity Summary — Aggregated submission counts, removal rate, and approval rate
- Rule Violations — Most-frequently broken rules ranked by count
- Repeat Offenders — Users with multiple removals, optionally enriched with link karma, comment karma, account age, snoovatar avatar, and subreddit-specific karma with period-over-period deltas
- Mod Leaderboard — Top 5 mods ranked by action count with workload percentage; inactive mod alerts with days-since-last-action
- Anomaly Alerts — Metrics exceeding 2× the 7-day rolling average flagged at the top of the report (e.g., "⚠️ Unusual activity: 300% more removals than average")
- Debug Info — When enabled, shows all 18 settings values and the resolved effective cron expression
18 configurable settings — Organized by category: scheduling (frequency, hour, minute, timezone, custom cron), visibility toggles (posts, comments, removals, approvals, rule violations, repeat offenders, mod activity, leaderboard, inactive alerts), enrichment (karma stats), and behavior (inactive threshold days, anomaly alerts, debug mode).
Snapshot on-demand — "Generate Report Now" in the subreddit overflow menu produces an immediate
[SNAPSHOT]report without affecting the production schedule. Snapshots write to dedicated Redis keys (snapshots:YYYYMMDD), isolating them from the regular data stream.Mod-only visibility — Every report is posted as a distinguished, approved self-post. Regular subreddit members never see it. The report is a private team dashboard disguised as a Reddit post.
Zero-config startup — Install and go. Smart defaults (daily at noon UTC, all sections enabled) mean the first report appears at the next cron tick with no configuration required.
How we built it
Architecture: triggers → Redis → heartbeat scheduler → enrichment → formatting → posting
onPostSubmit / onCommentCreate / onModAction
│
▼
Redis (real-time incremental writes)
│
▼
Heartbeat cron (* * * * *) → shouldGenerateReport() gate
│
├──→ Aggregate current period metrics
├──→ Compute trends (vs. previous period)
├──→ Enrich offenders (karma API calls)
├──→ Detect anomalies (7-day rolling avg)
├──→ Format Markdown (settings-aware sections)
└──→ Post as distinguished + approved
Tech stack:
| Layer | Technology |
|---|---|
| Platform | Devvit 0.12.24 (Reddit's developer platform) |
| Language | TypeScript 6.0 (strict mode) |
| Server framework | Hono 4.12, bridged via @hono/node-server to Devvit's createServer |
| Data store | Redis (Devvit-managed, per-installation) |
| Bundler | Vite 8.0 |
| Runtime | Node.js ≥ 22.2.0 |
Codebase structure (19 TypeScript source files):
src/
├── client/
│ └── index.html # Devvit app shell
└── server/
├── index.ts # Entry point: registers routes + triggers
├── server.ts # createServer bridging (Hono → Devvit Web)
├── settings.ts # Settings loading, validation, presets
├── scheduler-logic.ts # shouldGenerateReport, aggregation, anomaly detection
├── metrics.ts # Redis read/write layer for all metric keys
├── karma.ts # Reddit API enrichment (karma, age, snoovatar)
├── date-utils.ts # Date math, timezone offset, period boundaries
├── cron-matcher.ts # Cron expression parser and matcher
├── report.ts # Pure Markdown formatting (all section formatters)
├── posting.ts # Reddit API: submit + distinguish + approve
└── routes/
├── triggers.ts # onPostSubmit, onCommentCreate, onModAction handlers
├── scheduler.ts # Heartbeat cron handler
└── snapshot.ts # Overfow menu "Generate Report Now" handler
Testing: 426 tests across 6 test files
src/server/report.test.ts # Formatting output verification
src/server/scheduler-logic.test.ts # Aggregation, trends, anomaly detection
src/server/settings.test.ts # Settings resolution + validation
src/server/date-utils.test.ts # Date math + timezone offset
src/server/karma.test.ts # Karma enrichment formatting
src/server/cron-matcher.test.ts # Cron expression matching
Design principles (Pragmatic Programmer):
- DRY — 12 duplicate patterns eliminated during v0.1.0 refactoring.
formatBulletList,formatWithTrend, andtrendArroware single-source helpers reused across all 8 report sections. - Orthogonality — Each module has a single, well-defined responsibility.
metrics.tsreads/writes Redis.karma.tsenriches users.report.tsformats output.posting.tshandles API submission. No module reaches into another's internals. - FCIS (Functional Core, Imperative Shell) — Pure functions (
report.ts,cron-matcher.ts,date-utils.ts,settings.ts) form the functional core with zero side effects and are fully unit-testable. The imperative shell (routes/,posting.ts,metrics.ts) handles I/O: Redis reads/writes, Reddit API calls, and HTTP endpoints. - Crash Early — Every
catchblock logs toconsole.errorwith contextual detail. Redis read errors propagate immediately rather than silently returning{}(which was indistinguishable from genuinely empty data in early builds).
Challenges we ran into
1. Devvit server context bridging
Devvit's createServer from @devvit/web/server expects Express-style (req, res) callbacks, but Hono uses the Web Fetch API (Request / Response). The two paradigms don't natively interoperate. We solved this with @hono/node-server's getRequestListener, which creates a Node.js HTTP listener from a Hono app. This listener bridges Hono's fetch-based routing into Devvit's Express-compatible server context. The result is clean, modern Hono routing inside a platform that predates the Fetch API standard.
2. Settings validation response format
Devvit's settings validation endpoints require a specific response shape: { success: boolean }. The documentation was unclear — early builds returned full validation error objects, and Devvit swallowed them silently, displaying a generic "Oops" toast with no diagnostic information. After systematic trial-and-error testing, we identified the required contract: validation endpoints must return exactly { success: true } or { success: false }. Any other shape is silently discarded.
3. Interactive OAuth
devvit upload and devvit install require browser-based OAuth login. In a headless CI environment, this blocks automation. We used echo piping to feed credentials through the interactive CLI prompts, but the final OAuth redirect still requires a manual browser verification step. Our solution: automate everything up to the redirect, then open the verification URL in the default browser for a single click. Not fully headless, but close.
4. Silent error swallowing
Redis read errors in Devvit's managed Redis returned {} (empty object) — identical to the return value for a key that genuinely has no data. This made debugging impossibly opaque: was a report empty because there were no events, or because Redis threw an error? Applying the Pragmatic Programmer principle of "Crash Early," we added console.error in every catch block with the full error context, and added explicit null vs. {} guard checks in the metrics layer. Now errors are immediately visible in devvit logs.
5. Overview settings bug
The formatOverview() function originally hardcoded all four metrics (posts, comments, removals, approvals) regardless of the moderator's visibility toggle settings. If a mod disabled "Show Post Count" in settings, the Overview still displayed it. The fix was straightforward but easy to miss: gate each metric line behind its corresponding showPosts / showComments / showRemovals / showApprovals boolean. This is now enforced by the test suite, which verifies that disabled toggles produce no output in the formatted report.
6. Cron schedule rigidity
Devvit's scheduler configuration in devvit.json is static — you define a cron expression at build time, and it never changes. But ModVitals needed runtime-selectable reporting frequencies (hourly through custom cron). Our solution: set the static cron to * * * * * (every minute) and implement a heartbeat pattern. On each tick, shouldGenerateReport() loads the current settings from Redis, computes the effective cron from the preset (e.g., daily + reportHour=12 + timezoneOffset=-300 → 0 12 * * *), applies timezone-adjusted wall-clock time, and only proceeds if the current minute matches. Dedup guards prevent double-fires. The result: 6 independent reporting presets with full timezone support on a platform that only supports static cron.
Accomplishments that we're proud of
426 passing tests with pure-function architecture. Every formatting function, date calculation, trend computation, cron matcher, and settings resolver is tested in isolation. The test suite catches regressions immediately and serves as living documentation of expected behavior.
6 independent reporting presets with timezone support in a platform whose scheduler only supports a single static cron expression. The heartbeat pattern with runtime settings gating is a novel workaround that unlocks truly flexible scheduling without any platform changes.
PP-compliant codebase. After an initial "God module" prototype, we systematically split the codebase:
report.tswas extracted fromscheduler-logic.ts,formatBulletListwas deduplicated from 12 call sites, and all I/O was isolated to the imperative shell. The result is a codebase where every module has a single clear responsibility.Real-time karma enrichment with per-user API calls that don't crash the report. Each repeat offender triggers a Reddit API call for karma data. If any single call fails (rate limit, deleted user, private profile), the report continues — the offender still appears but without enrichment. Individual failures are logged but never fatal.
Anomaly detection with rolling averages on a serverless platform. Computing 7-day rolling averages requires historical data, but serverless platforms are stateless. We solved this by storing daily metric hashes in Redis with date-keyed names (
metrics:YYYYMMDD), loading the last 7 days on each report generation, and computing the baseline on the fly. The anomaly alert fires when any metric exceeds 2× its rolling average.Debug mode that shows resolved effective cron from preset settings. Instead of forcing mods to understand our internal scheduling logic, the debug section displays exactly what the system resolved: "Your daily report at hour 12 → effective cron:
0 12 * * *". This makes configuration verification trivial.Snapshots that don't pollute production schedules. On-demand reports via the overflow menu write to separate Redis keys (
snapshots:*) and skip thelastReporttimestamp update. Production cron continues unaffected. Mods can generate as many snapshots as they want without disrupting the regular reporting cadence.
What we learned
Devvit Web's context model and server bridging pattern. The
createServerAPI is Express-flavored but lives inside Reddit's platform context. Understanding the request lifecycle — from Reddit event → Devvit platform →createServer→ Hono → route handler — was essential for debugging.@hono/node-server'sgetRequestListenerturned out to be the perfect bridge between Web Fetch API and Node.js HTTP paradigms.Pragmatic Programmer principles in practice. The "DRY" refactoring pass took 6 features to feel the pain, then eliminated 12 duplicate patterns in a single afternoon. The "Orthogonality" principle caught several design issues early — when
report.tstried to call Redis directly, we knew the boundary was wrong before writing a single test. "Crash Early" directly solved our hardest debugging problem (silent error swallowing).Testing pure functions is the highest-leverage activity. Three real bugs were found through tests that would have been extremely difficult to reproduce manually: a timezone offset sign error (UTC-5 was adding 5 hours instead of subtracting), a cron matcher off-by-one for the hour field, and the Overview settings bug where disabled toggles were silently ignored. Each was caught immediately by a failing test before deployment.
Settings UIs need validation endpoint specs documented clearly. The
{ success: boolean }contract for Devvit validation endpoints was undocumented. A single example in the Devvit docs would have saved hours of trial-and-error debugging. If you're building a Devvit app with settings, addconsole.errorlogging inside every validation endpoint from day one.Heartbeat pattern is a powerful workaround for static platform constraints. When the platform gives you a static cron, don't fight it — lean in. A 1-minute heartbeat with runtime gating logic is more flexible than any static cron expression, and it costs essentially nothing in Redis read overhead (a single
hgetallper tick).
What's next for ModVitals
Modmail delivery option. Posting reports as distinguished self-posts works, but some mod teams prefer inbox delivery. Adding a modmail-based delivery channel would make reports accessible even to mods who don't regularly visit the subreddit.
Discord webhook integration. Many mod teams coordinate on Discord. Sending health report summaries to a Discord channel via webhook would bring the data where mods already are.
Public-facing community health reports (opt-in). Some communities want to share moderation transparency with their members. An opt-in public report mode could foster trust between mod teams and their communities.
Predictive insights. With enough historical data, ModVitals could forecast trends: "At current growth, you'll need 2 more mods by August" or "Spam volume typically spikes 40% on weekends — consider staffing up Saturday shifts."
Multi-subreddit aggregated reports. Moderators who oversee multiple communities need a cross-subreddit view. An aggregated report combining metrics from all installed subreddits would give power mods a single pane of glass.
Historical trend visualization. Charts and graphs inside reports — removal rate over time, mod activity heatmaps, rule violation trends — would make patterns immediately visible without reading numbers.
Built With
- devvit
- hono
- node.js
- redis
- typescript
- vite
Log in or sign up for Devpost to join the conversation.