DEV Community

Lith SEO
Lith SEO

Posted on

Building Games on Reddit — the complete guide for would-be developers

Version 1.0 · 2026-09-12 · Based on building, shipping, and operating six real
Reddit games — three arcade games (Higher or Lower, Reaction Rush, RPS Duel)
and three daily puzzle games (Sudoku, Minesweeper, Solitaire) — live at
r/PlayQuickGames since 2026-09-09,
three weeks after the first line of code. Everything below is what actually
happened, including the bug that made every Sudoku puzzle unwinnable for the
first three days.

Who this is for: you can write some code, you have heard of Git, and you
want to ship something real that strangers use — not another to-do app that
dies on localhost. When you finish, you will have a game live on Reddit with
real players, a daily retention loop, and the professional workflow (branches,
CI, deploys, live-ops) that teams pay for.

Why read this guide and not the official docs first? The docs tell you
what the platform can do. This tells you what to build, in what order, and
why — with the mistakes included. Keep the official
Devvit docs open in another tab; each
section here links to the part of the docs it uses.

How to read it: Part I gets your first app live. Part II is the daily-games
playbook — the pattern Reddit is actively rewarding right now. Part III is the
professional layer: testing, debugging, publishing, and how to grow from
hobbyist to paid developer. A glossary at
the end defines every term of art used here.


Contents


Part I — Foundations

1. Why build games on Reddit first

Every beginner's instinct is to build a standalone app or website. That is the
hardest possible place to start, because code is maybe 20% of a product's
success — the other 80% is the stuff you have no budget for: accounts, hosting,
payments, notifications, moderation, and above all distribution — how a
stranger discovers that you exist.

Reddit's developer platform (Devvit) hands
you all of it for free:

The hard part What Reddit gives you What you'd build alone
Distribution Your game renders inside Reddit feeds — a community's front page is your store shelf App-store SEO, ads, press
Accounts & identity Every Redditor is already logged in; you get a stable user ID per request Auth flows, password resets, GDPR
Hosting & scale Your bundle is served by Reddit's edge; your server runs on their infrastructure Servers, TLS, CDNs, uptime pagers
Database Redis is built in — key-value state with TTLs Provisioning, backups, billing
Notifications Reddit's own machinery: mentions (u/you), replies, mod sticky comments Push infrastructure, email deliverability
Moderation & safety Community mods control installs; Reddit handles abuse at the account level Reporting tools, ban systems
Content cadence A post per day per community — Reddit wants fresh content and promotes it A content engine nobody sees

And the economics are real: Reddit runs Developer Funds — engagement-based
payouts for popular apps — and has run daily-games hackathons with cash prize
categories. The realistic first dollar as a developer is a platform payout for
engagement, not a venture-scale startup.

The deeper argument — deploy where discovery is built in, learn everything
else:
when your game runs on Reddit, you still learn all the fundamentals:
client–server architecture, databases, deterministic logic, UI/UX, CI/CD,
live operations, debugging production. You just don't have to build the
boring infrastructure
before your first player arrives. Once those
fundamentals are second nature, the UI tooling and automation you build on top
(pinned how-tos, midnight result posts, weekly recap bots) are what separate a
toy from a product. Start where the players are; graduate to your own stack
when you have something worth moving.

A concrete data point from this repo: six games, zero marketing — the first
organic players arrived within 24 hours of the games being listed, purely from
posts appearing in one small community's feed. Try getting that with a
portfolio website.

2. The 20% of technology you actually need

Everything in this guide uses the stack below. Notice how short the list is.
Each term is used in context later; skim now, return when you meet it.

Technology What it is Why this project uses it
TypeScript (TS) JavaScript with types — every value has a declared shape the compiler checks Catches a whole class of bugs (typos, missing fields, wrong shapes) before runtime. All six games are TS end to end.
Node.js JavaScript runtime for servers The game server is a Node program (@devvit/web/server)
esbuild A bundler — merges many source files into one optimized file the browser loads Your client is 10 source files; the browser gets 1 fast bundle
HTML/CSS Page structure and styling The entire UI — no framework needed for v1
HTTP + JSON How the browser talks to the server; JSON is the text format for structured data Every game action is an HTTP request with a JSON body
Git + GitHub Version control (every saved state of your code) + remote hosting of that history + pull requests See §10
Redis An in-memory key–value database — you store values under string keys, optionally with automatic expiry All game state: leaderboards, streaks, puzzles, flags
CI (Continuous Integration) Robots that run your checks on every proposed change GitHub Actions runs types/lint/build on every pull request
cron A time-based scheduler ("0 0 * * *" = every midnight UTC) The daily post, yesterday's results, the weekly recap
UTC Coordinated Universal Time — the world's clock without time zones Every "day" boundary must be the same for all players; local time would give Tokyo a different puzzle than Paris
PRNG / seeding Pseudo-random number generator; a seeded one produces the same sequence from the same seed Same daily puzzle for every player, computed from the date, stored nowhere
iframe / webview A browser page embedded inside another page Your game runs in an iframe inside Reddit's post

