<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Lith SEO</title>
    <description>The latest articles on DEV Community by Lith SEO (@seolith).</description>
    <link>https://dev.to/seolith</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4123403%2F4fb1b1fe-0c51-4096-bfda-4ef783fdc7d7.png</url>
      <title>DEV Community: Lith SEO</title>
      <link>https://dev.to/seolith</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/seolith"/>
    <language>en</language>
    <item>
      <title>Building FoxyInvoice — Chapter 1: Every term you need, and where it will bite you</title>
      <dc:creator>Lith SEO</dc:creator>
      <pubDate>Sat, 19 Sep 2026 13:12:15 +0000</pubDate>
      <link>https://dev.to/seolith/building-foxyinvoice-chapter-1-every-term-you-need-and-where-it-will-bite-you-em6</link>
      <guid>https://dev.to/seolith/building-foxyinvoice-chapter-1-every-term-you-need-and-where-it-will-bite-you-em6</guid>
      <description>&lt;p&gt;&lt;em&gt;This series is written in the open, from a real production system. This chapter is the prerequisite map: six territories, each defined, each tied to where it actually broke in production. [All chapters and diagrams live in the public repo.]&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;This chapter is a map, not a textbook. Every term below gets the same&lt;br&gt;
treatment: what it is in plain words, where it lives in FoxyInvoice, and&lt;br&gt;
— because this series made an honesty contract with you — the real&lt;br&gt;
incident that shows why you should care. You don't need to master any of&lt;br&gt;
this before starting. You need to recognize each drawer when you have to&lt;br&gt;
open it.&lt;/p&gt;

&lt;p&gt;We'll cover six territories: &lt;strong&gt;Git, UI vs UX, databases, HTTP &amp;amp; APIs,&lt;br&gt;
Docker, and DNS &amp;amp; TLS.&lt;/strong&gt; In roughly the order they'll hurt you.&lt;/p&gt;




