One commit, 2,484 lines, and a runbook that quietly predicted a future disaster
Rooms, threads, votes, moderation and bans, shipped on a Friday afternoon. The deployment notes contain a sentence that took three months to become a problem.
Closers Coffee is the forum in this product. On 2026-06-12 at 15:11, on a Friday afternoon, it acquired sub-communities in one commit of 2,484 lines.
Rooms with their own URLs, that any user can create, rate-limited to three a day with slug validation and reserved words blocked. Thread pages with comments, editing and deleting your own content, and redirects when a room's address changes. Upvoting on posts and comments, with an atomic toggle so double-clicking cannot double-count, a denormalised score column so sorting does not have to count votes every time, and hot, new and top orderings. Joining and leaving, with member counts and a feed that can show everything or only your rooms. Moderation: pinning, a reversible soft-remove, community-level bans, and a moderation queue scoped to the rooms you actually moderate.
Every existing post in the product was backfilled into a seed community called The Floor.
One commit. Friday afternoon. Which, in a project whose commit histogram peaks in the evening and had not recorded a single commit before eleven in the morning during its entire first hundred days, makes this one of the few features in the codebase built in daylight.
The parts that show somebody had run a forum before
The design decisions here are unusually mature for a first version, and the tells are in what got included rather than what got left out.
Soft-remove being reversible is the big one. A moderation system where removal is destructive is a system where every moderator action is a small act of courage, and the predictable result is that moderators stop acting. Making removal reversible converts moderation from a judgement into a triage step.
Author experience points paying only once per voter per item is the other. Without a rule like it, a forum's scoring system is a machine for generating collusion. The version that shipped means a second upvote on the same post from the same person is still a vote, still moves the score, and simply stops paying. That is the correct shape, because the alternative, refusing the vote outright, would make the score wrong in order to protect the points.
It is also only half the defence, and the runbook says so: a daily cap on that award is on the known-follow-ups list, because per-item is not per-voter, and two people with a lot of posts between them can still farm it. Shipping the partial version with the gap written down beats shipping neither.
Community-level bans rather than global ones means a room can eject someone without the platform having to decide they are a bad person. That is the difference between a moderation system and a justice system, and small platforms get into enormous trouble by building the second when they needed the first.
None of this is novel. All of it is the accumulated knowledge of everyone who has ever run an online community, and the value is in having read that lesson before shipping rather than after.
The sentence in the runbook
The commit shipped with a deployment runbook, which is good practice, and the runbook contains this, in a section explaining the migration strategy: everything runs inside the application's startup routine, with no separate migration script. All the schema statements are conditional, the seed insert ignores conflicts, and so it is safe to re-run and safe on a half-deployed state.
Every clause of that is true. It is also, in retrospect, a description of the exact mechanism that nearly destroyed our users' gamification progress two months later.
The reasoning behind it is sound and I would probably have written the same thing. Making the schema changes idempotent means a partially failed deploy is not a catastrophe, because running it again converges. If your options are "a migration tool nobody has set up yet" and "conditional statements at startup," the second one ships a forum on a Friday and the first one ships nothing.
What that reasoning misses is one distinction: idempotent and destructive are not opposites. CREATE TABLE IF NOT EXISTS is idempotent and safe. DROP TABLE IF EXISTS is idempotent and permanent. Both re-run cleanly forever. Only one of them can take something away from a customer, and startup routines eventually accumulate both kinds because they are the only place anybody has to put database work.
By August that routine held six destructive operations, one of which sat exactly on its own trigger, waiting for a single row to change before wiping every user's progress. Retiring them is its own entry in this log. This runbook is where the pattern was written down as house practice, in a document that was careful, well-reasoned, and correct about everything except how long "startup" lasts.
There is a line further down the same runbook that made me laugh: deployment step five is to delete the test database branch, annotated "compute hygiene." Three weeks earlier we had discovered that four background workers were polling often enough to keep serverless Postgres awake permanently. The lesson took, and it took at the level of a checklist item in an unrelated feature's deployment notes, which is where lessons actually live.
The follow-up list is honest too. Room addresses are immutable in version one, which is an interesting thing to commit to in a codebase that renamed its leaderboard four times in twenty-five days. Rate limits live in memory and reset when the server restarts, which is noted as fine at current scale, which is the correct way to write down a known limitation: with the condition under which it stops being fine.
Fifty-two end-to-end checks, run against a real database branch, and then a Friday afternoon deploy. There are worse ways to ship a forum.
— DealArena Team