One deliberate absence: no framework. React/Phaser templates exist and are
fine, but vanilla TypeScript + HTML removes a build-tooling learning curve
from week one. Add a framework when you feel the pain it solves, not before.

3. Accounts and tooling, step by step

Do these in order. Budget one evening.

  1. Reddit account. reddit.com/register. Use a dedicated dev account — its username appears whenever your app posts or comments (Reddit shows "This is an automated account" next to it, which is good: players trust labeled bots more than disguised ones).
  2. Developer portal. Sign in at developers.reddit.com with that account. This dashboard lists your apps, their versions, installs, and analytics.
  3. GitHub account (github.com) — for code hosting and the CI robots.
  4. Node.js LTSnodejs.org (v22+ is what Devvit currently wants). Verify in a terminal: node --version.
  5. An editor. VS Code is the default choice; the repo's .vscode settings already carry sensible defaults.
  6. The Devvit CLI:
   npm install -g devvit
   devvit login        # opens the browser, authorizes the CLI
   devvit whoami       # sanity check
Enter fullscreen mode Exit fullscreen mode
  1. Get the code. Either fork/copy the official template:
   npm create devvit@latest -- --template=bare my-game
Enter fullscreen mode Exit fullscreen mode

or study a complete example — every game in
this repo is a working
reference.

  1. A dev subreddit. The CLI creates one automatically on first devvit playtest (named like u_yourname_dev). This is your private staging area: installs, posts, and crashes here affect nobody.

Naming rule you cannot undo: Reddit app names are globally unique and
permanent
. hilo, higher-lower, and higher-or-lower were all taken
before this project settled on play-highlow. Renaming means registering a
new app (new listing, new dev sub, old one dormant forever). Pick a
brandable family name (play-*, daily-*) before your first upload, and
never put "reddit" or "snoo" in the name — those are Reddit's trademarks.

4. Your first app — anatomy and the request path

4.1 The file tree

Every game in this repo has the same shape (based on the official
devvit-template-bare):

games/play-highlow/
├── devvit.json          ← the app manifest: name, entrypoints, permissions, cron
├── package.json         ← npm scripts + dependencies
├── public/              ← built browser files (what players actually load)
│   ├── splash.html      ← the card players see in the feed (entrypoint "default")
│   └── game.html        ← the full-screen game (entrypoint "game")
├── src/
│   ├── client/          ← browser code: UI, rendering, input
│   │   ├── splash.ts    └── game.ts, fetch.ts
│   ├── server/          ← Node code: endpoints, rules, database
│   │   ├── index.ts     └── server.ts, db.ts
│   └── shared/          ← the contract between the two
│       └── api.ts       ← endpoint names + request/response types
└── tsconfig*.json       ← TypeScript compiler settings
Enter fullscreen mode Exit fullscreen mode

The mental model: two programs, one contract. The client (browser)
renders and collects input. The server (Node) owns truth: state, scoring,
randomness. src/shared/api.ts is the typed contract between them — the
single file both sides import so that a mismatched request or response is a
compile error, not a production bug.

devvit.json wires it together:

{
  "name": "play-highlow",
  "post": { "entrypoints": {
    "default": { "entry": "splash.html" },
    "game":    { "entry": "game.html" } } },
  "server": {},
  "permissions": { "reddit": { "scope": "user", "asUser": ["SUBMIT_COMMENT"] } },
  "triggers": { "onAppInstall": "/internal/on/app/install" },
  "scheduler": { "tasks": { "daily-post": {
      "endpoint": "/internal/on/scheduler/daily-post",
      "cron": "0 0 * * *" } } }
}
Enter fullscreen mode Exit fullscreen mode

Reading it top to bottom: which HTML loads in a feed card vs full screen; that
the server exposes endpoints; that the app may submit comments as the
player
(only with the explicit SUBMIT_COMMENT scope — permissions are
grants, not defaults); that installing the app fires a trigger; and that a
cron task runs at 00:00 UTC every day. Every capability your app has should be
visible in this one file.

4.2 The architecture

flowchart LR
    subgraph Reddit["Reddit (the host you borrow)"]
        FEED["Community feed<br/>(your game = a post)"]
        SHELL["Post shell<br/>upvotes · comments · share"]
        IFRAME["iframe<br/>splash.html / game.html"]
        API["Reddit API<br/>posts · comments · identity"]
    end
    subgraph You["Your app (the part you own)"]
        CLIENT["Client bundle (TS → esbuild)<br/>UI · rendering · input"]
        SERVER["Game server (Node)<br/>routing · rules · scoring"]
        REDIS[("Redis<br/>state with TTLs")]
    end
    PLAYER(["Player"])

    PLAYER -->|"scrolls"| FEED
    FEED --> SHELL --> IFRAME
    IFRAME -->|"loads"| CLIENT
    CLIENT -->|"fetch('/api/…') JSON"| SERVER
    SERVER -->|"get/set/zAdd/expire"| REDIS
    SERVER -->|"submitPost · submitComment<br/>(as APP or USER)"| API
    CLIENT -.->|"never talks to Redis<br/>never talks to Reddit API directly"| REDIS