&lt;h2&gt;
  
  
  1. Git: time travel for a team of one
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;What it is.&lt;/strong&gt; Git stores your project's history as a chain of&lt;br&gt;
snapshots ("commits"). At any moment you can see what changed, who&lt;br&gt;
changed it, why (the commit message), and go back. GitHub is the cloud&lt;br&gt;
copy of that history — and, in our setup, the &lt;em&gt;trigger&lt;/em&gt; for deployment.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Where it lives here.&lt;/strong&gt; Everywhere. The rule that shapes all discipline&lt;br&gt;
in this repo: &lt;strong&gt;pushing to &lt;code&gt;main&lt;/code&gt; IS a production deploy.&lt;/strong&gt; There is no&lt;br&gt;
staging server, no release manager. &lt;code&gt;git push&lt;/code&gt; → tests → security gates&lt;br&gt;
→ eight minutes later it's live. That means the commit log is not&lt;br&gt;
bureaucracy; it's the changelog and the audit trail of the product.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The vocabulary you need:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;repository (repo)&lt;/strong&gt; — the project + its history. This app is one
repo containing backend (&lt;code&gt;src/&lt;/code&gt;), frontend (&lt;code&gt;web/&lt;/code&gt;), deploy config
(&lt;code&gt;deploy/&lt;/code&gt;), and CI (&lt;code&gt;.github/workflows/&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;clone / pull&lt;/strong&gt; — get the repo / get the latest.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;commit&lt;/strong&gt; — one snapshot with a message. The convention here:
&lt;code&gt;feat:&lt;/code&gt;, &lt;code&gt;fix:&lt;/code&gt;, &lt;code&gt;migration:&lt;/code&gt;, &lt;code&gt;docs:&lt;/code&gt; prefixes, written like
sentences to the future ("&lt;em&gt;fix: Edit invoice loads data — route input
bound after constructor&lt;/em&gt;").&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;branch&lt;/strong&gt; — a parallel line of work. In a team you'd branch per
feature and merge via pull request. Solo with auto-deploy, this repo
mostly commits straight to &lt;code&gt;main&lt;/code&gt; — but branches saved us during the
QA-harness work running in parallel with shipping.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;merge conflict&lt;/strong&gt; — two lines changed the same line of text. Git
refuses to guess; you pick. Scary the first time, routine by the
fifth.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;tag&lt;/strong&gt; — a named point in history (&lt;code&gt;blog-v1.0&lt;/code&gt;). We tag the blog's
milestones so every published revision is recoverable.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;git revert&lt;/code&gt;&lt;/strong&gt; — a commit that undoes a commit. Prefer it over
hand-editing "the fix" back in: history stays true, and history is
the story.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;The incident that teaches it.&lt;/strong&gt; During one shipping sprint, two lines&lt;br&gt;
of work ended up on the same repo — one committing to a feature branch,&lt;br&gt;
one shipping through temporary worktrees to &lt;code&gt;main&lt;/code&gt;. The branches&lt;br&gt;
diverged; the same file existed in two versions with different line&lt;br&gt;
endings, and a push of "everything" nearly shipped stale dependency&lt;br&gt;
files alongside new features. Recovery meant diffing trees&lt;br&gt;
(&lt;code&gt;git diff --ignore-cr-at-eol&lt;/code&gt; — line-ending noise is real) and&lt;br&gt;
extracting only the true changes. &lt;strong&gt;The lesson: commit small, commit&lt;br&gt;
often, one logical change per commit.&lt;/strong&gt; When every commit does one&lt;br&gt;
thing, you can cherry-pick, revert, and reason. When commits do five&lt;br&gt;
things, you can't.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;One more git habit that pays rent:&lt;/strong&gt; a strict &lt;code&gt;.gitignore&lt;/code&gt; plus an&lt;br&gt;
automated secret scanner in CI. Sooner or later you will paste an API&lt;br&gt;
key into a config file. The gate that catches it before the public repo&lt;br&gt;
does is the cheapest insurance in this whole series.&lt;/p&gt;




&lt;h2&gt;
  
  
  2. UI vs UX: the screen is not the journey
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;What they are.&lt;/strong&gt; &lt;strong&gt;UI (user interface)&lt;/strong&gt; is what's on the screen:&lt;br&gt;
buttons, spacing, color, typography. &lt;strong&gt;UX (user experience)&lt;/strong&gt; is whether&lt;br&gt;
the person succeeds: did they understand what to do, did it work, did&lt;br&gt;
they feel good afterward, did they come back? A beautiful UI with a&lt;br&gt;
broken journey fails. An ugly UI that gets someone paid wins.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Where it lives here.&lt;/strong&gt; The UI system is Angular Material — a component&lt;br&gt;
library of pre-built, accessible buttons, tables, form fields (more on&lt;br&gt;
"why a component library" in Chapter 03). The UX is a thousand&lt;br&gt;
decisions, three of which are worth showing you because they're all&lt;br&gt;
real, and two of them were bugs reported by a real user:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;The duplicate-invoice bug (pure UX).&lt;/strong&gt; After saving an invoice,&lt;br&gt;
the page stayed open with its button re-enabled. Users double-clicked&lt;br&gt;
— "invoice saved" but now there are three identical invoices. The UI&lt;br&gt;
was fine; the journey was broken. The fix was UX surgery: disable&lt;br&gt;
the button during the save flight, then navigate away to the list&lt;br&gt;
with a confirmation. The user's report said it perfectly: &lt;em&gt;"the&lt;br&gt;
message is displayed as invoice saved, but the page does not get&lt;br&gt;
closed automatically."&lt;/em&gt; Listen to users like that; they're doing&lt;br&gt;
your UX audit for free.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Progressive disclosure.&lt;/strong&gt; The app has a "simple view" navigation —&lt;br&gt;
Clients, Invoices, Payments, Settings — with everything else folded&lt;br&gt;
behind an "Advanced tools" button. First-time freelancers see five&lt;br&gt;
options, not forty. That's a UX principle (don't show what the user&lt;br&gt;
doesn't need &lt;em&gt;yet&lt;/em&gt;) expressed as a UI arrangement.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;The frozen preview (technical bug, UX symptom).&lt;/strong&gt; The invoice&lt;br&gt;
editor shows live totals as you type. One day the preview froze at&lt;br&gt;
its first value: tax and totals didn't update as line items changed.&lt;br&gt;
The UI displayed fine; the &lt;em&gt;experience&lt;/em&gt; of the feature was dead. Root&lt;br&gt;
cause was a re-system subtlety (a computed value tracking something&lt;br&gt;
it couldn't see). Users don't report "your reactivity graph is&lt;br&gt;
wrong" — they report "the numbers are wrong." Diagnose UX reports&lt;br&gt;
down to the technical layer.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;The rule to keep:&lt;/strong&gt; UI is what they see; UX is what they achieve. You&lt;br&gt;
will spend roughly ten times more effort on the second one, and most of&lt;br&gt;
that after launch, guided by feedback (Chapter 10 is that entire loop).&lt;/p&gt;




&lt;h2&gt;
  
  
  3. Databases: one table, one truth
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;What it is.&lt;/strong&gt; A relational database (we use &lt;strong&gt;PostgreSQL&lt;/strong&gt;) stores&lt;br&gt;
your application's facts as &lt;strong&gt;rows&lt;/strong&gt; in &lt;strong&gt;tables&lt;/strong&gt; with defined&lt;br&gt;
&lt;strong&gt;columns&lt;/strong&gt;. A &lt;strong&gt;primary key&lt;/strong&gt; uniquely identifies each row (here, a&lt;br&gt;
UUID); a &lt;strong&gt;foreign key&lt;/strong&gt; says "this row belongs to that one" (every&lt;br&gt;
invoice points at a client; every client points at a tenant/workspace).&lt;br&gt;
&lt;strong&gt;Indexes&lt;/strong&gt; make lookups fast and — just as important — can &lt;em&gt;enforce&lt;/em&gt;&lt;br&gt;
business rules: the &lt;code&gt;export_profiles&lt;/code&gt; table has a unique index on&lt;br&gt;
&lt;code&gt;tenant_id&lt;/code&gt;, so "one tax profile per workspace" is guaranteed by the&lt;br&gt;
database, not by hopeful application code.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The five-paragraph schema tour.&lt;/strong&gt; &lt;code&gt;tenants&lt;/code&gt; (workspaces) → &lt;code&gt;users&lt;/code&gt;&lt;br&gt;
(people, attached to tenants) → &lt;code&gt;clients&lt;/code&gt; (your customers) → &lt;code&gt;invoices&lt;/code&gt;&lt;br&gt;
(header: number, dates, status) → &lt;code&gt;invoice_line_items&lt;/code&gt; (qty × price&lt;br&gt;
rows). Everything else — payments, reminders, feedback, audit — hangs&lt;br&gt;
off that spine.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Lessons this repo paid full price for:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Money is &lt;code&gt;DECIMAL(18,2)&lt;/code&gt;, never a float.&lt;/strong&gt; Binary floats cannot&lt;br&gt;
represent &lt;code&gt;0.10&lt;/code&gt; exactly — the error is tiny, but accountants add&lt;br&gt;
thousands of numbers and notice. This app stores every amount as a&lt;br&gt;
decimal pair &lt;code&gt;(amount, currency)&lt;/code&gt; all the way down; the type system&lt;br&gt;
refuses to add dollars to euros (a &lt;code&gt;Money.Add&lt;/code&gt; across currencies&lt;br&gt;
throws — which once surfaced as a cryptic 500 we'll dissect in&lt;br&gt;
Chapter 13).&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Migrations are code, and code that doesn't run doesn't exist.&lt;/strong&gt; A&lt;br&gt;
database &lt;strong&gt;migration&lt;/strong&gt; is a versioned script that evolves the schema&lt;br&gt;
(create table, add column). One was committed to the repo &lt;em&gt;without&lt;br&gt;
its registration file&lt;/em&gt; — so the migration engine never saw it, the&lt;br&gt;
table never got created in production, and the feature returned 500s&lt;br&gt;
for every user. The fix taught rule 3.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Never change production schema by hand.&lt;/strong&gt; Twice, a column was&lt;br&gt;
added directly in &lt;code&gt;psql&lt;/code&gt; on the live database. Both times, the next&lt;br&gt;
deploy's proper migration collided with the hand-made change and&lt;br&gt;
&lt;strong&gt;crash-looped the API at startup&lt;/strong&gt; — a billing app, down, loudly.&lt;br&gt;
The surviving rule: migrations must be idempotent where reality is&lt;br&gt;
messy (&lt;code&gt;ADD COLUMN IF NOT EXISTS&lt;/code&gt;), and the hand-edit is a last&lt;br&gt;
resort you document immediately.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;An ORM is a lever with traps.&lt;/strong&gt; An &lt;strong&gt;ORM&lt;/strong&gt; (object-relational&lt;br&gt;
mapper — here, Entity Framework Core) lets application code speak in&lt;br&gt;
objects while it writes the SQL. Its best trick in this app:&lt;br&gt;
&lt;em&gt;global query filters&lt;/em&gt; that automatically scope every query to the&lt;br&gt;
current tenant (Chapter 04's isolation story). Its worst trap, also&lt;br&gt;
real: a repository method loaded an invoice &lt;em&gt;without its line&lt;br&gt;
items&lt;/em&gt;, so "edit line" returned 404 and "add line" silently&lt;br&gt;
recomputed totals against an empty set — data corruption wearing a&lt;br&gt;
boring bug's clothes.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Connection pools run out.&lt;/strong&gt; Postgres defaults to 100 connections;&lt;br&gt;
two application pools (API + worker, twice over for two stacks)&lt;br&gt;
can exhaust that under burst. Ours is set to 200 with a comment in&lt;br&gt;
the docs explaining why. Infrastructure defaults are decisions&lt;br&gt;
someone else made for a generic workload; your job is to notice&lt;br&gt;
which ones were wrong for yours.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  4. HTTP &amp;amp; APIs: the conversation format
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;What it is.&lt;/strong&gt; Every interaction between the browser app and the&lt;br&gt;
server is an HTTP &lt;strong&gt;request/response&lt;/strong&gt;: the browser asks (a &lt;strong&gt;verb&lt;/strong&gt;&lt;br&gt;
plus a &lt;strong&gt;URL&lt;/strong&gt; plus usually a JSON body), the server answers with a&lt;br&gt;
&lt;strong&gt;status code&lt;/strong&gt; plus JSON. That's the whole ceremony. "&lt;strong&gt;REST&lt;/strong&gt;" is the&lt;br&gt;
set of habits around making those URLs sensible:&lt;br&gt;
&lt;code&gt;GET /api/v1/invoices&lt;/code&gt; (list), &lt;code&gt;POST /api/v1/invoices&lt;/code&gt; (create),&lt;br&gt;
&lt;code&gt;PUT /api/v1/invoices/{id}&lt;/code&gt; (update), &lt;code&gt;DELETE&lt;/code&gt; for the obvious.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The status codes you'll actually meet&lt;/strong&gt; — each with a real endpoint&lt;br&gt;
from this app:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Code&lt;/th&gt;
&lt;th&gt;Meaning&lt;/th&gt;
&lt;th&gt;Real example&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;200&lt;/td&gt;
&lt;td&gt;OK&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;GET /healthz&lt;/code&gt; — the smoke test&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;201&lt;/td&gt;
&lt;td&gt;Created&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;POST /api/v1/invoices&lt;/code&gt; — a new invoice exists&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;400&lt;/td&gt;
&lt;td&gt;Your request was malformed&lt;/td&gt;
&lt;td&gt;posting an invalid email to &lt;code&gt;/public/template-drafts&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;401&lt;/td&gt;
&lt;td&gt;Not authenticated&lt;/td&gt;
&lt;td&gt;calling &lt;code&gt;/template-drafts/convert&lt;/code&gt; with no token&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;404&lt;/td&gt;
&lt;td&gt;Not found&lt;/td&gt;
&lt;td&gt;opening a shared invoice link with a bad token&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;409&lt;/td&gt;
&lt;td&gt;Conflict&lt;/td&gt;
&lt;td&gt;accepting a quote that was already handled — one-shot operations say "no" this way&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;500&lt;/td&gt;
&lt;td&gt;Our fault&lt;/td&gt;
&lt;td&gt;the crash-loop era; the server logs carry the stack trace&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Learning to &lt;em&gt;want&lt;/em&gt; the right code is a skill: 404-vs-409 is the&lt;br&gt;
difference between "gone" and "already did that," and clients behave&lt;br&gt;
better when you're precise.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Authentication, in one paragraph.&lt;/strong&gt; After login, the server hands the&lt;br&gt;
browser a &lt;strong&gt;JWT&lt;/strong&gt; (JSON Web Token): a signed ticket containing the&lt;br&gt;
user's id, tenant, and permissions. The browser shows it with every&lt;br&gt;
request (&lt;code&gt;Authorization: Bearer …&lt;/code&gt;); the server verifies the signature&lt;br&gt;
— no session table lookup needed. Long sessions use a second,&lt;br&gt;
rotating &lt;strong&gt;refresh token&lt;/strong&gt;. The vocabulary: &lt;em&gt;claims&lt;/em&gt; (facts inside the&lt;br&gt;
ticket), &lt;em&gt;expiry&lt;/em&gt; (tickets are short-lived on purpose), &lt;em&gt;scopes&lt;/em&gt; (what&lt;br&gt;
the ticket allows — ours carry permission names like&lt;br&gt;
&lt;code&gt;invoice:create&lt;/code&gt;).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Two patterns worth stealing:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Rate limiting&lt;/strong&gt; — public endpoints (feedback, template capture) sit
behind per-IP buckets. The internet will script your forms; make
scripting expensive.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The share token&lt;/strong&gt; — for the public invoice view and quote
acceptance, the URL contains a 32-byte unguessable token, &lt;em&gt;and the
token is the entire authorization&lt;/em&gt;. No login for the client, no data
leak (it scopes to exactly one document), expires on a clock.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  5. Docker: "works on my machine" dies here
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;What it is.&lt;/strong&gt; A &lt;strong&gt;Docker image&lt;/strong&gt; is a blueprint (built from a&lt;br&gt;
&lt;code&gt;Dockerfile&lt;/code&gt;: "start from this OS, install these dependencies, copy this&lt;br&gt;
code, run this command"). A &lt;strong&gt;container&lt;/strong&gt; is a running instance of that&lt;br&gt;
image — isolated processes with their own filesystem. &lt;strong&gt;Docker&lt;br&gt;
Compose&lt;/strong&gt; describes a whole stack (database, API, worker, proxy) in one&lt;br&gt;
YAML file so &lt;code&gt;docker compose up&lt;/code&gt; reproduces it identically anywhere.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Where it lives here.&lt;/strong&gt; One modest VPS runs, in containers: two&lt;br&gt;
Postgres instances, two .NET APIs, two workers, and a Caddy reverse&lt;br&gt;
proxy — the freemium stack and the enterprise stack side by side,&lt;br&gt;
sharing nothing but the machine. They find each other by &lt;strong&gt;container&lt;br&gt;
name&lt;/strong&gt; on a shared Docker &lt;strong&gt;network&lt;/strong&gt; (&lt;code&gt;caddy&lt;/code&gt; proxies to&lt;br&gt;
&lt;code&gt;fox-api:8080&lt;/code&gt; — that name is load-bearing). Database files live on a&lt;br&gt;
&lt;strong&gt;volume&lt;/strong&gt; (&lt;code&gt;pgdata&lt;/code&gt;), which is why you can rebuild containers all day&lt;br&gt;
and the data doesn't move.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The three lessons this stack paid for:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Environment files are credentials with a hat on.&lt;/strong&gt; The freemium&lt;br&gt;
stack's compose command &lt;em&gt;must&lt;/em&gt; include &lt;code&gt;-p fox --env-file&lt;br&gt;
.env.freemium&lt;/code&gt;. Run it without the env file once, and compose&lt;br&gt;
interpolates the &lt;em&gt;other&lt;/em&gt; stack's database credentials into the&lt;br&gt;
container, which then crash-loops on auth failure. Config-by-&lt;br&gt;
environment ("12-factor") is great; ambiguous defaults are not.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Secrets never go in layers.&lt;/strong&gt; The API image restores private&lt;br&gt;
NuGet packages, which needs a token. The Dockerfile declares a&lt;br&gt;
&lt;strong&gt;BuildKit secret&lt;/strong&gt;: the token is mounted during the build step and&lt;br&gt;
evaporates — never baked into the image or its history, where anyone&lt;br&gt;
with the image could read it.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;A masked failure is worse than a failure.&lt;/strong&gt; The deploy script once&lt;br&gt;
ran the image build as &lt;code&gt;build … || echo "WARNING: build failed&lt;br&gt;
(using cached image)"&lt;/code&gt; and then started containers with&lt;br&gt;
&lt;code&gt;up -d --no-build&lt;/code&gt;. The build failed (missing token), the script&lt;br&gt;
shrugged, and the deploy "succeeded" — shipping eight-hour-old&lt;br&gt;
containers while the pipeline showed green. &lt;strong&gt;Failures must be&lt;br&gt;
allowed to fail.&lt;/strong&gt; That one line of shell is the most expensive&lt;br&gt;
fourteen words in this chapter.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  6. DNS &amp;amp; TLS: how a name becomes a page
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;The chain, in order.&lt;/strong&gt; You type &lt;code&gt;foxyinvoice.com&lt;/code&gt; → &lt;strong&gt;DNS&lt;/strong&gt; (the&lt;br&gt;
internet's phone book) resolves the name to the server's IP via an&lt;br&gt;
&lt;strong&gt;A record&lt;/strong&gt; → your browser connects on port 443 → &lt;strong&gt;TLS&lt;/strong&gt; handshake:&lt;br&gt;
the server presents a &lt;strong&gt;certificate&lt;/strong&gt; (signed proof that this server is&lt;br&gt;
entitled to this name) → encrypted HTTP flows inside. That certificate&lt;br&gt;
is issued by Let's Encrypt, renewed automatically by Caddy — no cron&lt;br&gt;
jobs, no expiry surprises. This is "auto-TLS," and it's why we run&lt;br&gt;
Caddy at the edge.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Record types you'll touch:&lt;/strong&gt; &lt;strong&gt;A&lt;/strong&gt; (name → IP), &lt;strong&gt;MX&lt;/strong&gt; (where mail&lt;br&gt;
for the domain goes — ours route through Cloudflare Email Routing,&lt;br&gt;
which is its own Chapter 06 saga), &lt;strong&gt;TXT&lt;/strong&gt; (free-form text, used for&lt;br&gt;
ownership proofs like Search Console verification).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A real subtlety:&lt;/strong&gt; for the TLS certificate to be issued, the&lt;br&gt;
certificate authority must reach &lt;em&gt;our server directly&lt;/em&gt; — so the DNS&lt;br&gt;
records are "grey-clouded" (DNS-only) rather than proxied through&lt;br&gt;
Cloudflare's orange cloud. One toggle in a dashboard, decided by&lt;br&gt;
understanding what the handshake needs.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;And one forward pointer:&lt;/strong&gt; the edge is also where crawler hygiene&lt;br&gt;
lives. Before our app executes a single line, Caddy has already decided&lt;br&gt;
— per URL — whether the response carries &lt;code&gt;X-Robots-Tag: noindex&lt;/code&gt; or is&lt;br&gt;
allowed into search indexes. The public marketing pages are on an&lt;br&gt;
explicit allowlist; everything else (your invoices, the admin console,&lt;br&gt;
&lt;code&gt;/upgrade&lt;/code&gt;) tells crawlers to stay away. Security and discoverability&lt;br&gt;
both start at the front door.&lt;/p&gt;




&lt;h2&gt;
  
  
  The minimum setup
&lt;/h2&gt;

&lt;p&gt;You can follow this entire series with:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Git&lt;/strong&gt; and a &lt;strong&gt;GitHub account&lt;/strong&gt; (free).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;VS Code&lt;/strong&gt; (or any editor) and a terminal you don't fear.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Docker Desktop&lt;/strong&gt; — even if you never write a Dockerfile, running
the stack is one command.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Node.js LTS&lt;/strong&gt; and the &lt;strong&gt;.NET SDK&lt;/strong&gt; — only if you'll modify code,
not if you're reading.&lt;/li&gt;
&lt;li&gt;The willingness to read an error message top to bottom before
panicking. Stack traces are biographies, not insults.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;The whole map, rendered live:&lt;/strong&gt; &lt;a href="https://foxyinvoice.com/blog/01-prerequisites/" rel="noopener noreferrer"&gt;What you now know — Git, UI/UX, Databases, HTTP &amp;amp; APIs, Docker, DNS &amp;amp; TLS, and where each one bites&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Every one of those arrows has a scar somewhere in this repo's history —&lt;br&gt;
and each scar gets its full story in the chapters ahead.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Reading this and want to build along on the real thing? Create a free workspace at&lt;br&gt;
&lt;a href="https://foxyinvoice.com/login" rel="noopener noreferrer"&gt;foxyinvoice.com&lt;/a&gt;, then redeem founding code&lt;br&gt;
&lt;code&gt;U8B4Z8S87X&lt;/code&gt; on the Upgrade page — 6 months of Pro, free, no card. If anything&lt;br&gt;
breaks, there's a feedback button in the app. I read every one.&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Next: Chapter 2 — Product thinking: pick a real problem, price it, position it.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>saas</category>
      <category>webdev</category>
      <category>tutorial</category>
      <category>beginners</category>
    </item>
    <item>
      <title>Building FoxyInvoice — Chapter 0: We built an invoicing SaaS, and you can too</title>
      <dc:creator>Lith SEO</dc:creator>
      <pubDate>Sat, 19 Sep 2026 12:21:33 +0000</pubDate>
      <link>https://dev.to/seolith/building-foxyinvoice-chapter-0-we-built-an-invoicing-saas-and-you-can-too-4pd9</link>
      <guid>https://dev.to/seolith/building-foxyinvoice-chapter-0-we-built-an-invoicing-saas-and-you-can-too-4pd9</guid>
      <description>&lt;p&gt;&lt;em&gt;This series is written in the open, from a real production system. Chapter 1 covers every prerequisite term — Git, UI/UX, databases, HTTP, Docker, DNS — each tied to a real incident. [All chapters and diagrams live in the public repo.]&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;This is the story of &lt;strong&gt;FoxyInvoice&lt;/strong&gt; — a real, live, multi-tenant&lt;br&gt;
invoicing SaaS — and a complete, step-by-step guide to building,&lt;br&gt;
hosting, securing, operating, and growing one yourself. Not a toy&lt;br&gt;
tutorial: the app this series documents sends real invoices, takes real&lt;br&gt;
payments through Stripe, calculates real US sales tax, and serves real&lt;br&gt;
users today at &lt;a href="https://foxyinvoice.com" rel="noopener noreferrer"&gt;foxyinvoice.com&lt;/a&gt;. Its&lt;br&gt;
enterprise twin runs the books of an actual business at&lt;br&gt;
&lt;a href="https://invoices.seolith.com" rel="noopener noreferrer"&gt;invoices.seolith.com&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Everything you'll read here happened. The bugs are real bugs. The costs&lt;br&gt;
are real costs. When we tell you a deploy once silently shipped stale&lt;br&gt;
code for eight hours while reporting success — that's a Thursday we&lt;br&gt;
lived, not a hypothetical. That's the difference between this series and&lt;br&gt;
most "build a SaaS" content: &lt;strong&gt;you get the postmortems, not just the&lt;br&gt;
brochure.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What you'll build (and learn)
&lt;/h2&gt;

&lt;p&gt;By the end of this series you will understand, end to end:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The stack and why each piece is there&lt;/strong&gt; — Angular for the app, .NET
for the API, Postgres for truth, a worker for everything slow, Docker
to make it reproducible, one Caddy in front of it all. Boring on
purpose: boring survives production.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Multi-tenant data security&lt;/strong&gt; — how thousands of businesses can share
one database without ever seeing each other's invoices, enforced at
the query layer and &lt;em&gt;proven&lt;/em&gt; by tests.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Deployment as a habit&lt;/strong&gt; — push to &lt;code&gt;main&lt;/code&gt;, and eight minutes later
it's live with health gates, secret scanning, and automatic rollback
pressure. No staging server. No devops team. One person.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The economics&lt;/strong&gt; — the actual monthly cost sheet, Stripe's take,
what a free-tier user costs you (spoiler: almost nothing), and where
the break-even lines really sit.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The operating cadence&lt;/strong&gt; — the 20-minute daily routine that runs the
whole business: find people with the problem (Reddit, HN), read every
piece of feedback, turn bugs into fixes into deploys into replies.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Modern reach&lt;/strong&gt; — why the AI crawlers (GPTBot, ClaudeBot,
PerplexityBot) couldn't see client-side-rendered pages, and how being
machine-readable became a growth strategy, not a chore.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Who this is for
&lt;/h2&gt;

&lt;p&gt;You, if you can write some code and want to ship a product with it. We&lt;br&gt;
teach every term as it comes up — Git, UI vs UX, relational databases,&lt;br&gt;
REST APIs, DNS, TLS, Docker — always tied to the exact place it matters&lt;br&gt;
in this app. If you already know a term, skim; the &lt;em&gt;why&lt;/em&gt; is usually&lt;br&gt;
still worth your time, because the why comes from an incident.&lt;/p&gt;

&lt;p&gt;You do &lt;strong&gt;not&lt;/strong&gt; need: a team, investors, a staging environment, or a&lt;br&gt;
budget beyond a coffee-priced VPS and a domain.&lt;/p&gt;

&lt;h2&gt;
  
  
  The honesty contract
&lt;/h2&gt;

&lt;p&gt;Three rules this series lives by:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Every claim is checkable.&lt;/strong&gt; Numbers, dates, and behaviors come from
the production system and its git history.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Mistakes stay in.&lt;/strong&gt; The six incidents in the final chapters are
the most valuable pages in this series. You will make different
mistakes; the &lt;em&gt;shapes&lt;/em&gt; will rhyme.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No fantasy scale.&lt;/strong&gt; This is not a "how we got 100k users" story.
It's a "how the machine works and what each part costs" story — the
thing nobody writes because it isn't glamorous. It's also the thing
you actually need before you have users to serve.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  The tour
&lt;/h2&gt;

&lt;p&gt;Before Chapter 1, watch the machine run: a visitor with no account&lt;br&gt;
picks an industry invoice template, fills it in, and their work&lt;br&gt;
auto-saves in their browser. They ask us to email it to them — we&lt;br&gt;
capture it, no signup required. When they're ready, one click carries&lt;br&gt;
that exact invoice into a new account as a real draft. A founder sends&lt;br&gt;
a quote link; the client approves it from their phone; a real invoice&lt;br&gt;
appears, ready to send with a payment link. Behind the screen: tenants&lt;br&gt;
isolated to the row, money stored as decimals never floats, deploys&lt;br&gt;
gated by tests, and a feedback widget that records the user's &lt;em&gt;screen&lt;/em&gt;&lt;br&gt;
when words aren't enough.&lt;/p&gt;

&lt;p&gt;That tour, diagram by diagram and gif by gif, is this series.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Reading this and want to kick the tires for real? Create a free workspace at&lt;br&gt;
&lt;a href="https://foxyinvoice.com/login" rel="noopener noreferrer"&gt;foxyinvoice.com&lt;/a&gt;, then redeem founding code&lt;br&gt;
&lt;code&gt;U8B4Z8S87X&lt;/code&gt; on the Upgrade page — 6 months of Pro, free, no card. If anything&lt;br&gt;
breaks, there's a feedback button in the app. I read every one.&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Next: Chapter 1 — Prerequisites: every term you need, and where it will bite you.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>saas</category>
      <category>webdev</category>
      <category>tutorial</category>
      <category>beginners</category>
    </item>
    <item>
      <title>Building Games on Reddit — the complete guide for would-be developers</title>
      <dc:creator>Lith SEO</dc:creator>
      <pubDate>Sun, 13 Sep 2026 18:28:42 +0000</pubDate>
      <link>https://dev.to/seolith/building-games-on-reddit-the-complete-guide-for-would-be-developers-1g4l</link>
      <guid>https://dev.to/seolith/building-games-on-reddit-the-complete-guide-for-would-be-developers-1g4l</guid>
      <description>&lt;p&gt;&lt;em&gt;Version 1.0 · 2026-09-12 · Based on building, shipping, and operating six real&lt;br&gt;
Reddit games — three arcade games (Higher or Lower, Reaction Rush, RPS Duel)&lt;br&gt;
and three daily puzzle games (Sudoku, Minesweeper, Solitaire) — live at&lt;br&gt;
&lt;a href="https://www.reddit.com/r/PlayQuickGames/" rel="noopener noreferrer"&gt;r/PlayQuickGames&lt;/a&gt; since 2026-09-09,&lt;br&gt;
three weeks after the first line of code. Everything below is what actually&lt;br&gt;
happened, including the bug that made every Sudoku puzzle unwinnable for the&lt;br&gt;
first three days.&lt;/em&gt;&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;Why read this guide and not the official docs first?&lt;/strong&gt; The docs tell you&lt;br&gt;
what the platform can do. This tells you what to &lt;em&gt;build&lt;/em&gt;, in what order, and&lt;br&gt;
&lt;em&gt;why&lt;/em&gt; — with the mistakes included. Keep the official&lt;br&gt;
&lt;a href="https://developers.reddit.com/docs/" rel="noopener noreferrer"&gt;Devvit docs&lt;/a&gt; open in another tab; each&lt;br&gt;
section here links to the part of the docs it uses.&lt;/p&gt;

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


&lt;h2&gt;
  
  
  Contents
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Part I — Foundations&lt;/strong&gt;

&lt;ul&gt;
&lt;li&gt;1. Why build games on Reddit first&lt;/li&gt;
&lt;li&gt;2. The 20% of technology you actually need&lt;/li&gt;
&lt;li&gt;3. Accounts and tooling, step by step&lt;/li&gt;
&lt;li&gt;4. Your first app — anatomy and the request path&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Part II — The daily-games playbook&lt;/strong&gt;

&lt;ul&gt;
&lt;li&gt;5. What makes a game "daily"&lt;/li&gt;
&lt;li&gt;6. State and the database question&lt;/li&gt;
&lt;li&gt;7. UI and UX that keep players&lt;/li&gt;
&lt;li&gt;8. Engagement loops — making the app talk&lt;/li&gt;
&lt;li&gt;9. Reach — how strangers find your game&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Part III — Professional habits&lt;/strong&gt;

&lt;ul&gt;
&lt;li&gt;10. Ship like a team of one, with Git and CI&lt;/li&gt;
&lt;li&gt;11. Testing, and the bug every developer should study&lt;/li&gt;
&lt;li&gt;12. Publishing and operating&lt;/li&gt;
&lt;li&gt;13. Platform first, polish second, automation third&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;14. Glossary — every term we used&lt;/li&gt;
&lt;li&gt;15. Appendix — repo map and further reading&lt;/li&gt;
&lt;/ul&gt;


&lt;h1&gt;
  
  
  Part I — Foundations
&lt;/h1&gt;
&lt;h2&gt;
  
  
  1. Why build games on Reddit first
&lt;/h2&gt;

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

&lt;p&gt;Reddit's developer platform (&lt;a href="https://developers.reddit.com/" rel="noopener noreferrer"&gt;Devvit&lt;/a&gt;) hands&lt;br&gt;
you all of it for free:&lt;/p&gt;

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

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

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

&lt;p&gt;A concrete data point from this repo: six games, zero marketing — the first&lt;br&gt;
organic players arrived within 24 hours of the games being listed, purely from&lt;br&gt;
posts appearing in one small community's feed. Try getting that with a&lt;br&gt;
portfolio website.&lt;/p&gt;
&lt;h2&gt;
  
  
  2. The 20% of technology you actually need
&lt;/h2&gt;

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

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

&lt;p&gt;One deliberate absence: &lt;strong&gt;no framework&lt;/strong&gt;. React/Phaser templates exist and are&lt;br&gt;
fine, but vanilla TypeScript + HTML removes a build-tooling learning curve&lt;br&gt;
from week one. Add a framework when you feel the pain it solves, not before.&lt;/p&gt;
&lt;h2&gt;
  
  
  3. Accounts and tooling, step by step
&lt;/h2&gt;

&lt;p&gt;Do these in order. Budget one evening.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Reddit account.&lt;/strong&gt; &lt;a href="https://www.reddit.com/register/" rel="noopener noreferrer"&gt;reddit.com/register&lt;/a&gt;.
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).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Developer portal.&lt;/strong&gt; Sign in at &lt;a href="https://developers.reddit.com/" rel="noopener noreferrer"&gt;developers.reddit.com&lt;/a&gt;
with that account. This dashboard lists your apps, their versions, installs,
and analytics.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;GitHub account&lt;/strong&gt; (&lt;a href="https://github.com" rel="noopener noreferrer"&gt;github.com&lt;/a&gt;) — for code hosting and
the CI robots.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Node.js LTS&lt;/strong&gt; — &lt;a href="https://nodejs.org" rel="noopener noreferrer"&gt;nodejs.org&lt;/a&gt; (v22+ is what Devvit
currently wants). Verify in a terminal: &lt;code&gt;node --version&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;An editor.&lt;/strong&gt; &lt;a href="https://code.visualstudio.com/" rel="noopener noreferrer"&gt;VS Code&lt;/a&gt; is the default
choice; the repo's &lt;code&gt;.vscode&lt;/code&gt; settings already carry sensible defaults.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The Devvit CLI:&lt;/strong&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;   npm &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-g&lt;/span&gt; devvit
   devvit login        &lt;span class="c"&gt;# opens the browser, authorizes the CLI&lt;/span&gt;
   devvit &lt;span class="nb"&gt;whoami&lt;/span&gt;       &lt;span class="c"&gt;# sanity check&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Get the code.&lt;/strong&gt; Either fork/copy the official template:
&lt;/li&gt;
&lt;/ol&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;   npm create devvit@latest &lt;span class="nt"&gt;--&lt;/span&gt; &lt;span class="nt"&gt;--template&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;bare my-game
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;or study a complete example — every game in&lt;br&gt;
   &lt;a href="https://gitlab.com/playquick-group/PlayQuick-project" rel="noopener noreferrer"&gt;this repo&lt;/a&gt; is a working&lt;br&gt;
   reference.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;A dev subreddit.&lt;/strong&gt; The CLI creates one automatically on first
&lt;code&gt;devvit playtest&lt;/code&gt; (named like &lt;code&gt;u_yourname_dev&lt;/code&gt;). This is your private
staging area: installs, posts, and crashes here affect nobody.&lt;/li&gt;
&lt;/ol&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Naming rule you cannot undo:&lt;/strong&gt; Reddit app names are &lt;strong&gt;globally unique and&lt;br&gt;
permanent&lt;/strong&gt;. &lt;code&gt;hilo&lt;/code&gt;, &lt;code&gt;higher-lower&lt;/code&gt;, and &lt;code&gt;higher-or-lower&lt;/code&gt; were all taken&lt;br&gt;
before this project settled on &lt;code&gt;play-highlow&lt;/code&gt;. Renaming means registering a&lt;br&gt;
&lt;em&gt;new&lt;/em&gt; app (new listing, new dev sub, old one dormant forever). Pick a&lt;br&gt;
brandable family name (&lt;code&gt;play-*&lt;/code&gt;, &lt;code&gt;daily-*&lt;/code&gt;) before your first upload, and&lt;br&gt;
never put "reddit" or "snoo" in the name — those are Reddit's trademarks.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;
  
  
  4. Your first app — anatomy and the request path
&lt;/h2&gt;
&lt;h3&gt;
  
  
  4.1 The file tree
&lt;/h3&gt;

&lt;p&gt;Every game in this repo has the same shape (based on the official&lt;br&gt;
&lt;code&gt;devvit-template-bare&lt;/code&gt;):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;games/play-highlow/
├── devvit.json          ← the app manifest: name, entrypoints, permissions, cron
├── package.json         ← npm scripts + dependencies
├── public/              ← built browser files &lt;span class="o"&gt;(&lt;/span&gt;what players actually load&lt;span class="o"&gt;)&lt;/span&gt;
│   ├── splash.html      ← the card players see &lt;span class="k"&gt;in &lt;/span&gt;the feed &lt;span class="o"&gt;(&lt;/span&gt;entrypoint &lt;span class="s2"&gt;"default"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
│   └── game.html        ← the full-screen game &lt;span class="o"&gt;(&lt;/span&gt;entrypoint &lt;span class="s2"&gt;"game"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
├── 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&lt;span class="k"&gt;*&lt;/span&gt;.json       ← TypeScript compiler settings
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;p&gt;&lt;code&gt;devvit.json&lt;/code&gt; wires it together:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"play-highlow"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"post"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"entrypoints"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"default"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"entry"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"splash.html"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"game"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"entry"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"game.html"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"server"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"permissions"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"reddit"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"scope"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"user"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"asUser"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"SUBMIT_COMMENT"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"triggers"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"onAppInstall"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"/internal/on/app/install"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"scheduler"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"tasks"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"daily-post"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"endpoint"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"/internal/on/scheduler/daily-post"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"cron"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"0 0 * * *"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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


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

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



&lt;p&gt;Why the split matters — the three rules that follow from it:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;The client is in the player's hands.&lt;/strong&gt; They can open devtools, edit the
JavaScript, and send any HTTP request they can imagine. So the client does
rendering and &lt;em&gt;feel&lt;/em&gt;; the server does truth and &lt;em&gt;scoring&lt;/em&gt;. The client never
generates the daily number, never grades its own grid, never sets its own
rank.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The server is stateless.&lt;/strong&gt; Every request arrives with a &lt;code&gt;context&lt;/code&gt; — 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.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Everything crosses the boundary through &lt;code&gt;shared/api.ts&lt;/code&gt;.&lt;/strong&gt; Both sides
import the same &lt;code&gt;Endpoint&lt;/code&gt; names and request/response types. Change a field
on one side only, and &lt;code&gt;tsc --build&lt;/code&gt; refuses to compile.&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;
  
  
  4.3 The request path — trace one full action
&lt;/h3&gt;

&lt;p&gt;Higher or Lower: the player guesses whether the next number is higher. The&lt;br&gt;
complete journey of one tap:&lt;br&gt;
&lt;/p&gt;

&lt;pre data-lang="mermaid"&gt;&lt;code&gt;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-&amp;gt;&amp;gt;C: tap "Higher"
    C-&amp;gt;&amp;gt;F: fetchGuess({ guess: 'higher' })
    F-&amp;gt;&amp;gt;S: POST /api/guess  { guess: 'higher' }
    S-&amp;gt;&amp;gt;S: validate shape (bad shape → 400)
    S-&amp;gt;&amp;gt;D: draw next number, compare, update score
    D-&amp;gt;&amp;gt;R: ZADD top:&amp;lt;post&amp;gt; score member · SET run:&amp;lt;user&amp;gt; state
    R--&amp;gt;&amp;gt;D: OK
    D--&amp;gt;&amp;gt;S: { next: 42, correct: true, score: 3 }
    S--&amp;gt;&amp;gt;F: 200 OK (JSON)
    F--&amp;gt;&amp;gt;C: parsed response
    C-&amp;gt;&amp;gt;C: render new number, animate, play sound
    C-&amp;gt;&amp;gt;P: "3 correct! Next card…"&lt;/code&gt;&lt;/pre&gt;



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


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cd &lt;/span&gt;my-game
npm &lt;span class="nb"&gt;install
&lt;/span&gt;npm run dev       &lt;span class="c"&gt;# builds, watches, and opens a playtest post&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


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

&lt;p&gt;This is what players actually see — the feed card and the game behind it:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fypv0lt33r0ty5i09b5lh.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fypv0lt33r0ty5i09b5lh.gif" alt="Playing a Reddit game from the feed — the splash card renders in the post, ▶ Play opens the full game" width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;
  
  
  4.5 Before you publish: the minimum quality bar
&lt;/h3&gt;

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

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


&lt;h1&gt;
  
  
  Part II — The daily-games playbook
&lt;/h1&gt;

&lt;p&gt;Arcade games get you installs; &lt;strong&gt;dailies get you returns.&lt;/strong&gt; A daily game gives&lt;br&gt;
every player on Earth the same small challenge each day, resets at midnight,&lt;br&gt;
and makes the result shareable. It is the Wordle/ArenaTap pattern, it is the&lt;br&gt;
pattern Reddit's own hackathons and Developer Funds reward (they pay for&lt;br&gt;
&lt;em&gt;return visits&lt;/em&gt;), and it is the highest-leverage thing you will build. This&lt;br&gt;
whole part is the field manual, from the repo's&lt;br&gt;
&lt;a href="https://gitlab.com/playquick-group/PlayQuick-project/-/blob/main/docs/daily-challenge-guide.md" rel="noopener noreferrer"&gt;daily-challenge study&lt;/a&gt; and three shipped dailies.&lt;/p&gt;
&lt;h2&gt;
  
  
  5. What makes a game "daily"
&lt;/h2&gt;

&lt;p&gt;Five properties, none optional:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Deterministic content from the date.&lt;/strong&gt; Every player gets the identical
puzzle/board/deal for a given day — &lt;em&gt;computed&lt;/em&gt;, not stored, by seeding a
PRNG with the UTC date:
&lt;/li&gt;
&lt;/ol&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;   &lt;span class="c1"&gt;// hash the string "solitaire:2026-09-12" → 32-bit seed → shuffle&lt;/span&gt;
   &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;rng&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;mulberry32&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;fnv1a&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`solitaire:&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;day&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
   &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;deal&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Array&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="na"&gt;length&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;52&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
   &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;k&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;deal&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;k&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;k&lt;/span&gt;&lt;span class="o"&gt;--&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
     &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;j&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;floor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;rng&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;k&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
     &lt;span class="p"&gt;;[&lt;/span&gt;&lt;span class="nx"&gt;deal&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;k&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="nx"&gt;deal&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;j&lt;/span&gt;&lt;span class="p"&gt;]]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;deal&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;j&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;deal&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;k&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
   &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;The seed is derived only from the date string, so every server instance,&lt;br&gt;
   every player, every retry — same deal. The puzzle "exists nowhere and&lt;br&gt;
   everywhere": no storage, no leak, no copy from a published book (which also&lt;br&gt;
   settles the IP question for classic puzzles — see&lt;br&gt;
   &lt;a href="https://gitlab.com/playquick-group/PlayQuick-project/-/blob/main/docs/classic-games-research.md" rel="noopener noreferrer"&gt;classic-games-research.md&lt;/a&gt;).&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;UTC day keys, never local time.&lt;/strong&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;   &lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;dayKey&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;now&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;()):&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
     &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;now&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toISOString&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;slice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;   &lt;span class="c1"&gt;// "2026-09-12"&lt;/span&gt;
   &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;If you derive the day from a local clock, midnight arrives 24 times around&lt;br&gt;
   the planet and Tokyo players race Paris players on &lt;em&gt;different&lt;/em&gt; puzzles.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;One scored attempt per player per day.&lt;/strong&gt; With a deterministic sequence,&lt;br&gt;
unlimited retries mean memorizing a perfect run — so the &lt;em&gt;first&lt;/em&gt; completed&lt;br&gt;
run scores, later runs are practice. This is an integrity rule, not a&lt;br&gt;
meanness.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;A hard reset with a visible countdown.&lt;/strong&gt; &lt;code&gt;nextResetMs&lt;/code&gt; comes from the&lt;br&gt;
server (&lt;code&gt;msToNextUtcMidnight()&lt;/code&gt;); the client shows ⏳ once you've played.&lt;br&gt;
The countdown is the "come back tomorrow" hook — loss aversion does your&lt;br&gt;
retention for you.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Stale-day rejection at the server.&lt;/strong&gt; Every scoring endpoint resolves&lt;br&gt;
&lt;em&gt;which day this post belongs to&lt;/em&gt; and refuses to score a post from a&lt;br&gt;
previous day (409). Otherwise yesterday's finished board gets replayed for&lt;br&gt;
"wins" after midnight.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The full life of one day, across all the systems at once:&lt;br&gt;
&lt;/p&gt;

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



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

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

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

&lt;p&gt;The patterns worth stealing:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Sorted sets are leaderboards.&lt;/strong&gt; &lt;code&gt;ZADD top:&amp;lt;day&amp;gt; score member&lt;/code&gt;, then
&lt;code&gt;ZRANGE … BY rank&lt;/code&gt; gives fastest-first; &lt;code&gt;ZRANK&lt;/code&gt; gives a player's exact
position; &lt;code&gt;ZCARD&lt;/code&gt; gives "N solved today" for free.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;TTL everything ephemeral.&lt;/strong&gt; 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.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Idempotency flags.&lt;/strong&gt; Any action that must happen &lt;em&gt;at most once&lt;/em&gt; (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.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Key versioning for cache invalidation.&lt;/strong&gt; When the &lt;em&gt;meaning&lt;/em&gt; of a cached
value changes, change the key name (&lt;code&gt;sudoku:puzzle:&lt;/code&gt; → &lt;code&gt;sudoku:puzzle:v2:&lt;/code&gt;).
§11 is the story of
why this saved us.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Player identity is given, not built.&lt;/strong&gt; &lt;code&gt;context.userId&lt;/code&gt; when logged in,
&lt;code&gt;context.loid&lt;/code&gt; otherwise — one stable key per player with zero auth code.
The server also displays &lt;code&gt;context.username&lt;/code&gt; (or an &lt;code&gt;anon-&lt;/code&gt; name) — and when
turning names into mentions, never mention &lt;code&gt;anon-&lt;/code&gt; handles.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Persistence beyond Redis has one caveat: the browser's &lt;code&gt;localStorage&lt;/code&gt; is fine&lt;br&gt;
for &lt;em&gt;convenience&lt;/em&gt; state (an in-progress grid so a refresh doesn't lose work —&lt;br&gt;
players forgive a lost game, not lost work), but it is per-device and&lt;br&gt;
per-browser, so never for score or identity.&lt;/p&gt;
&lt;h2&gt;
  
  
  7. UI and UX that keep players
&lt;/h2&gt;

&lt;p&gt;The daily games are played on phones, inside a Reddit app, one thumb. Every&lt;br&gt;
design decision below exists because a real player hit the opposite.&lt;/p&gt;

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

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F553r2gg0xbuj4iibxxj5.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F553r2gg0xbuj4iibxxj5.gif" alt="A complete winning Solitaire run — tap-tap moves, auto-finish, win screen with confetti" width="680" height="589"&gt;&lt;/a&gt;&lt;/p&gt;

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

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

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

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

&lt;p&gt;&lt;strong&gt;The Sudoku daily flow end to end&lt;/strong&gt; — fill, check, teach-on-error, fix, win:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F4g1hqhclcodpjaiwnytl.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F4g1hqhclcodpjaiwnytl.gif" alt="Sudoku daily flow — fill the grid, auto-check catches one wrong cell with clash highlighting and the penalty message, fixing it wins the game" width="680" height="680"&gt;&lt;/a&gt;&lt;/p&gt;

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

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

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

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



&lt;p&gt;The mechanics, in shipping order:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Streaks (🔥).&lt;/strong&gt; Consecutive UTC days with a scored run — a hash
&lt;code&gt;{n, last}&lt;/code&gt; per player; alive if the last solve was today or yesterday.
Yesterday's streak showing "at risk" today is the loss-aversion hook.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Spoiler-free share text.&lt;/strong&gt; Wordle's growth loop: the share line
(&lt;code&gt;Sudoku Daily #4 · easy · 3m 12s 🟩⬛🟩 · 🔥 5-day streak&lt;/code&gt;) brags without
spoiling, &lt;code&gt;navigator.share&lt;/code&gt; on mobile, clipboard fallback with the post URL
baked in.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The pinned how-to comment&lt;/strong&gt; under every daily post — early comments push
posts into feeds, and the pin is the invitation to add one.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Midnight results with &lt;code&gt;u/&lt;/code&gt; mentions.&lt;/strong&gt; 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.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;One-tap "Post to comments."&lt;/strong&gt; The share line becomes the player's own
comment via &lt;code&gt;submitComment({ runAs: 'USER' })&lt;/code&gt; — which needs the explicit
&lt;code&gt;SUBMIT_COMMENT&lt;/code&gt; permission in &lt;code&gt;devvit.json&lt;/code&gt;. Server gates: scored today,
not archived, one per day, 280 chars. Every refusal keeps the old
copy-paste path alive. &lt;strong&gt;Live-check still owed: a real tap from a
logged-in account&lt;/strong&gt; — note to self, from the repo's own playbook.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cross-sell.&lt;/strong&gt; Every splash shows the other dailies; every game's live
stats line normalizes "people play this every day."&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The weekly recap.&lt;/strong&gt; 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 — &lt;strong&gt;the post is the meeting point.&lt;/strong&gt;
Skips silently for a game with no scores that week.
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Guardrails we hold to: no buying upvotes, no spamming the same promo across&lt;br&gt;
subreddits, and never auto-posting into communities that didn't install the&lt;br&gt;
game — all three get apps banned and poison the brand.&lt;/p&gt;
&lt;h2&gt;
  
  
  9. Reach — how strangers find your game
&lt;/h2&gt;

&lt;p&gt;Reach on Reddit = &lt;strong&gt;fresh daily content × communities where you're installed&lt;br&gt;
× sharing loops.&lt;/strong&gt; You control all three. The repo's&lt;br&gt;
&lt;a href="https://gitlab.com/playquick-group/PlayQuick-project/-/blob/main/docs/reach-playbook.md" rel="noopener noreferrer"&gt;reach playbook&lt;/a&gt; tracks these as a living checklist;&lt;br&gt;
here's the strategy.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Layer 1 — Reddit-native discovery (week one).&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;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.&lt;/li&gt;
&lt;li&gt;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.&lt;/li&gt;
&lt;li&gt;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 &lt;em&gt;engage
the comments&lt;/em&gt;. Respect the 9:1 norm — one good post per community, not a
blitz.&lt;/li&gt;
&lt;/ul&gt;

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

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

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


&lt;h1&gt;
  
  
  Part III — Professional habits
&lt;/h1&gt;
&lt;h2&gt;
  
  
  10. Ship like a team of one, with Git and CI
&lt;/h2&gt;

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

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

&lt;p&gt;&lt;strong&gt;CI is your reviewer.&lt;/strong&gt; &lt;code&gt;.github/workflows/ci.yaml&lt;/code&gt; runs, for &lt;em&gt;every game&lt;/em&gt;,&lt;br&gt;
on every PR and on &lt;code&gt;main&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;npm run test:types&lt;/span&gt;   &lt;span class="c1"&gt;# tsc --build — shape errors&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;npm run lint&lt;/span&gt;         &lt;span class="c1"&gt;# biome — style + bug classes&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;npm run build&lt;/span&gt;        &lt;span class="c1"&gt;# esbuild — it must actually bundle&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;p&gt;The whole pipeline, idea to players:&lt;br&gt;
&lt;/p&gt;

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



&lt;h2&gt;
  
  
  11. Testing, and the bug every developer should study
&lt;/h2&gt;

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

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

&lt;p&gt;&lt;strong&gt;The root cause.&lt;/strong&gt; The puzzle generator's backtracking filler signalled&lt;br&gt;
failure like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;done&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;fillGrid&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;rng&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;grid&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;done&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;done&lt;/span&gt;      &lt;span class="c1"&gt;// ← done is an ARRAY. Arrays are always truthy.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

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

&lt;p&gt;&lt;strong&gt;The fix (three lines) and the deploy trap (one more):&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;fillGrid&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;rng&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;grid&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;81&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;fill&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)):&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;index&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;grid&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;indexOf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;index&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;grid&lt;/span&gt;
  &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nf"&gt;shuffledValues&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;rng&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;canPlace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;grid&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;index&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;grid&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;index&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt;
      &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;fillGrid&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;rng&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;grid&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;grid&lt;/span&gt;   &lt;span class="c1"&gt;// truthiness is now meaningful&lt;/span&gt;
      &lt;span class="nx"&gt;grid&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;index&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;                        &lt;span class="c1"&gt;// undo — this is backtracking&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;                                &lt;span class="c1"&gt;// dead end: honest failure&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

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

&lt;p&gt;&lt;strong&gt;The lessons, generalized:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Failure must be a value, not an implication.&lt;/strong&gt; Return &lt;code&gt;null&lt;/code&gt;/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
(&lt;code&gt;0&lt;/code&gt;, &lt;code&gt;""&lt;/code&gt;, &lt;code&gt;null&lt;/code&gt;, &lt;code&gt;undefined&lt;/code&gt;, &lt;code&gt;NaN&lt;/code&gt; — &lt;em&gt;not&lt;/em&gt; &lt;code&gt;{}&lt;/code&gt; or &lt;code&gt;[]&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pure logic deserves its own tests.&lt;/strong&gt; The generator is a pure function
from &lt;code&gt;(date, difficulty)&lt;/code&gt; to puzzle — trivially testable. Anything
deterministic (seeding, scoring, rules) can be validated across thousands
of inputs in seconds. Do it on every PR.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A "solved" game needs an end-to-end test at least once.&lt;/strong&gt; 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 &lt;em&gt;production bundle&lt;/em&gt; against a mock server and played a
complete winning Solitaire game — 170 taps, one scoring call) before
calling a game shipped.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Watch the silent metric.&lt;/strong&gt; Empty leaderboards were screaming. Metrics
you don't look at can't tell you the product is broken.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Cache invalidation is part of the fix, not an afterthought.&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;
  
  
  12. Publishing and operating
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Publishing&lt;/strong&gt; (full details in &lt;a href="https://gitlab.com/playquick-group/PlayQuick-project/-/blob/main/docs/publishing.md" rel="noopener noreferrer"&gt;publishing.md&lt;/a&gt;):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cd &lt;/span&gt;games/my-game
&lt;span class="nv"&gt;DEVVIT_ALLOW_SOURCE_UPLOAD&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;1 npx devvit publish
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

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

&lt;p&gt;&lt;strong&gt;Live operations, the daily loop:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;devvit logs r/&amp;lt;community&amp;gt;&lt;/code&gt; while interacting with the live post — your
server's &lt;code&gt;console.error&lt;/code&gt; is your only window when a player says "it's
broken."&lt;/li&gt;
&lt;li&gt;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).&lt;/li&gt;
&lt;li&gt;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.&lt;/li&gt;
&lt;li&gt;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).&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  13. Platform first, polish second, automation third
&lt;/h2&gt;

&lt;p&gt;The arc of this repo is the arc we recommend:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Fundamentals on a platform (week one).&lt;/strong&gt; 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.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The pattern that compounds (weeks two–three).&lt;/strong&gt; Add the daily loop:
deterministic content, time-boxed runs, leaderboards, streaks, shares.
This is one idea applied consistently, and it converts installs into
habits.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;UI polish where the data says (ongoing).&lt;/strong&gt; 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.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Automation as a product feature (the fun part).&lt;/strong&gt; Once the fundamentals
are solid, &lt;em&gt;the app itself becomes staff&lt;/em&gt;: 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.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Then the business layer.&lt;/strong&gt; 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.&lt;/li&gt;
&lt;/ol&gt;

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




&lt;h2&gt;
  
  
  14. Glossary — every term we used
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Git &amp;amp; shipping&lt;/strong&gt;&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;TypeScript &amp;amp; the web&lt;/strong&gt;&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;Data &amp;amp; correctness&lt;/strong&gt;&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;Design&lt;/strong&gt;&lt;/p&gt;

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

&lt;h2&gt;
  
  
  15. Appendix — repo map and further reading
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;This repo, as a curriculum:&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;To learn&lt;/th&gt;
&lt;th&gt;Read&lt;/th&gt;
&lt;th&gt;Then build&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;The minimal loop&lt;/td&gt;
&lt;td&gt;
&lt;a href="https://gitlab.com/playquick-group/PlayQuick-project/-/blob/main/docs/../games/play-highlow/" rel="noopener noreferrer"&gt;&lt;code&gt;games/play-highlow&lt;/code&gt;&lt;/a&gt; — splash → endpoint → sorted-set leaderboard&lt;/td&gt;
&lt;td&gt;Rebuild it from the bare template&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Server-side rules &amp;amp; counters&lt;/td&gt;
&lt;td&gt;
&lt;a href="https://gitlab.com/playquick-group/PlayQuick-project/-/blob/main/docs/../games/play-rps/" rel="noopener noreferrer"&gt;&lt;code&gt;games/play-rps&lt;/code&gt;&lt;/a&gt; — hashes, validation, global stats&lt;/td&gt;
&lt;td&gt;Best-of-5 matches&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Client game-feel&lt;/td&gt;
&lt;td&gt;
&lt;a href="https://gitlab.com/playquick-group/PlayQuick-project/-/blob/main/docs/../games/play-reflex/" rel="noopener noreferrer"&gt;&lt;code&gt;games/play-reflex&lt;/code&gt;&lt;/a&gt; — timers, states, sanity checks&lt;/td&gt;
&lt;td&gt;Sound + per-round storage&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;The daily pattern&lt;/td&gt;
&lt;td&gt;
&lt;a href="https://gitlab.com/playquick-group/PlayQuick-project/-/blob/main/docs/../games/play-sudoku/" rel="noopener noreferrer"&gt;&lt;code&gt;games/play-sudoku&lt;/code&gt;&lt;/a&gt; + &lt;a href="https://gitlab.com/playquick-group/PlayQuick-project/-/blob/main/docs/daily-challenge-guide.md" rel="noopener noreferrer"&gt;daily-challenge-guide.md&lt;/a&gt;
&lt;/td&gt;
&lt;td&gt;Port the daily loop to your own game&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sharing &amp;amp; engagement&lt;/td&gt;
&lt;td&gt;
&lt;a href="https://gitlab.com/playquick-group/PlayQuick-project/-/blob/main/docs/../games/play-solitaire/" rel="noopener noreferrer"&gt;&lt;code&gt;games/play-solitaire&lt;/code&gt;&lt;/a&gt; — share, one-tap comments, auto-finish&lt;/td&gt;
&lt;td&gt;Your own recap bot&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Ops &amp;amp; lessons&lt;/td&gt;
&lt;td&gt;
&lt;a href="https://gitlab.com/playquick-group/PlayQuick-project/-/blob/main/docs/publishing.md" rel="noopener noreferrer"&gt;publishing.md&lt;/a&gt;, &lt;a href="https://gitlab.com/playquick-group/PlayQuick-project/-/blob/main/docs/reach-playbook.md" rel="noopener noreferrer"&gt;reach-playbook.md&lt;/a&gt;, &lt;a href="https://gitlab.com/playquick-group/PlayQuick-project/-/blob/main/docs/launch-playbook.md" rel="noopener noreferrer"&gt;launch-playbook.md&lt;/a&gt;
&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Official docs:&lt;/strong&gt; the &lt;a href="https://developers.reddit.com/docs" rel="noopener noreferrer"&gt;Devvit documentation&lt;/a&gt;&lt;br&gt;
(platform, permissions, scheduler, Redis API), the&lt;br&gt;
&lt;a href="https://github.com/reddit/devvit-template-bare" rel="noopener noreferrer"&gt;devvit-template-bare&lt;/a&gt;&lt;br&gt;
starting point, and the developer portal's App Analytics.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The ArenaTap study&lt;/strong&gt; (&lt;a href="https://gitlab.com/playquick-group/PlayQuick-project/-/blob/main/docs/daily-challenge-guide.md" rel="noopener noreferrer"&gt;daily-challenge-guide.md&lt;/a&gt;)&lt;br&gt;
is the reverse-engineering exercise this whole playbook descends from — worth&lt;br&gt;
reading to see how much of a product you can infer from its public surface&lt;br&gt;
before writing a line of code.&lt;/p&gt;




&lt;h3&gt;
  
  
  Version history of this guide
&lt;/h3&gt;

&lt;p&gt;Saved as commits in this repo — every edit is one &lt;code&gt;git log&lt;/code&gt; away.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Version&lt;/th&gt;
&lt;th&gt;Date&lt;/th&gt;
&lt;th&gt;What changed&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;v0.1&lt;/td&gt;
&lt;td&gt;2026-09-12&lt;/td&gt;
&lt;td&gt;Part I: why Reddit, the stack, setup, first app, architecture diagrams&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;v0.2&lt;/td&gt;
&lt;td&gt;2026-09-12&lt;/td&gt;
&lt;td&gt;Part II: daily playbook — determinism, Redis modeling, UI/UX, engagement loops, reach&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;v1.0&lt;/td&gt;
&lt;td&gt;2026-09-12&lt;/td&gt;
&lt;td&gt;Part III: Git/CI pipeline, the fillGrid case study, publishing &amp;amp; ops, glossary; GIFs; first complete release&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;em&gt;Written by the developer of &lt;a href="https://www.reddit.com/r/PlayQuickGames/" rel="noopener noreferrer"&gt;r/PlayQuickGames&lt;/a&gt;&lt;br&gt;
with Claude. The games are live; the code is open; go build one.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>gamedev</category>
      <category>tutorial</category>
    </item>
  </channel>
</rss>
