Inspiration

A thread on r/Devvit got me thinking about how the install-payment model funds moderator tools — and how almost every mod tool I'd seen ran silently in the background. They removed spam, locked threads, mopped comments. Useful, but invisible to the community.

The obvious gap: what if a mod tool also generated engagement? Moderators install it for the cleanup utility, and the broader community ends up using it.

The format itself came from a hard constraint: Reddit comments don't support inline images, custom emoji, or arbitrary HTML. But they do render Unicode flawlessly across every client. So instead of fighting the platform, I leaned all the way in — 540 hand-curated Unicode/ASCII faces ((╯°□°)╯︵ ┻━┻, ( ͡° ͜ʖ ͡°), ʕ•ᴥ•ʔ), plus a sticker layer that posts as real Reddit-hosted inline images using Devvit's media + rich-text APIs.

What it does

Reddmoji is a Reddit-native reaction layer that works on three surfaces in any subreddit where it's installed:

  1. Reddmoji posts — a moderator creates one from the subreddit menu. Anyone who opens it gets an inline picker with three modes:
    • React — one tap registers an in-app reaction. Counts go up live, your reacted faces stay highlighted across visits, tap again to un-react. No comment posted — perfect for lurkers.
    • 💬 Comment — opens a sheet with the chosen face, optional caption, and a before / after / wrap position selector with a live preview. Submits as a real Reddit comment under the user's account.
    • 📋 Copy — copies the face text to clipboard for use anywhere.

Plus a 🖼️ Stickers tab for image stickers that post as actual inline images in the comment thread.

  1. "React with Reddmoji" — a menu action on every post in the subreddit. Pick a face or sticker, optionally add a caption, choose position, submit.

  2. "Reply with Reddmoji" — same menu action on every comment. Threads correctly under the parent comment.

Other features:

  • Live search across 540 faces by name, tag, or category (happy, rage, lenny, table flip, bear, goat…)
  • 20 categories including emoji entries for food, weather, gaming, music, sports, nature
  • Per-subreddit trending strip with a rolling 7-day window
  • Per-user reaction state persisted across visits
  • Bundles the Mop moderator tools (bulk-remove or lock comments by thread or post)

How we built it

  • Devvit Web for the full stack — Hono server with route handlers for menu actions, form submissions, lifecycle triggers, and the public /api/* surface that the webview calls.
  • Vanilla TypeScript + a single CSS file for the picker — no React, no framework. Under 30KB of bundled JS, loads instantly even on slow mobile.
  • Redis (@devvit/web/server) for all state:
    • Per-post reaction counts in a hash (reactions:post:<id>)
    • Per-(post, user) reacted faces in a hash so a single hGetAll returns the user's full set on page load — no key scans
    • Rolling-window trending in per-day zSet buckets with an 8-day TTL (reactions:trending:<sr>:day:<YYYYMMDD>) — old reactions naturally drop off the 7-day window with zero scheduler overhead
    • Sticker uploads cached as media:sticker:<id> → Reddit mediaUrl
    • Rate-limit counters using fixed-window incrBy + expire (6 comments/min/user)
  • runAs: 'USER' for both submitComment and submitCustomPost — comments and posts appear under the actual user's account, count for their karma, and are deletable by them.
  • Image stickers via reddit.media.upload({ url, type }) + RichTextBuilder().image({ mediaUrl }) — the same path Reddit's native "upload image to comment" button uses, so stickers render as real inline images.
  • Typed face dataset with weighted search — exact tag match > name match > category match.

Challenges we ran into

  • runAs: 'USER' discoverability. The runtime requires explicit permissions.reddit.asUser: ["SUBMIT_COMMENT", "SUBMIT_POST"], but the schema docs label these as "not currently in use." The runtime error message taught us the right config — the docs didn't.
  • Trending without a scheduler. Sliding-window trending typically needs a cron job to age out old data. We sidestepped that with daily zSet buckets + redis expire. No cron, no race conditions, free maintenance.
  • navigator.clipboard in sandboxed webviews isn't always available — required a document.execCommand('copy') fallback so Copy mode works regardless of how the iframe was loaded.
  • comment.permalink is relative while navigateTo requires an absolute URL — small thing but turned a "post comment" success into a hard failure until we wrapped it with new URL(path, 'https://www.reddit.com/').toString().
  • TypeScript module resolution for the client@devvit/web/client exports use the browser condition. Required splitting into per-environment tsconfigs (tsconfig.client.json with customConditions: ["browser"]) and a solution-style root tsconfig.
  • Webview CSP only allows images from *.redd.it / *.redditmedia.com / data: / blob: / self. Sticker source URLs (GitHub raw, R2, etc.) are blocked from rendering directly in the picker. Solved by uploading every sticker to Reddit on first list and serving the resulting mediaUrl for both the preview and the comment.
  • Devvit form constraints — no tabs, no conditional fields, no image grids. Forced a clean separation: webview gets the visual picker; the form picker on regular posts/comments stays focused on what selects do well (searchable, named lists).

Accomplishments that we're proud of

  • A dual-purpose mod tool — utility for moderators and a content engine for users, in one install. Maps directly to the install-payment model the platform is built around.
  • 540 categorized, tagged faces across 20 categories, with rolling-window trending so the experience stays fresh in active subreddits.
  • Three input pathways (custom post, post menu, comment menu) so Reddmoji is usable whether or not the subreddit has dedicated reaction posts.
  • Two output formats in one tool — Unicode text comments (zero hosting, instant) and image stickers (real inline images via Reddit's native media path).
  • Server-state-aware UI — the picker shows your already-reacted faces, the trending strip, and live count updates without page refreshes.
  • Zero hosted assets for the core experience. No CDN, no fonts. Stickers piggyback on Reddit's own media hosting after a one-time upload.

What we learned

  • The cleanest engagement loops are the ones that keep the comment thread itself useful. Reactions stay in-app; only the user-initiated Comment and Sticker paths post replies — so threads don't drown.
  • Redis primitives + careful key design solve a surprising amount of "do I need a real backend?" — daily buckets + TTL replaced what would otherwise be a scheduler.
  • runAs: 'USER' is the unlock for any Devvit app that wants to feel community-native instead of bot-driven. It also opens deletion semantics — user-owned posts behave like normal Reddit posts.
  • Devvit's surface constraints are a forcing function for good UX. We can't inject HTML into comments, can't put a grid in a form, can't render arbitrary CSP-violating images in the webview. Each constraint pushed us toward a cleaner answer than we'd have built unconstrained.

What's next for Reddmoji

  • Realtime — broadcast reactions via @devvit/realtime so other viewers see counts move live, not on refresh. This is the biggest remaining UX gap.
  • Favorites and recents per user — pin the faces you keep coming back to.
  • Subreddit theming — let mods pick a default face set, featured category, or sticker pack for their community.
  • Engagement widgets — top reactor / debate battles / reaction streaks inside Reddmoji posts to push the gamification angle.
  • Sticker library expansion — community-contributed sticker packs, with moderator approval per subreddit.
  • Per-subreddit insights for moderators — top reactions, top reactors, hour-of-day patterns. Turns the engagement signal back into something useful for the people running the community.

Built With

Share this project:

Updates