Why the split matters — the three rules that follow from it:

  1. The client is in the player's hands. They can open devtools, edit the JavaScript, and send any HTTP request they can imagine. So the client does rendering and feel; the server does truth and scoring. The client never generates the daily number, never grades its own grid, never sets its own rank.
  2. The server is stateless. Every request arrives with a context — the post it belongs to, the user asking, the community — and all durable state lives in Redis. Any request can be served by any server instance; nothing is kept "in memory" that matters.
  3. Everything crosses the boundary through shared/api.ts. Both sides import the same Endpoint names and request/response types. Change a field on one side only, and tsc --build refuses to compile.

4.3 The request path — trace one full action

Higher or Lower: the player guesses whether the next number is higher. The
complete journey of one tap:

sequenceDiagram
    actor P as Player
    participant C as Client (game.ts)
    participant F as fetch.ts
    participant S as Server (server.ts)
    participant D as db.ts
    participant R as Redis

    P->>C: tap "Higher"
    C->>F: fetchGuess({ guess: 'higher' })
    F->>S: POST /api/guess  { guess: 'higher' }
    S->>S: validate shape (bad shape → 400)
    S->>D: draw next number, compare, update score
    D->>R: ZADD top:<post> score member · SET run:<user> state
    R-->>D: OK
    D-->>S: { next: 42, correct: true, score: 3 }
    S-->>F: 200 OK (JSON)
    F-->>C: parsed response
    C->>C: render new number, animate, play sound
    C->>P: "3 correct! Next card…"

Follow that path in the code and you have learned the entire architecture:
src/client/game.ts (event listener) → src/client/fetch.ts (the fetch
call) → src/shared/api.ts (endpoint + types) → src/server/server.ts (the
routing switch) → src/server/db.ts (Redis calls) → JSON response → render.

4.4 Run it

cd my-game
npm install
npm run dev       # builds, watches, and opens a playtest post
Enter fullscreen mode Exit fullscreen mode

devvit playtest creates a real post in your dev subreddit running your local
code. Edit a file, refresh the post — the change is live. Watch server logs
with devvit logs r/<your-dev-sub> in a second terminal while you click
around; console.log in server code shows up there, and it is the first place
to look when something breaks.

This is what players actually see — the feed card and the game behind it:

Playing a Reddit game from the feed — the splash card renders in the post, ▶ Play opens the full game

4.5 Before you publish: the minimum quality bar

The three games that launched first passed this checklist; the lessons in
Part III come from the times we skipped parts of it:

  • [ ] Works at 360px wide (a phone in a hand, one thumb)
  • [ ] First interaction happens within 3 seconds of tapping Play
  • [ ] A loss and a win are both communicated (text, color, sound — one at minimum)
  • [ ] Refreshing mid-game loses nothing (see §6)
  • [ ] Every server endpoint validates its inputs and rejects bad ones with a status code
  • [ ] Types, lint, and build pass — then commit and push (§10)

Part II — The daily-games playbook

Arcade games get you installs; dailies get you returns. A daily game gives
every player on Earth the same small challenge each day, resets at midnight,
and makes the result shareable. It is the Wordle/ArenaTap pattern, it is the
pattern Reddit's own hackathons and Developer Funds reward (they pay for
return visits), and it is the highest-leverage thing you will build. This
whole part is the field manual, from the repo's
daily-challenge study and three shipped dailies.

5. What makes a game "daily"

Five properties, none optional:

  1. Deterministic content from the date. Every player gets the identical puzzle/board/deal for a given day — computed, not stored, by seeding a PRNG with the UTC date:
   // hash the string "solitaire:2026-09-12" → 32-bit seed → shuffle
   const rng = mulberry32(fnv1a(`solitaire:${day}`))
   const deal = Array.from({length: 52}, (_, i) => i)
   for (let k = deal.length - 1; k > 0; k--) {
     const j = Math.floor(rng() * (k + 1))
     ;[deal[k], deal[j]] = [deal[j] as number, deal[k] as number]
   }
Enter fullscreen mode Exit fullscreen mode

The seed is derived only from the date string, so every server instance,
every player, every retry — same deal. The puzzle "exists nowhere and
everywhere": no storage, no leak, no copy from a published book (which also
settles the IP question for classic puzzles — see
classic-games-research.md).

  1. UTC day keys, never local time.
   export function dayKey(now = new Date()): string {
     return now.toISOString().slice(0, 10)   // "2026-09-12"
   }
Enter fullscreen mode Exit fullscreen mode

If you derive the day from a local clock, midnight arrives 24 times around
the planet and Tokyo players race Paris players on different puzzles.

  1. One scored attempt per player per day. With a deterministic sequence,
    unlimited retries mean memorizing a perfect run — so the first completed
    run scores, later runs are practice. This is an integrity rule, not a
    meanness.

  2. A hard reset with a visible countdown. nextResetMs comes from the
    server (msToNextUtcMidnight()); the client shows ⏳ once you've played.
    The countdown is the "come back tomorrow" hook — loss aversion does your
    retention for you.

  3. Stale-day rejection at the server. Every scoring endpoint resolves
    which day this post belongs to and refuses to score a post from a
    previous day (409). Otherwise yesterday's finished board gets replayed for
    "wins" after midnight.

The full life of one day, across all the systems at once:

sequenceDiagram
    autonumber
    participant Cron as Scheduler (00:00 UTC)
    participant S as Server
    participant R as Redis
    participant Reddit as Reddit API
    participant P1 as Early bird
    participant P2 as Evening player

    Cron->>S: daily-post task fires (cron "0 0 * * *")
    S->>R: GET daily-post:2026-09-13 → (flag already set? stop)
    S->>Reddit: submitCustomPost "Sudoku Daily #4 — …"
    S->>R: SET daily-post:2026-09-13 t3_xxx (TTL 3d) + post-day:t3_xxx
    S->>Reddit: comment pinned how-to under the post
    S->>Reddit: comment yesterday's final board (🥇🥈🥉 + u/ mentions) under yesterday's post
    P1->>S: GET /api/daily (from the new post)
    S->>R: SET start:<day>:<player> (anti-cheat window starts)
    S->>R: GET puzzle:v2:2026-09-13:easy (cache hit? else generate + store)
    S-->>P1: givens, top-25, my state, streak, countdown
    P1->>S: POST /api/finish (grid, elapsedMs)
    S->>S: grade against solution · clamp clock · apply penalties
    S->>R: SET done:<day>:<player> · ZADD top:<day> · HSET streak:<player>
    S-->>P1: score, rank, streak — now Share / Post-to-comments unlock
    P2->>S: GET /api/daily … same puzzle, same everything
    Note over P2: sees "12 solved · fastest 2m 03s" — social proof
    Cron->>S: (next midnight) …

Steps 2–5 deserve a close look, because they are the automation layer that
makes this a product: the post creates itself, invites itself, summarizes
itself, and links yesterday's players to today's game — every day, in every
community that installed the app, with a Redis flag per day so a cron double
fire never posts twice.

6. State and the database question

"Do I need a database?" — you already have one: Redis, built into the
platform. Redis is a key–value store in memory. That's the whole model, and
it's enough, if you design your keys deliberately. The entire state model of
Sudoku Daily:

Key Type Holds TTL
sudoku:puzzle:v2:<day>:<difficulty> string (JSON) givens + solution, generated once 8 days
sudoku:done:<day>:<diff>:<player> string player's scored seconds 8 days
sudoku:top:<day>:<difficulty> sorted set member=username, score=seconds — the leaderboard 8 days
sudoku:start:<day>:<player> string first-fetch timestamp (anti-cheat window) 2 days
sudoku:checks:… / sudoku:hints:… hash failed-check / hint counters 2 days
streak:<player> hash {n, last} — consecutive-day counter none
post-day:<postId> string which day a post belongs to (archive lock) none
daily-post:<day> string post id for the day (dedupe flag) 3 days
commented:<day>:<player> string one-comment-per-day flag 2 days
results-posted:<day> string midnight-results dedupe flag 3 days

The patterns worth stealing:

  • Sorted sets are leaderboards. ZADD top:<day> score member, then ZRANGE … BY rank gives fastest-first; ZRANK gives a player's exact position; ZCARD gives "N solved today" for free.
  • TTL everything ephemeral. Leaderboards and per-day state expire in ~8 days — the platform's storage is not yours to hoard, and old keys rotting forever is how you get a slow, expensive app. Streaks are the exception: tiny, permanent, and the only long-term memory the game needs.
  • Idempotency flags. Any action that must happen at most once (daily post, results comment, weekly recap, player comment) writes a flag key and checks it first. Crons and retries are not reliable; flags make duplicates impossible regardless.
  • Key versioning for cache invalidation. When the meaning of a cached value changes, change the key name (sudoku:puzzle:sudoku:puzzle:v2:). §11 is the story of why this saved us.
  • Player identity is given, not built. context.userId when logged in, context.loid otherwise — one stable key per player with zero auth code. The server also displays context.username (or an anon- name) — and when turning names into mentions, never mention anon- handles.

Persistence beyond Redis has one caveat: the browser's localStorage is fine
for convenience state (an in-progress grid so a refresh doesn't lose work —
players forgive a lost game, not lost work), but it is per-device and
per-browser, so never for score or identity.

7. UI and UX that keep players

The daily games are played on phones, inside a Reddit app, one thumb. Every
design decision below exists because a real player hit the opposite.

The tap-tap rule (mobile-first input). Drag-and-drop — the desktop
solitaire control scheme — is where thumb-play goes to die: long-press
conflicts with scrolling, drop targets are small, and mid-drag finger occludes
the card. The fix used in Solitaire: tap a card to select, tap where it
goes.
And the 2026-09-12 improvement: tap it again to send it home to its
foundation, because the likeliest abandonment point was the endgame slog of
moving 13 cards one tap at a time. Once nothing is hidden and nothing is left
to draw, the game auto-finishes at 70 ms per card, pausing with a hint if a
card is still buried.

A complete winning Solitaire run — tap-tap moves, auto-finish, win screen with confetti

Feedback on every action. Numbers change (moves, timer), cards animate,
sounds confirm placements (with a mute button, persisted), a solved Sudoku
pops, confetti falls on a win. A silent tap feels broken even when it worked.

Error messages that teach. "1 cell off — fix the highlighted clashes.
Each failed check adds 30s" says what, where, and what it costs. Compare
"Invalid grid". The counter also makes re-checking-until-passing a visible
cost — server-counted, so clearing devtools doesn't clear it.

Reduced motion is accessibility, not decoration. All animation collapses
under prefers-reduced-motion — confetti included. Players who get motion
sickness from bouncy UI are a real minority you'll never hear from, because
they just leave.

The splash card is your store listing. Emoji logo, one-sentence promise,
three feature chips, a live line ("Daily #3 · 12 solved · fastest 1m 14s" —
social proof from api/stats), one enormous ▶ button, and a link to the
other dailies (cross-sell). The GIF at §4.4 is the whole pitch.

The Sudoku daily flow end to end — fill, check, teach-on-error, fix, win:

Sudoku daily flow — fill the grid, auto-check catches one wrong cell with clash highlighting and the penalty message, fixing it wins the game

Archive posts respect the player. Opening yesterday's numbered post shows
that day's puzzle and its final board (server refuses scoring), with a
"▶ Play today's" button. Nothing is sneakily replaced; nobody wastes ten moves
on a board that was never going to score.

Sizing bug class (the silent layout killer): a fanned card stack is
absolutely-positioned inside a relative pile; if the pile keeps its
aspect-ratio height, the fan overflows onto the buttons below. The fix is to
measure and set the pile height per render (width × 1.4 + (n−1) × fan-step).
Generalize: any UI that grows downward inside a phone viewport needs its
container to grow with it.

8. Engagement loops — making the app talk

An install is a coin flip; a habit is a machine. Each loop below is small, and
together they are why daily posts pull players back without any marketing.
Ship them in this order — the first two alone carry most of the value.

flowchart TB
    PLAY(["Player finishes today's run"]) --> SCORE["Server scores it<br/>(first run only)"]
    SCORE --> STREAK["🔥 streak +1<br/>visible chip; 'at risk' next day"]
    SCORE --> SHARE["Share button:<br/>spoiler-free emoji line + link"]
    SCORE --> ONETAP["💬 Post-to-comments:<br/>one tap, as the player"]
    SHARE --> FEED["Comments = ads<br/>every paste recruits players"]
    ONETAP --> FEED
    SCORE --> MENTION["Next midnight:<br/>yesterday's 🥇🥈🥉 with u/ mentions"]
    MENTION --> NOTIF["Reddit notifies the winners<br/>→ they return to defend"]
    NOTIF --> NEXT["Today's post links from<br/>yesterday's final board"]
    STREAK --> NEXT
    NEXT --> PLAY
    WEEKLY["Monday 00:05–00:14 UTC:<br/>📅 weekly recap post,<br/>one comment per game,<br/>podium + mentions"] --> NEXT
    PLAY -.-> WEEKLY

The mechanics, in shipping order:

  1. Streaks (🔥). Consecutive UTC days with a scored run — a hash {n, last} per player; alive if the last solve was today or yesterday. Yesterday's streak showing "at risk" today is the loss-aversion hook.
  2. Spoiler-free share text. Wordle's growth loop: the share line (Sudoku Daily #4 · easy · 3m 12s 🟩⬛🟩 · 🔥 5-day streak) brags without spoiling, navigator.share on mobile, clipboard fallback with the post URL baked in.
  3. The pinned how-to comment under every daily post — early comments push posts into feeds, and the pin is the invitation to add one.
  4. Midnight results with u/ mentions. At 00:00 UTC the app posts yesterday's podium under yesterday's post, links today's game. Winners get a Reddit notification — the platform's own return-visit channel, aimed at exactly the people most likely to return.
  5. One-tap "Post to comments." The share line becomes the player's own comment via submitComment({ runAs: 'USER' }) — which needs the explicit SUBMIT_COMMENT permission in devvit.json. Server gates: scored today, not archived, one per day, 280 chars. Every refusal keeps the old copy-paste path alive. Live-check still owed: a real tap from a logged-in account — note to self, from the repo's own playbook.
  6. Cross-sell. Every splash shows the other dailies; every game's live stats line normalizes "people play this every day."
  7. The weekly recap. Monday 00:05–00:14 UTC, each app (staggered minutes) finds-or-creates one shared "📅 This week in r/" post and comments its own week: per-player best across seven days, podium, mentions, solves. Apps cannot read each other's Redis — the post is the meeting point. Skips silently for a game with no scores that week.

Guardrails we hold to: no buying upvotes, no spamming the same promo across
subreddits, and never auto-posting into communities that didn't install the
game — all three get apps banned and poison the brand.

9. Reach — how strangers find your game

Reach on Reddit = fresh daily content × communities where you're installed
× sharing loops.
You control all three. The repo's
reach playbook tracks these as a living checklist;
here's the strategy.

Layer 1 — Reddit-native discovery (week one).

  • The auto-post every midnight UTC (shipped, §5) — each daily is a fresh shot at a community's front page. Rhythm beats timing: pick 00:00 UTC and never miss.
  • Launch posts: r/Devvit with Feedback-Request flair (pulled our first real bug report within hours — the King-to-empty-column solitaire fix), then one post per game on r/GamesOnReddit. Individual games travel better than bundles.
  • Niche subreddits where your game's audience already exists (r/sudoku, r/minesweeper, r/WebGames). Read each sub's self-promo rules, message mods where required, post as a developer sharing work-in-progress, and engage the comments. Respect the 9:1 norm — one good post per community, not a blitz.

Layer 2 — product loops that manufacture reach (weeks two–three). The
whole of §8 is this layer: shares and mentions are player-made distribution.

Layer 3 — platform programs (month one). App Directory polish (per-app
icon, keyword-rich one-liners — search is the only store surface), the
hackathons, Developer Funds once you have 2+ weeks of engagement data. Watch
App Analytics weekly and double down on the single game with the best D1/D7
retention instead of spreading effort evenly.

Metrics worth a weekly look: daily-post upvotes/comments, solves per day
per game, installs (2 communities → goal: 10+ by month end), streak counts,
and share-text pastes spotted in the wild.


Part III — Professional habits

10. Ship like a team of one, with Git and CI

You are one person, and that is exactly why the robots matter: a solo
developer has no reviewer, no QA, no release manager — so make GitHub do all
three. The workflow in this repo (and what you should copy):

Git in five verbs. A commit saves a snapshot with a message; a branch
is a parallel line of history; a push uploads it; a pull request (PR)
proposes "merge this branch into main"; merge is the yes. Never work on
main directly — one branch per change (feat/…, fix/…, docs/…), one
idea per branch, small diffs you can actually re-read a month later.

CI is your reviewer. .github/workflows/ci.yaml runs, for every game,
on every PR and on main:

- run: npm run test:types   # tsc --build — shape errors
- run: npm run lint         # biome — style + bug classes
- run: npm run build        # esbuild — it must actually bundle
Enter fullscreen mode Exit fullscreen mode

The rules of the house: a PR that isn't green doesn't merge — not for
"just one test", because the one time you bend it is the time the broken
version deploys. And commit messages are written for the future reader: the
why, not just the what ("fix: fillGrid never backtracked — every daily
puzzle was unwinnable" beats "fix bug").

The whole pipeline, idea to players:

flowchart LR
    DEV(["Idea / bug report"]) --> BR["git checkout -b fix/…<br/>small diff, one idea"]
    BR --> PR["push + open Pull Request"]
    PR --> CI{"CI: types · lint · build<br/>× every game"}
    CI -->|"red"| BR
    CI -->|"green"| MERGE["merge to main<br/>(history = your versions)"]
    MERGE --> PUB["devvit publish<br/>(upload source + consent)"]
    PUB --> APPROVE["approve version in<br/>the developer portal"]
    APPROVE --> LIVE(["LIVE — every install<br/>gets the upgrade"])
    LIVE -->|"cron fires at 00:00 UTC<br/>in every installed community"| OPS["live-ops: logs, analytics,<br/>player reports → back to DEV"]

11. Testing, and the bug every developer should study

On 2026-09-12 — three days after launch — a full test sweep found that
every Sudoku Daily puzzle ever served was unwinnable. Not flaky, not
hard: mathematically unsolvable for every player, every day, since day one.
The post-mortem is a complete software-engineering education in one bug.

The symptom. Sudoku Daily showed "Easy · 17 clues" (easy is supposed to
have 38). Completing any valid grid returned "64 cells off". Empty leaderboards
everywhere — because nobody, ever, could solve one.

The root cause. The puzzle generator's backtracking filler signalled
failure like this:

const done = fillGrid(rng, grid)
if (done) return done      // ← done is an ARRAY. Arrays are always truthy.
Enter fullscreen mode Exit fullscreen mode

fillGrid returns the grid. On a dead end it should say "this branch failed,
back up and try another value" — but its failure signal was also an array
(the partially-filled grid), which JavaScript treats as truthy. So every dead
end was celebrated as success, backtracking never happened, and whatever
cells happened to be filled at the first dead end became both the puzzle's
clues and its official "solution". A 64-zero solution. Every completed
player grid differed from it in 64 places. Forever.

Why the compiler didn't save us. fillGrid's return type was
number[] — "always returns a grid" is exactly what the type said. The type
system can only check the story you told it. The honest type is
number[] | null (fail or succeed), and once you declare that, tsc
refuses to compile if (done) — "object is not null" — wait, no: it refuses
everywhere the null case isn't handled. The type change forces the fix.

The fix (three lines) and the deploy trap (one more):

function fillGrid(rng: () => number, grid: number[] = Array(81).fill(0)): number[] | null {
  const index = grid.indexOf(0)
  if (index < 0) return grid
  for (const value of shuffledValues(rng)) {
    if (canPlace(grid, index, value)) {
      grid[index] = value
      if (fillGrid(rng, grid)) return grid   // truthiness is now meaningful
      grid[index] = 0                        // undo — this is backtracking
    }
  }
  return null                                // dead end: honest failure
}
Enter fullscreen mode Exit fullscreen mode

Deploying the fixed generator alone would not have fixed the game: broken
puzzles were cached in Redis (sudoku:puzzle:<day>:<difficulty>, 8-day TTL),
and a cache hit serves before generation runs. So the fix renamed the key
namespace
(sudoku:puzzle:v2:…) — cache invalidation by versioning, the
same trick as browser cache-busting with ?v=2. When the meaning of cached
data changes, change the key.

Validation. Ran the fixed generator against 60 days of seeds × 3
difficulties: 178/180 broken before the fix, 0/180 after, with every
difficulty landing on its clue target. That check is 180 deterministic
generations — it runs in seconds and would have caught this at commit time.

The lessons, generalized:

  1. Failure must be a value, not an implication. Return null/throw on failure; never let "the thing I return normally" double as the failure signal. Truthiness bugs are a JavaScript tax — know which values are falsy (0, "", null, undefined, NaNnot {} or []).
  2. Pure logic deserves its own tests. The generator is a pure function from (date, difficulty) to puzzle — trivially testable. Anything deterministic (seeding, scoring, rules) can be validated across thousands of inputs in seconds. Do it on every PR.
  3. A "solved" game needs an end-to-end test at least once. The bug survived because no one had ever finished a puzzle — including the developer. Replay a full game through your real client (the harness in this repo served the production bundle against a mock server and played a complete winning Solitaire game — 170 taps, one scoring call) before calling a game shipped.
  4. Watch the silent metric. Empty leaderboards were screaming. Metrics you don't look at can't tell you the product is broken.
  5. Cache invalidation is part of the fix, not an afterthought.

12. Publishing and operating

Publishing (full details in publishing.md):

cd games/my-game
DEVVIT_ALLOW_SOURCE_UPLOAD=1 npx devvit publish
Enter fullscreen mode Exit fullscreen mode

Then approve the uploaded version in the developer portal. Two hard-won
notes: the consent prompt appears even after an upload (hence the env var —
without it publish crashes), and devvit upload registers your app name
permanently
— the naming rule from §3 strikes here, at first upload.

What upgrading looks like. Publishing a new version updates every
installed community. Your onAppInstall trigger fires again on upgrade — so
make it idempotent (Redis flag post-created → installs never re-post the
intro post on upgrade). Scheduled tasks re-arm from the new devvit.json
a cron that fires mid-deploy catch-up is normal; the per-day Redis flag is
what makes a double fire harmless.

Live operations, the daily loop:

  • devvit logs r/<community> while interacting with the live post — your server's console.error is your only window when a player says "it's broken."
  • New-game feedback is gold: the r/Devvit feedback post produced the first real bug report (Kings couldn't move to empty solitaire columns — fixed and deployed same day, 0.0.13).
  • Version every deploy in the publishing doc (game → version → what changed). When a player on 0.0.15 reports a bug, you must know what they're running.
  • Every feature that reaches out to Reddit on a schedule (midnight results, weekly recap) gets a first-run check: the recap's first live run is Monday 2026-09-14, and the playbook says exactly what to look for (the 📅 post + four game comments).

13. Platform first, polish second, automation third

The arc of this repo is the arc we recommend:

  1. Fundamentals on a platform (week one). Ship a minimal game and learn the universal skills — client/server split, a database model, HTTP contracts, validation, deploy. The platform deletes the infrastructure work, not the learning.
  2. The pattern that compounds (weeks two–three). Add the daily loop: deterministic content, time-boxed runs, leaderboards, streaks, shares. This is one idea applied consistently, and it converts installs into habits.
  3. UI polish where the data says (ongoing). Watch where players abandon (the solitaire endgame), fix with interaction design (tap-tap, auto-finish), re-deploy. Polishing what retention data points at beats polishing what your pride points at.
  4. Automation as a product feature (the fun part). Once the fundamentals are solid, the app itself becomes staff: it writes the how-to, posts the daily thread, announces winners by name, publishes the weekly recap. Every cron task is an employee who never forgets. This is the "use UI to improve and automate" stage — and it only works because stages 1–3 made the game worth automating.
  5. Then the business layer. Developer Funds application once you have weeks of engagement data; developer verification + payments only for the one game with the best retention; your own site/stack only when a feature genuinely needs off-platform capabilities.

A parting reframe: the games are small on purpose. A 600-line client and a
600-line server that thousands of people play beat a 20,000-line ambition
that ships to nobody. Ship the small thing, watch real players, and let what
you learn — not what you imagine — pick the next change.


14. Glossary — every term we used

Git & shipping

Term Meaning
Commit A saved snapshot of your code with a message explaining why
Branch / main A parallel line of history; main is the blessed one that CI watches and deploys come from
Push / Pull Upload local commits to GitHub / download remote ones
Pull request (PR) A proposal to merge a branch, with review + CI attached
CI (Continuous Integration) Robots running types/lint/build on every PR; green = mergeable
Deploy / publish Getting code to players; on Devvit: devvit publish + portal approval
Semantic version 0.0.18 — the last number bumps per fix/deploy here
Idempotent Safe to run twice; same result as once (Redis flags enforce this)

TypeScript & the web

Term Term Meaning
TypeScript JavaScript + static types, checked at compile time by tsc
Type / interface / union Declared shape of data / named shape / "this or that" (`number[] \
Truthiness / falsy How JS coerces values in {% raw %}if — falsy: 0, "", null, undefined, NaN; everything else (including [], {}) is truthy
Bundler (esbuild) Combines + minifies source files into the one file the browser loads
fetch / HTTP verb Browser→server call / GET reads, POST writes
JSON Text format for structured data; the language of every endpoint
Endpoint / route A URL your server answers (api/daily) and the code that answers it
Status code 200 ok · 400 bad input · 401 log in · 409 conflict · 500 server broke
iframe / webview Browser page embedded in another page; how games render inside Reddit
localStorage Tiny per-browser key–value store; convenience only, never truth
prefers-reduced-motion Browser setting your CSS should honor by disabling animation

Data & correctness

Term Meaning
Redis In-memory key–value database built into the platform
Key / TTL The name a value lives under / time-to-live — auto-expiry
Sorted set (zSet) Redis structure ranked by score — the leaderboard primitive
Hash Redis object with named fields (streak:<player> → {n, last})
Deterministic / seed / PRNG Same input → same output; the number a generator starts from; the generator
UTC / day key / cron The world's clock / YYYY-MM-DD from it / time-based scheduler (0 0 * * *)
Cache invalidation Refreshing stale stored results — by TTL or by key versioning
Anti-cheat / trust boundary Never trust the client: server generates, validates, grades, clamps
D1 / D7 retention Share of players returning 1 / 7 days later — the metric platforms pay for
Live-ops Operating the game after launch: logs, analytics, deploys, player reports

Design

Term Meaning
UX / UI User experience (does it work for humans) / user interface (what they see and touch)
Thumb zone / tap target Screen reach with one hand / minimum finger-sized hit area
Feedback loop Every input gets a visible/audible response
Onboarding The first 30 seconds: the splash card + pinned how-to here
Social proof "12 solved · fastest 1m 14s" — others are playing, so should you
Loss aversion Streaks at risk; countdowns; the psychology the daily loop runs on

15. Appendix — repo map and further reading

This repo, as a curriculum:

To learn Read Then build
The minimal loop games/play-highlow — splash → endpoint → sorted-set leaderboard Rebuild it from the bare template
Server-side rules & counters games/play-rps — hashes, validation, global stats Best-of-5 matches
Client game-feel games/play-reflex — timers, states, sanity checks Sound + per-round storage
The daily pattern games/play-sudoku + daily-challenge-guide.md Port the daily loop to your own game
Sharing & engagement games/play-solitaire — share, one-tap comments, auto-finish Your own recap bot
Ops & lessons publishing.md, reach-playbook.md, launch-playbook.md

Official docs: the Devvit documentation
(platform, permissions, scheduler, Redis API), the
devvit-template-bare
starting point, and the developer portal's App Analytics.

The ArenaTap study (daily-challenge-guide.md)
is the reverse-engineering exercise this whole playbook descends from — worth
reading to see how much of a product you can infer from its public surface
before writing a line of code.


Version history of this guide

Saved as commits in this repo — every edit is one git log away.

Version Date What changed
v0.1 2026-09-12 Part I: why Reddit, the stack, setup, first app, architecture diagrams
v0.2 2026-09-12 Part II: daily playbook — determinism, Redis modeling, UI/UX, engagement loops, reach
v1.0 2026-09-12 Part III: Git/CI pipeline, the fillGrid case study, publishing & ops, glossary; GIFs; first complete release

Written by the developer of r/PlayQuickGames
with Claude. The games are live; the code is open; go build one.

Top comments (1)

Some comments may only be visible to logged-in visitors. Sign in to view all comments.