Everything that has shipped on Classic Games Hub.
The complete history, newest first: every release line, every release inside it, and every individual change that has landed in production. For what's coming next, see the roadmap.
33
Releases shipped
246
Features delivered
64
Changes in production
22
Pull requests merged
33 releases across 6 lines. Open a line to see its releases; open a release to see what it brought and which changes it was made of. A release is a unit of work rather than a unit of time - each one says why it was drawn where it was.
Things worth keeping, and the machinery around them: seasons and collectable sets, cosmetics that layer three deep, two new games - a Discord server that now finishes its own setup, keeps itself in step with the site, and writes down everything that happens in it - and a pass over the rough edges underneath all of it.
Accounts can now ask for a code from an authenticator app as well as a password. The second factor is Supabase's own TOTP MFA, so nothing here invents cryptography; what the site adds is the parts Supabase leaves out - recovery codes, a login challenge that a half-authenticated session cannot walk around, and somewhere to turn it on.
Why this is one release: One release for one feature. Two-factor authentication is not big, but it is the sort of thing that has to be complete or not shipped at all - a second factor with no way back in is a feature that loses accounts - so the recovery codes, the login gate and the settings panel are the same release rather than a promise to follow up.
Settings → Security → Set up two-factor. Scan the QR with any TOTP app, enter one code, done.
Enrol, challenge and verify are the auth API's, and the `aal2` claim it writes into the access token is what the site trusts - so the question 'has this session cleared its second factor?' has one answer, signed, rather than a flag in a table that could disagree with it. The QR is inlined as a data URL, so the setup dialog fetches nothing and adds no dependency - after a false start where it rendered as a broken image, because `totp.qr_code` is sometimes already a data URL and sometimes raw markup, and Supabase's type and its own example disagree about which.
A factor stays unverified until a code from the app is accepted, and an unverified factor never makes a login ask for anything. Opening the dialog and wandering off is therefore free, and closing it cleans the attempt up rather than leaving it half-made.
Disabling asks for the authenticator again, so a session left open on a shared computer cannot quietly remove the protection it is sitting behind.
Ten single-use codes, shown once, stored only as hashes.
A second factor with no escape hatch is an account nobody can reach, including whoever runs the site. Ten `XXXXX-XXXXX` codes are issued the moment 2FA goes on, from an alphabet with no I, L, O, U, 0 or 1 - these get copied by hand off a screenshot, and ambiguity is a real failure mode.
A recovery code cannot raise a session to `aal2` - only the authenticator can. So it does the other thing that unblocks the account: it spends the code, removes the factor, and hands you back a signed-in session with two-factor off and instructions to set it up again. Honest about what just happened, and it leaves no second door standing.
sha-256, reachable only through four definer functions with no row-level policy behind them - not even the owner of a code can read its hash. A code exists in plaintext exactly once: in the response that shows it to you.
The interesting half. A password login produces an authenticated session that has to be treated as worthless.
`/two-factor`, the legal pages and `/status`. Everything else redirects back to the challenge, because a session at `aal1` on an account that owes `aal2` is authenticated in every way that matters to `getUser()` and should be trusted with nothing. Sign out still works - the form posts to the one page the gate allows.
The check compares the `aal` claim in the token the proxy is already holding against the factors listed on the session. This runs on every request on the site, so a version of it that asked the auth server would have been a tax on every page load.
An audit of what Supabase, Vercel, GitHub and Cloudflare were actually doing for the site, and the gaps it turned up. CI that had never run once, a scheduler living in somebody's free third-party account, thirty-three unindexed foreign keys, and every shared link on the site rendering the same picture. Nothing here changes how the arcade plays; it changes how much of it is checked, watched and shareable.
Why this is one release: One release because it is one question answered: are the four platforms this site runs on actually being used, or merely paid for. Everything here came out of that audit, and none of it is a feature - it is the scaffolding that should have been there already. Splitting it into five patches would file the same afternoon's work under five headings nobody would search for.
The workflow file existed. It was in `.github/`, which is not `.github/workflows/`, so GitHub had never read it.
The Definition of Done has asked for all three since it was written, and until now every one of them was a manual promise made by whoever remembered. The file moved one directory and started working. The bot is a second job, because it is a separate package with its own dependencies and its own typecheck that nothing had ever run.
Both npm packages and the workflows themselves, weekly, batched into a handful of pull requests instead of one per Radix package. Security updates are their own group so they are never buried in a routine bump.
Weekly matters more than it sounds: it means a rule published next month finds today's code, rather than waiting for someone to happen to edit the file it applies to.
This file is the most structured record in the project and GitHub had zero releases and zero tags. A workflow now reads it and cuts a tagged release for every version that does not have one, so a version number in the changelog resolves to a diff you can click. It refuses to run against a shallow clone, because that would tag all thirty-one releases at HEAD.
`docs/cron-jobs.md` listed Supabase cron as the option that 'needs no third party', and then recommended a third party.
The status probe, role sync, counter channels and the audit poller now fire from Postgres at the cadences they actually want, instead of from a free account on an external scheduler with no alerting. The objection was never that the scheduler was unreliable - it is that `status_record_checks` counts a failed check as five minutes of downtime, so a probe that quietly stops does not leave a gap in the graph, it leaves a wrong one.
`CRON_SECRET` is not in the migration and never will be. Until it is set, every tick is skipped with a warning naming the fix, rather than firing unauthenticated at a route that would return 401 four times a minute forever. Rotating it is one update with no redeploy.
Each job's schedule, whether it is active, when it last ran and how that run ended - plus whether the token is set, which is the first thing to check when everything is firing and nothing is happening. A schedule you cannot inspect is exactly what this replaced.
Every profile, every game and the site itself rendered one generic card - and the card had a decorative row that rendered as nothing at all.
Paste a profile into Discord and you get the avatar, the name, the level, how far into it they are, four numbers and what they are best at. Profiles are what people actually send each other, so this is the share surface that was wasting the most posts.
The thumbnail in a frame that fits, next to the title, the tagline, the category, the play count and the rating. Previously the raw square thumbnail was handed to each platform to crop however it liked, with no title on it anywhere.
The site card carried four emoji across the top. `next/og` ships no emoji font, so all four rendered as nothing and the card had a band of empty space where the decoration was meant to be. Removed - which is what it already looked like. The same card also still advertised 23 games; there are 26.
Remote images are fetched with a three-second budget and inlined, so a deleted avatar or a slow host produces a plain card instead of a broken one. `next/og` fetching it directly would throw and take the whole image down.
Postgres does not index the referencing side for you. Without one, every join across that key is a sequential scan and - the part that bites later - deleting a single shop item has to scan the whole of `inventory_items`, `gift_tokens` and `wishlist_items` to check the constraint.
A bare `auth.uid()` in a policy is treated as volatile and evaluated once per candidate row; wrapped as `(select auth.uid())` it becomes an InitPlan evaluated once per query. Same rows out, and the gap widens with every row the table gains. It matters most on the two party policies, where it was scaling with party size.
The static security headers have been right for a while; the one that actually contains an XSS was missing, on a site that renders usernames, bios, chat messages and third-party GIFs. Shipped report-only on purpose - a policy written from reading the code is a hypothesis, and the browser is the thing that knows. Promoting it to enforcing is renaming one header.
Pageviews can say someone opened Snake. They cannot say how many runs get finished, what the shop converts at, or which difficulty people give up on. Five events, scalars only, nothing identifying, behind the same consent gate as everything else.
The logging feature shipped in v1.5.9 lives in the gateway worker, and the worker needs an always-on host this deployment does not have. Discord's audit log is pollable over plain REST, so there is now a second way to run the logs: a cron route, any free scheduler, no hosted process - covering every structural change in the server, and honest about the four things it can never see.
Why this is one release: Its own release rather than a patch onto v1.5.9, because it is a different answer to the same question. v1.5.9 built the logs; this one is about the fact that they could not be switched on, and the shape of what runs without a host is a design decision worth being able to find on its own.
A feature that cannot be switched on is worth exactly as much as one that was never built.
It needs no persistent connection, which is the entire problem with the gateway worker, and it already contains every structural change: channels created, renamed, moved, re-permissioned and deleted; roles created, recoloured, re-permissioned and deleted; members kicked, banned, unbanned, timed out, renamed and given roles; invites, webhooks, emoji, stickers, threads, server settings, and messages deleted by a moderator. Point any free scheduler at one route every five minutes - the same one already running the status probe.
A role that gained Manage Server says so. Printing `1071698660929 → 1071698660961` either side of an arrow is technically the same information and answers nobody's question, which is the difference between a log and a diff of a log.
Message content, message edits, self-deleted messages, joins, leaves and voice are absent from Discord's audit log and always will be - so they are absent here, permanently, rather than being a gap that a later version quietly closes. The docs carry a comparison table rather than a claim of parity, because discovering the limit while looking for a specific deleted message is the worst possible time to find out.
The poller records where it has read to, so it never repeats an entry or skips one. Its first run records the position and posts nothing at all: switching a log on should not begin by dumping a hundred historical entries into the channel. It also ignores the bot's own actions, which are already reported by whatever performed them.
The worker and the poller write the same structural entries to the same channels, so running both duplicates every line. It is in the docs and in the failure-modes list, because 'every entry appears twice' is otherwise a genuinely confusing thing to debug.
The bot now writes down everything that happens in the server: messages deleted and edited, channels and roles created, renamed, moved and re-permissioned, members joining, leaving, being kicked, banned, timed out or renamed, emoji, invites, webhooks and voice. Each entry names who did it. Alongside it, the audit that produced it: a real privilege-escalation hole closed, the worker scoped to one server, and six bugs that were each quietly costing something.
Why this is one release: One release, because it is one audit of one thing. The logging feature is the headline, but it was written after reading the bot end to end, and what that reading turned up - a cross-guild hole in the interactions endpoint, a worker that ran on every server it was in, two maps that only ever grew - belongs with it rather than in a separate 'and also some fixes' release nobody would connect to it.
The last thing Sapphire still did better, and the one part that genuinely needs the gateway worker - none of these events are ever sent to an HTTP endpoint.
Messages deleted, edited and bulk-purged; channels created, renamed, moved, re-permissioned and deleted; roles created, recoloured, re-permissioned and deleted; members joining, leaving, kicked, renamed, given and stripped of roles; bans, unbans, timeouts; emoji, stickers, threads, invites, webhooks, voice movement and the server's own settings. Five categories, each routable to its own channel and all falling back to one catch-all, so the simple setup is a single channel and the loud category can be moved out later without touching anything else.
Gateway events carry no actor at all - a deleted message and a recoloured role both arrive anonymous - so each one is matched against Discord's own audit log, live where the entry arrives in time and by a fetch where it doesn't. It needs the View Audit Log permission, and when that is missing the bot says so once at startup rather than silently writing 'Unknown actor' forever and leaving somebody to work out why.
A role change reads `Colour #5865f2 → #ff0000`. A permission change lists the permissions that moved, not two bitfields that technically contain the same information and answer nobody's question. A channel update names the overwrite that changed and whose it was. 'Role updated' is a notification; this is a log.
The way a log dies is a single channel carrying four hundred message edits a day next to the one role change somebody needed to find. So: every event switchable on its own, ignore lists for channels, roles and users, an ignore-bots switch, and content quoting that can be turned off outright because writing down what people said is a decision worth making deliberately. A channel deletion is logged even when that channel is ignored - hiding that is the one thing an ignore list must never do.
Entries queue per channel and go out ten to a message on a fixed tick, which is what stops a raid or a 100-message purge rate-limiting the log into uselessness at the exact moment it matters. The queue is bounded and drops rather than growing: an out-of-memory worker loses chat XP, automod, the counters and the online dot too.
Every command handler acts on `DISCORD_GUILD_ID` - this server - while the permission check only ever read the permissions Discord sent for the guild the command was *used* in. So anybody who could add the bot to a server they administered could run `/ban` there and have it land here, with their own server's permissions as the only gate. Adding a bot needs Manage Server in the server you're adding it to and nothing else, so 'we only invited it to one server' was never the control it appeared to be. Interactions from any other guild are now refused before they reach a handler.
The dashboard has refused both since it was written. The slash commands sent them to Discord and returned a bare 'couldn't ban that member' - an error that reads like a permissions problem and reliably sends people off to check role hierarchy for a fault that was never there.
Six things that were each costing something quietly.
Chat XP, automod and the welcome message were never scoped to the configured guild, so anyone who added the bot elsewhere got Hub XP awarded against their server's chat. Every listener is now scoped, and the worker says so at startup if it isn't in the guild it was configured for.
The XP cooldown map kept one entry per person who has ever spoken, for the lifetime of a process meant to run for months. Automod's flood map 'bounded' itself by wiping everyone's history the moment it passed 5000 entries - including, necessarily, the person mid-flood the rule exists to catch. Both now age entries out on a timer, which bounds them without ever helping a spammer.
Which produced the worst outcome available: a process that passes its health check and has stopped doing its job, so the host never restarts it and nothing looks wrong. It now exits, and the host brings up a clean one.
A failed config read wasn't cached, so the RPC was retried on every single message until Supabase came back - turning a momentary blip into sustained load at the worst possible time. The answer was never going to change inside the same second anyway.
The worker never listened for member updates, and its role sync had drifted from the website's - no `__booster__` key, no call to stamp the boost onto the profile - so a new booster paid for a month and waited for the nightly reconcile before anything happened. The two implementations now do the same thing, which is the actual fix: drift between them is how someone ends up with different roles depending on which one happened to run.
Posting something innocuous and editing an invite into it a second later walked past every rule, because a message was only ever checked once.
The last 3800 characters were taken and then the first 1900 of *those* were posted - so a long ticket lost the question at the start and the resolution at the end, and kept the part in between. Transcripts are chunked now, and when they still don't fit it is the end that survives, because the end of a support ticket is where the answer is.
The fallback for an unresolved target was the invoker's name, so a lookup that failed produced a confident, wrong answer instead of an obviously incomplete one.
It read `npm_package_version`, which only exists when npm started the process - and the Docker image, which is what everyone actually deploys, runs `node dist/index.js` directly. `BOT_VERSION` now covers it.
The plan stopped awarding itself a major version. The relaunch, Head to Head and Player Made were v2.0.0, v2.1.0 and v2.2.0 for work that has not started; they are now v1.7.0, v1.8.0 and v1.9.0, and 2.0.0 is left deliberately unclaimed. Two pieces of Discord setup guidance that were actively wrong were corrected at the same time.
Why this is one release: A documentation-only release, drawn on its own because renumbering the whole forward plan is the kind of change that has to be findable later. Nothing shipped to the site; what changed is what the roadmap claims and what the Discord setup instructions tell you to do.
The relaunch, Head to Head and Player Made carried v2.0.0, v2.1.0 and v2.2.0 - a major version awarded to work nobody had started. They became v1.7.0, v1.8.0 and v1.9.0, so the rebuild at v1.6.0-v1.6.3 and the three releases behind it read as one sequence rather than as a plan that crosses a boundary for no stated reason.
A planned number is not a reservation, so nothing inherits it. Whatever eventually deserves to be called 2.0.0 will take it because there is a reason to, not because a sketch written months earlier said so. The relaunch's summary used to justify its own number; it now says why it isn't 2.0.0, which is the more useful sentence.
It told you to apply `0033` and `0041`. The schema was at `0071`, and publishing needs `0063` and `0068` - so following the instructions literally left a database that could not run the bot. It now says to apply everything in order and check `status_meta.schema` on /status, which is the check that would have caught it.
Env vars set between 3 and 13 August never reached a running build, because the redeploy they need could not happen while `vercel.json` was refusing every deployment. Written down, because the symptom - a variable that is present in the dashboard and absent at runtime - reads as a wrong value and sends you looking in the wrong place.
Nothing had deployed since 3 August - not because builds were failing, but because none were ever started. Vercel's Hobby plan rejects a sub-daily cron expression when the deployment is created, producing no build, no error and nothing in the dashboard. `vercel.json` is back to two daily entries and the jobs that need a tighter cadence moved to an external scheduler.
Why this is one release: One release for one fault, even though the fix is a four-line file. The size of a change and the size of what it unblocked are different measurements, and ten days in which every push silently vanished is worth its own entry in the history.
The application code was fine throughout - typecheck, lint and build all passed on the stuck commit.
Vercel's Hobby plan caps cron jobs at two, each running at most once a day, and it rejects a sub-daily expression at the moment the deployment is *created*. So there is nothing to look at: no failed build, no error in the dashboard, no clue in the repository. Every push simply vanished, which is exactly why reconnecting the git integration - the obvious first move - changed nothing at all.
`de6cfc9` moved role sync from `30 4 * * *` to `*/2 * * * *`. `c0d2064` then added a `*/15` job and v1.5.6 a `*/5` one, so by the time anyone counted there were five crons, four of them sub-daily - and each of those commits was itself invisible, having never deployed.
`vercel.json` carries `discord-publish` at 05:00 and `booster-drops` at 06:00, both of which a daily run genuinely serves. The status probe deliberately did not stay: `status_record_checks` counts a failed check as five minutes of downtime, so a daily probe would not merely be coarse, it would report wrong uptime percentages on the page whose entire job is being right about that.
Which job wants which cadence, which two are on Vercel and why, how to point an external scheduler at the rest, and what to move back if the plan ever changes. Repeated in CLAUDE.md because the failure is silent and the first instinct is wrong.
/status stopped being a list of counters and became a status page: ten services with ninety days of uptime each, incidents with timelines, a probe that opens and closes them by itself, and a Downdetector-style report button so players can say something is broken before our own checks notice. All of it is a public, keyless API, which is what /status in Discord now reads.
Why this is one release: One release, because a status page and the API behind it are the same piece of work seen from two sides: the page is the API's first consumer, the Discord command is its second, and building either without the other would have meant writing the definition of 'is the shop up' twice and watching the two drift.
The old page counted players online and plays today. Interesting, and no help at all with 'is the arcade broken'.
One bar per day per service, which is the shape you read at a glance: a single red notch in a field of green is visible from across a room in a way '99.87% uptime' never is. A day with no checks is drawn as no data and left out of the percentage - a day before we were watching did not have perfect uptime, and rounding it up would be the one lie a status page cannot tell.
Investigating, identified, monitoring, resolved - each update timestamped and attributed, newest first. A page showing only the current state makes people refresh it; a page showing 'identified twenty minutes ago, monitoring since ten' tells them whether to keep waiting. Scheduled maintenance gets its own section and its own words, because planned work is not an outage.
Players online, plays today, credits earned - kept, and moved below the answer. They are a different question, and a page that led with them was answering the one nobody asked.
The statuses are Statuspage's - operational, degraded performance, partial outage, major outage - as is the none/minor/major/critical indicator on top. Inventing our own would have cost nothing here and everything at the edges, because the API this feeds is meant to be read by tools already written against a status page. It also settles a hundred small naming arguments by deferring to prior art.
Downdetector's idea: one tap, counted in aggregate, compared against the site's own normal.
Two taps - what is going wrong, and where - and no sign-in, because the person best placed to tell us sign-in is broken is the person who cannot sign in. The problems are phrased as symptoms rather than causes, since nobody reporting one can know the cause and guessing at it makes the total useless.
A game that renders a blank canvas returns HTTP 200 all day long, and every probe we have will call it healthy. Forty people saying otherwise in a quarter of an hour is the only signal that catches it - which is exactly why it is worth the machinery underneath.
Reports are bucketed by quarter hour against a rolling baseline, and the alert needs both to fire. A multiple alone makes a quiet site hysterical - two reports against a baseline of 0.3 is a sixfold spike and means nothing - and a floor alone makes a busy site deaf. The baseline also excludes the last hour, so an outage in progress cannot raise the bar it is being measured against.
The page shows totals and a percentage breakdown of what is being reported. The free-text notes people leave are staff-only and are read in the admin console, because they are evidence for whoever is on call rather than page content.
The whole value of a report count is that it is hard to poison, so the submit function is service-role only rather than reachable with the key that ships in the browser; the fingerprint that rate-limits it is derived from the request rather than chosen by the sender; and both the per-service cooldown and the hourly cap are enforced in the database. No address is stored - the fingerprint is a daily hash, so it cannot be matched across days into a way of following anyone around.
Every five minutes: the site's own front page, Supabase auth, Discord's gateway, and one self-check inside the database that times a representative read per area and reads the bot's heartbeat on the way past. Adding a service to the page does not add a round trip to the probe.
One failed check is a network blip far more often than an outage, and an incident opened for every blip trains everyone to ignore the page. Two in a row at this cadence means the fault has survived five minutes. A partial unique index guarantees at most one open automatic incident per service, so a flapping probe cannot bury the page in duplicates.
Probes write what they measured; an open incident carries its own claim about each service it names, because a human saying 'leaderboards are degraded' must be able to say so while the probe is still happily getting a 200; and a staff pin beats both. The pin is the only one that can make the board look better than the evidence, which is why it records who set it and why, in public.
Each check folds into a daily rollup, so ninety bars for ten services is a ninety-row scan rather than an aggregate over a quarter of a million samples - and it is what lets the raw samples be thrown away after a fortnight without losing the history.
Summary, components, one component, incidents, uptime and reports, all as JSON with CORS open and a short shared cache so polling it does not become load. Add format=statuspage to the summary and it comes back in the shape existing status widgets and uptime tools already understand.
An SVG badge in shields.io's proportions, because a badge that does not sit level with the row of badges already in a README is worse than no badge.
When the database cannot be read the API says so and the page says so. Answering 'no incidents' because nothing could be fetched is precisely the failure that makes a status page worthless, and it is the one thing every endpoint here is written to avoid.
Public rather than ephemeral, because 'is the site down' is a question a whole channel is usually asking at once. The service option autocompletes from the live component list, so a service added to the page appears in Discord with no code change and no re-registration, and incidents, reports and versions are offered alongside the services so nobody has to know they exist.
The release, the deployed commit, the database schema and the bot worker's own version. Migrations are applied to Supabase separately from deploys, so the schema and the app drift apart routinely and used to do it silently - the page now compares what the database reports against what the build expects and says when they differ.
Declare it, post updates, close it, pin a service, and read what players actually wrote - all in one place, because whoever is using it is using it while something is on fire and a flow spread across three pages is a flow nobody finishes.
There is no separate close button, so a resolved incident can never have a timeline that stops mid-sentence. Resolving also releases the incident's claim on the services it named, so the board goes back to whatever the checks say without anyone having to remember to unpick it.
Every game on the site is now marked in development - still listed, still holding its leaderboards, but playable only by staff while it is rebuilt - and the roadmap now says exactly what is wrong with each of the twenty-six and what it becomes.
Why this is one release: One release, because the two halves only make sense together: taking every game offline to normal players is indefensible without publishing what is being done to them, and publishing a rebuild plan for twenty-six games while they all still sit there marked Published would be a plan nobody believed.
A new game status meaning shipped, and being rebuilt. Not the same as coming soon, which means never released.
They stay listed, badged and clickable, and keep every leaderboard, rating, favourite and play count they had - nothing is reset. Only admins and moderators may record a play, so each overhaul gets played on the real site, on a real phone, before it opens to everyone. A game comes back by being set to Published from Admin → Games, one at a time, as each rebuild finishes.
The trigger that already enforced booster early access now enforces both rules, so no non-staff account can record a score for an in-development game by any route. The two compose deliberately: boosting does not get you in, because early access is a head start on a release and this is not a release.
'published' was not a label, it was a literal that the row-level security policy, submit_score, set_party_game, platform_status and the command palette all tested for. Missing any one of them would not have degraded the arcade, it would have emptied it - the security policy alone would have hidden all twenty-six games from everyone who is not staff.
Not a repair list. Every game gets a new look, animation on essentially everything, a mobile pass, and a stage that fills your screen.
v1.6.0 'Ground Up' builds the platform and reopens nothing. Three waves then rebuild and reopen the games a category at a time, and inside them every game is its own section carrying the same four items - the look, the animation, the feel and mobile, and what it does with the whole screen. All twenty-six have all four, and the repetition is deliberate: a game receiving less than the others is visible at a glance. The relaunch closes the series, and Head to Head moves behind all of it.
A stage that fills your whole screen rather than a small box on a page - and rather than today's fullscreen, which deliberately letterboxes the canvas, so Snake uses about 56% of a laptop screen and Tetris about 35%. An effects library, so animation stops being something eight games happen to have and fourteen do not. Skins and arenas for every game, free, earned, bought or boosted. A mixer, so the sound slider does something. And input that does not throw your presses away.
Four of the twenty-six engines respond to the difficulty picker. Three honour reduced motion. Six have a pause button that does not pause. Eighteen freeze their layout the moment they start, so they cannot be resized at all. Twenty-one hardcode their text sizes in pixels. And the sound slider in settings saves what you set and is read by nothing.
Neon Runner's touch button only ever sends a jump, so ducking under its overhead obstacles is impossible on a phone. Frogger has no lily pads to fill. Space Invaders' own description promises barricades the game does not have. Connect Four advertises keyboard controls it never implemented. Gem Cascade's timer keeps draining behind the pause overlay. 2048 works out how far each tile should slide and then throws the number away, which is why nothing animates. And Snake has been reporting the length of the snake where the number of seconds played belongs, on every run ever recorded.
The games page claimed there were 23 games; there are 26. The guide for adding a game never mentioned the status column at all, and now documents all five values and who each one lets in.
Removed an unused feature-flag fetch from the game page, and changing a game's status now refreshes the home page too - so reopening a game puts it back on the featured rail immediately rather than at the next deploy.
Every change that has ever shipped now has a version number and a place in a tree, and the log no longer lives only on the website - releases and announcements are mirrored into Discord and kept there in step, rather than typed out twice and left to drift.
Why this is one release: One release: the update log gained its version tree and the Discord channels that mirror it, and neither half is much use without the other.
Twenty-four releases across six lines, instead of eight releases and a long list of loose commits.
All 60 changes in production are now assigned to a release. Previously eight releases covered part of the history and the rest - the March prototype, the rebuild, the whole run of Discord work between 26 July and 3 August - sat in a flat list with no version at all.
Three small fixes in one afternoon are one patch; a single pull request that redesigns the interface is a patch on its own. Each release says which changes it holds and why it was drawn there, so the grouping is something you can disagree with rather than something you have to take on trust.
Releases nest inside their series: open v1.4.0 to find v1.4.10 down to v1.4.0, each opening onto its own notes. The newest line and the newest release inside it are open when the page loads, so the thing you almost certainly came for needs no clicks.
The interface redesign shipped as v1.4.1 while the games and themes overhaul - eight days earlier - carried the same number. "Sanded Down" shipped as v1.5.1 while two runs of Discord work from earlier the same day carried none. Chronology wins in both cases, so they became v1.4.10 and v1.5.3 and each says so on its card. Nothing else moved.
The website is the source of truth; Discord is a mirror of it.
Every release posts as its own embed in an update-log channel, oldest first, so the channel reads in the order things happened. Re-syncing edits the message that is already there rather than posting a second copy - each one is fingerprinted, so a sync with nothing to say writes nothing at all.
Publishing an announcement on the website posts it to the Discord announcements channel in the same breath, with its call-to-action link and an optional role ping. Editing one edits the Discord message; unpublishing or deleting one removes it. It is a mirror, not a one-way fire-and-forget post.
Full setup now provisions the two channels as well: readable by everyone, writable by nobody, adopted by name if the server already has them. Both are still overridable with an id from Admin → Discord bot → Publishing.
A cron job re-syncs both every fifteen minutes, so a message deleted by hand comes back and a release added by a deploy appears without an admin remembering. It is idempotent by fingerprint, so the usual run costs one database read.
The rough edges. No new headline features - the naming that was wrong, the date that was never right, the settings page that never grew past a list of switches, and the consent story a site collecting anything at all is expected to have.
Why this is one release: Three commits carrying one intention - go round the site and fix what was merely wrong. Published as v1.5.1, but two runs of Discord work landed earlier the same day; numbers that run backwards in time are not worth reading, so this took the next free one and kept its old number in `formerly`.
Three things that were simply wrong, in rising order of how long they had been wrong.
Renamed in the game title, the party picker and the docs. The rest of the site is written in British English, so this was the odd one out from launch. The slug stays `tictactoe` - it is the join key for every score and play session and is baked into the party protocol, so renaming it would break all of that to change a string nobody sees.
A launch flag is only useful while it is news.
It reported the day the account was created, for everyone, since launch - and it was never a display bug. A Supabase query builder is a lazy thenable that only sends its request inside `then()`, and the presence heartbeat was written as fire-and-forget, so it was built and thrown away without ever reaching the network. Every profile in the database had `last_seen_at` exactly equal to `created_at`, to the microsecond. Now awaited, with failures logged rather than swallowed - the silence is what let it survive this long.
Default difficulty, sound and music as separate sliders, high contrast, time zone and date format. Volumes save when you let go of the slider rather than on every pixel of the drag, and every row explains what it actually changes - most of them used to restate their own label.
Nothing optional loads until you allow it - not loaded and silenced, genuinely not rendered. Reject carries the same weight as Allow, there is no dismiss-without-choosing, and the banner is not modal, because refusing must not cost you anything. Global Privacy Control counts as a refusal. The stored consent record carries the policy version, so a policy that widens what is collected asks again instead of carrying an old answer over. A new /legal/cookies page generates its tables from the same constants the banner uses, so it cannot describe cookies the site does not set.
Delete your own, and post more than one. The database always allowed both - the strip had a single button that chose between viewing and composing, so the composer became unreachable the moment you had a story.
A report action on any message, and the conversation around it in the admin queue - because a given message reads as a joke or as abuse depending entirely on what surrounds it. Scoped deliberately: the lookup resolves the message from the report row rather than taking a conversation id, so staff get what they need to judge one report and cannot page through an inbox with it.
It used to stretch a small canvas buffer across a large display, which is what made it look soft, and reserved a hardcoded 170 pixels for the controls whatever the screen was. The buffer now tracks the element's real size, and the layout measures its own leftover space.
On Frogger, Snake, Minesweeper and Hangman, starting from your saved default. Each is tuned on its own terms rather than by one global scalar - doubling speed makes Snake harder and Whack-a-Mole easier, so only the engine knows which way "harder" points. Rewards scale with the choice, so easy is a comfort setting rather than the best way to farm credits.
Each difficulty ranks on its own board, with tabs on the game page and the leaderboards page. Separate boards rather than one mixed list, because a ranking only means something between runs that faced the same game - sorted together, every easy run outranks every hard one and it stops being a leaderboard. Achievements, the podium badge, the friends feed and your "best game" all still read the regular board only: an easy run should not quietly unlock something written for a real one.
A way to see the whole Discord server at once - and the diagnostics that answer most 'the bot is broken' reports before anyone has to ask.
Why this is one release: One substantial change plus the roadmap edit that shipped beside it. The export is groundwork for reorganising the server, which is why it is its own release rather than part of the setup work before it.
Both produce the same JSON: every channel nested under its category in draw order, every role, every permission overwrite - with ids resolved to names and bitfields decoded, because `"deny": "1024"` on an id says nothing and `deny: ["ViewChannel"]` on @everyone says all of it. In Discord it arrives as a file attachment, since a modest server exports past the 2,000-character message limit and a truncated server map is worse than none.
The export reports what cannot be seen from inside Discord: the bot's own highest role and which roles sit above it, its effective permissions, every configured id that no longer resolves, and whether the gateway worker has ever checked in.
It was nightly, which cannot hold up a promise of the same roles and level as the website. Costs one database round trip when nobody is linked.
Full setup stopped two steps short of doing anything useful. It now provisions the channels its panels live in, and there is a way to clear a dashboard that has been pointed at the wrong server.
Why this is one release: Three commits about the same button, including the hotfix for the one before it - one release, because shipping the fix as its own version would suggest the feature was ever usable without it.
It created the verification and level roles and the counter channels, then reported both panels as skipped - so a fresh server finished setup holding verification roles with nothing handing them out. It now creates and posts into a verify channel, a support channel, a Tickets category and a Staff role, adopting any that already exist by name rather than duplicating them.
The point is the ids, not the toggles: a dashboard pointed at one server accumulates role, channel, category and panel-message ids, and a stale id is worse than an empty one because setup reads it as an instruction to use that exact channel. Deletes the rows rather than writing defaults into them, touches nothing inside Discord, and is audit-logged.
The first cut failed the first time the button was pressed - "DELETE requires a WHERE clause". The migration applied cleanly because DDL runs as `postgres`; the dashboard connects as `authenticator`, which preloads `safeupdate` and rejects an unqualified delete even inside a `SECURITY DEFINER` function. Qualified by the same key allowlist the other two config writers already enforce.
The panel steps re-read the config to find the channel the step before them had written. Depending on that write having landed turned one failed round trip into a panel that silently never posted - the exact failure the channel step was added to remove.
Things worth keeping. Seasons and collectable sets give the long game a shape, cosmetics now layer three deep and finally include the expressive extras that have been promised since v1.2.0, and the arcade gained two games - a playable Rubik's cube, and the first title rendered with the camera inside the scene.
Why this is one release: Two hundred and twenty-nine files in one pull request, plus the changelog regeneration that followed it. The largest release the site has had.
Two new games, and the renderer that makes a third one cheap.
The classic 3x3 twisty puzzle. Drag a sticker in the direction you want it to travel and that layer turns, drag the background to look around, or use the standard letters on a keyboard. Scored on moves and time, so a tidy solve beats a lucky one.
A first-person maze in real 3D: corridors you cannot see round, walls that light properly as you turn, and a map that fills itself in only where you have actually been. Three mazes to a run, each bigger than the last.
Both games are drawn on the same plain 2D canvas as everything else in the arcade. Adding a 3D library would have put back more weight than every game engine here carries put together, and undone the work v1.4.1 did to make pages lighter. The renderer is shared, so the next 3D game starts from a camera rather than from trigonometry.
Layered, optional, and never pay-to-win.
Two new layers that sit alongside the ones you already wear rather than replacing them: a decoration over your avatar, and a frame around the whole profile card. An avatar frame, a decoration, a profile frame, a nameplate and an effect can all be on at once.
The last of the extras promised back in v1.2.0. Your profile can arrive with an animation, leave a trail behind a visitor's cursor, and play a track you already own from the music library. The music is click-to-play, never automatic - a profile that starts making noise on its own is nobody's favourite feature.
A chip under your name shows what you are playing, or last played, worked out from your sessions rather than anything new being recorded, and it follows the online-status setting you already had. The visitor counter is off by default, counts unique people per day rather than refreshes, and never counts you.
Save everything you are wearing as a set and switch between them. Unlocked at level 20.
The long game: sets to complete, seasons to climb, and perks for boosters.
A tier track that fills as you play, with a reward at each tier. Progress is worked out from what you have actually done rather than a stored counter, so it can never drift; the only thing recorded is whether you have taken a reward.
Cosmetic sets with an exclusive badge for finishing one. Owning the set is derived from your inventory, so items arriving by any route count.
A monthly exclusive cosmetic, a monthly token to gift any cosmetic to a friend for 30 days, and early access to new games before everyone else. Locked games stay visible with a countdown, because a perk nobody can see is a perk nobody wants.
Recent achievements, purchases and new friendships from the people you follow, in one place.
Ten more routes now show a skeleton of the page instead of a blank screen, and spinners wait 200ms before appearing so a fast load never flashes one.
Every game card is generated from the same near-black base, grid and vignette, with a single accent hue giving each game its identity. Twenty-six cards now read as one product rather than twenty-six pieces of unrelated art.
The longest line the site has had: a step into 3D and real multiplayer at one end, one bot replacing four at the other, and eight patch releases of Discord and dashboard work in between.
A ground-up pass over how the site looks, feels and performs. One design system instead of many near-copies, motion that stays out of the way, and a lighter page on every device - the whole animation runtime and the query cache left the bundle entirely.
Why this is one release: Sixty-nine files in one pull request. Published as v1.4.1, which the games and themes overhaul eight days earlier already held - renumbered to the end of the line it actually shipped at.
The parts every page is built from, fixed once so eight screens stop drifting into eight slightly different looks.
Shadows are now brand-tinted tokens that deepen properly in dark mode, headings scale fluidly instead of jumping at breakpoints, and every transition uses one of two shared easing curves. Body text meets AA contrast in both themes.
Every top-level page opens with the same masthead, and every 'nothing here yet' surface - no friends, no messages, an empty inventory, no search results - uses the same component, with a real explanation and a way forward rather than a bare sentence.
Buttons gained a proper loading state that doesn't resize mid-click, cards gained surface variants, inputs and selects finally match each other, and dialogs stay inside a short phone screen instead of overflowing off it.
Enough to feel alive, little enough to stay out of the way.
Cards lift on hover, grids stagger in, the credit balance rolls when it changes, menus scale from the button that opened them, and the play button on a game card springs up under the pointer. All of it is transform and opacity only, so it runs on the compositor.
Every animation is gated behind a motion-safe check rather than merely shortened, so choosing 'reduce motion' gives a genuinely still interface instead of a fast one.
Fewer bytes to download, fewer pixels to repaint.
The animation library was doing six small jobs that CSS does natively, and the query cache existed for a single call in the command palette. Both are gone; the palette now caches the game list in module scope, and every animation they powered still works.
Hero and auth backdrops swapped full-viewport blur filters for background gradients, skeleton shimmer became a transform instead of an animated gradient position, and below-the-fold sections skip layout and paint until they approach the viewport.
Fonts swap in rather than blocking, the mono face no longer preloads, and the image size ladder was trimmed to the widths the layout actually requests.
A skip link opens every page, focus rings are consistent everywhere and never fire on a mouse click, navigation marks the current page, loading skeletons announce themselves, and progress bars report their value.
The favourite button no longer hides behind a hover state on phones, tab bars and filter rows scroll instead of wrapping, every tap target clears 44px, text inputs stay at 16px so iOS won't zoom, and toasts sit above the mobile tab bar.
Game grids fill available space rather than snapping between fixed column counts, which fixes the awkward tablet range where four columns were too many and three left a gap.
The admin dashboard stopped being eleven equal links above a page and became a sidebar, three groups and one set of shared pieces.
28 - 29 Jul 2026 · two pull requests, a day apart
Why this is one release: Two pull requests doing the same job from opposite ends - one grouped the Discord page into tabs, the other rebuilt the shell around every admin page. Six hundred lines together, and neither reads as a release on its own.
The nav sat above the content, so every page opened by pushing what you came for below the fold. It now sits beside the content on desktop and stays put as you scroll.
Pages used to begin however their author felt that day - a bare paragraph, a search box, a heading at whatever size. The heading now comes from the route, so a new page gets a consistent one by adding an entry rather than by remembering to match ten other pages.
Result lines, the Discord-ID field and empty states, in one file. The result line existed in five separate copies and the ID field in two; they had already started to diverge.
One page carrying thirteen cards and several hundred form fields became Actions, Sync, Levelling and Server - grouped by why you came, not by which part of Discord the setting touches. Same URL, so bookmarks still work.
Community, Content and System, with Overview above them. Eleven items get re-scanned every visit; three groups are learned once. Open reports now surface as a banner rather than a number to notice.
Settings that saved but never applied, linked roles that were duplicated rather than used, and a bot that was still called something else in half its own strings.
Why this is one release: Three fix-shaped pull requests inside ninety minutes, four hundred lines between them. Each is a paragraph; together they are a release.
Pointing a setting at an existing role or channel created a second one beside it. A configured id is now an instruction to use that exact one - renamed or recoloured to match if need be - and an id pointing at something since deleted is reported as missing rather than silently replaced.
Fields that were dropped on save, so the value could not survive a reload - including the ticket panel channel, which meant re-posting the panel was impossible.
One name, defined once, rather than a dozen string literals drifting apart - plus four bugs found in the sweep that renaming it required.
The dashboard could only store settings - the Discord side of a change happened when someone ran the matching slash command, and until then the panel and the server disagreed.
Why this is one release: One pull request, a thousand lines, and a structural change: every Discord operation moved out of the slash-command handlers into functions both surfaces call.
Announce, moderate, purge, slowmode and lock all call the same functions the slash commands call, so a case raised from the dashboard is numbered, DM'd and logged exactly like one raised in Discord. There is no second implementation to drift.
A save writes to Postgres and then pushes the section to Discord, and the result says what actually changed there. The push is best-effort: a Discord outage is reported as a warning against a successful save, never as a failure that leaves you wondering whether to retype everything.
Setup failures stopped being opaque. Every one of these was costing a round trip of debugging a permissions problem that was never there.
Why this is one release: Three small fixes in one afternoon, all about the same thing: a Discord call failing without saying why. Two hundred and thirty lines between them - one release, not three.
A checklist of which Discord environment variables are actually set, instead of a setup that fails opaquely when one of four is absent.
When `/setup` cannot create something, the reply is Discord's own message and status code. Any guess we could make about the cause is worse than the answer Discord already sent.
HTTP header values must be Latin-1, and the audit reason contained an em dash - so `fetch` threw before the request was ever sent, which surfaced as a network error. The header is now URL-encoded, which is how Discord documents it.
The roadmap had grown into an archive. Everything shipped moved to /updates, the roadmap became forward-only, and registering slash commands stopped needing a terminal.
Why this is one release: One pull request that split the roadmap in two and gave the shipped half its own page - the release this very page came from.
Past releases, merged pull requests and every change that has landed on `main`, generated from git rather than maintained by hand.
The roadmap now carries what is coming and nothing else. When something ships it moves out rather than being marked shipped and left in place - which is how it became an archive the first time.
A button in the dashboard, or one authenticated POST. Registration is a full replace, so it is safe to repeat.
Play with other people: across accounts online, or on one device in the same room - plus a status page, vanity URLs, and the heartbeat that makes the bot's Online light mean something.
Why this is one release: Forty-five files in one pull request. This is where multiplayer and parties actually shipped - v1.4.0 promised them, and carried an 'extended' note for a week to cover the gap.
Play with other people - across accounts online, or on one device in the same room.
Games multiple people can play together, online across accounts. Tic-Tac-Toe, Connect 4 and Reversi are true head-to-head matches on one shared board with alternating turns; every other game becomes a score race - same game, same moment, live standings. Local pass-and-play remains in Tic-Tac-Toe via the 2P toggle.
Group up into a party to jump into multiplayer games together. Create one, share a six-character code or invite friends straight from your friends list, and the leader picks the game and starts it for everyone at once.
Rearrange the homepage straight from the admin dashboard - reorder or hide any section from Admin → Site, no code required.
One page saying whether the site, the database and the bot are up - including the gateway worker's heartbeat, which until now had nothing writing it.
Claim a custom profile link, and an extra daily challenge for boosters that everyone can see but only boosters can claim.
One bot doing what Appy, Sapphire, Arcane and ServerStats were doing between them - and running for free, because the commands are served by the website itself.
Why this is one release: Thirty-three files and five thousand lines replacing four separate bots. Nothing else went near it, and nothing else belongs with it.
A button panel, a verified role, a minimum account age and an optional welcome message or DM - with the log of who got in and when.
Numbered cases with DMs and a mod-log channel, an announcement command with scoped role pings, automod for invites, links, mentions and spam, and a ticket system with its own category and staff role.
Chat XP with configurable rates, cooldowns and curve, milestone roles handed out as people climb, and level-up announcements.
Voice-channel counters for players online, total members, plays today and Discord members, from templates you can edit.
An optional always-on gateway worker for chat XP, automod, join handling and the live feed - and the heartbeat that lets the site say whether it is actually running.
Two fixes worth having and one chore: a boost that was being thrown away, and five RPCs reachable by more roles than intended.
Why this is one release: Three unrelated small commits across two quiet days between feature releases. None is worth a version on its own; leaving them unversioned was the thing worth fixing.
Buying a boost beyond the stack cap puts it in a queue to take over when the current one expires. The queue was not being drained, so the boost was simply gone. Fixed in the database, where the rule belongs - and the duplicated auth round-trips around it were removed while the code was open.
Five RPCs meant for signed-in players were revoked from `anon` but not from `PUBLIC`, which grants to every role including `anon`. Revoking from `anon` alone does nothing while the `PUBLIC` grant stands.
Every canvas game re-rendered at device resolution, fullscreen that takes the whole player rather than the page, and a colour theme for the entire site.
Why this is one release: Nineteen files in one commit, all about how games look and feel on the device you are actually holding. Its commit message and changelog heading both said v1.4.1, and it keeps the number by right of arriving first.
Tic-Tac-Toe, Connect Four, Simon, 15 Puzzle, Lights Out, Bubble Pop, Target Rush and Reversi were rebuilt to the quality bar in v1.2.2 and are now republished.
Every canvas renders at device resolution up to 2×, so games are pin-sharp on retina screens. Fullscreen takes the whole player - score, controls and touch pads - onto an ambient themed backdrop with the game letterboxed, and it works on mobile.
The Controls tab shows touch controls on touch devices and keyboard controls on desktop, with a toggle to peek at the other. Games pause themselves when the tab loses visibility.
Arcade Violet, Midnight, Ocean and Emerald free for everyone; Crimson, Gold Rush, Neon Rose and the animated Synthwave and Aurora reserved for boosters and staff, with a lock shown on the swatches. The gate is enforced in the database, applied before first paint, and the animated ones respect reduced motion.
The games library, game pages, shop, leaderboards, messages and profiles now show the shape of the page while it loads, and the document preconnects to Supabase for a faster first fetch.
The step out of two dimensions - a pseudo-3D racer running smoothly on a phone.
Why this is one release: One commit: the first 3D title and the engine behind it. The parties and multiplayer this release originally promised shipped six days later and are recorded at v1.4.4, where they belong.
Beyond the 2D arcade - fully playable 3D games in the browser.
An OutRun-style pseudo-3D racer: a projected road, hills and curves that read correctly at speed, and traffic to weave through - drawn on the same 2D canvas as everything else in the arcade.
Two players on one device, taking turns on the same board - the simplest multiplayer there is, and the one that needs no network code at all.
One release, and a deliberately short line: the loops meant to hold someone for a year rather than a fortnight.
Turn the hub into somewhere players return to for years, not weeks: original music, stacking events, long-term streaks, deep booster and level rewards, and a proper analytics control centre.
Why this is one release: Twenty-four files, two thousand lines, one commit. Every part of it serves the same goal, so splitting it would have made five releases that each explain a fifth of an idea.
Daily streaks alone won't hold someone for a year - these loops are designed to.
Server-wide co-op goals where everyone pulls in the same direction - e.g. 'play 500 games together this weekend' - with a live progress bar and an achievement plus bonus credits for everyone who took part.
Hitting a level milestone unlocks a real feature, giving levelling a point beyond a number: L5 background music · L10 create groups · L15 stories · L30 a vanity profile URL. Two further milestones are planned for v1.5.0.
A Snapchat-style daily streak, but with messages instead of images - keep a conversation going day after day with a friend to build a streak and earn rewards, giving people a reason to check in on each other.
Boosters keep the community's home running - the perks should feel genuinely worth it, while never becoming pay-to-win.
An extra daily challenge on top of everyone else's - more ways to earn, every day. Everyone can see it, so the perk is visible, but only boosters can claim it.
The Booster badge evolves the longer you've boosted (1 month → 3 → 6 → 12), with a visibly fancier treatment at each tier to recognise loyalty.
A larger daily reward and a faster-growing streak multiplier while your boost is active.
Claim a custom profile link (e.g. /u/yourname). Unlocked by boosting, by reaching level 30, or by being staff - claim or change it from Settings.
Level 5+ players can buy original 'tracks' from the shop and play them in the background while they browse and play. Every track is composed in-house, so there are zero copyright concerns - and boosters can set one as their profile theme song.
Credit boosts stack up to 5× (10× for Discord boosters). Buy another beyond the cap and it doesn't go to waste - it joins an effect queue and automatically takes over the moment the current boost runs out, so your boosts are always working.
A dedicated admin section for analytics - site clicks, popular games, active players and retention - plus control over which surfaces appear across the site, without touching code.
Manage this very roadmap from the admin dashboard - add, edit and reorder releases and items without touching code, so plans stay fresh with a few clicks.
The plan to run ads - the simulated 'watch to double your credits' flow and a NitroPay integration - has been dropped. It complicated the reward maths and there was never a network behind it, so the whole programme was removed rather than left half-built. Credits now come from playing, streaks and boosts alone.
The line that gave the site a personality: profiles worth looking at, a messenger worth using, an arcade rebuilt to a quality bar, and the first version of the Discord bot.
The Discord bot becomes a first-class citizen - rebuilt serverlessly so it runs for free, with secure account linking, an Arcane-replacing level system, automatic role sync, and a proper legal foundation for the whole platform.
Why this is one release: Four commits on one day, all platform-level: the bot, the legal pages, the share image and the analytics that arrived with them.
One bot, wired straight into your Hub account - no paid hosting anywhere.
All commands (/link, /rank, /levels, /daily, /pay, /profile, /leaderboard, /sync, moderation) now run through Discord HTTP interactions served by the website itself - signature-verified, free, and always on.
Link Discord from Settings → Connections via Discord OAuth, or with a one-time /link code minted in the server. Both paths prove you own the Discord account; unlink any time.
Chat XP with configurable rates, cooldowns and level curve - anti-spam enforced in the database. /rank and /levels leaderboards, level-up announcements, website notifications, and an optional XP trickle into your Hub level.
Hub badges, achievements, staff status, nameplates and levels map to Discord roles. Synced on change, on join, on /sync and nightly - the website is always the source of truth.
A new Admin → Discord bot page to tune XP rates, curves, announcements and the role map without touching code.
Levelling now unlocks real features: create groups at level 10 and post stories at level 15 - or link Discord for instant access, as before.
A new Admin → Analytics page: daily/weekly/monthly active players, plays per day, sign-ups per day and average session length - computed from existing data, no extra tracking.
Proper, readable legal pages written for UK GDPR and linked from the footer, sign-up and settings - describing exactly what the platform actually collects.
Links to the Hub now unfurl with a proper branded preview image.
A quality pass on the two things people do most - play and chat. Six more games rebuilt to a modern, tactile bar; the kept cosmetics given a Discord-tier animation glow-up; and messaging made genuinely reliable, now with GIFs.
Why this is one release: Nine commits over one long day, all of them the same quality pass held to the bar Tic-Tac-Toe set in the first of them.
Chat should feel instant and alive - and a little more fun.
A Discord-style GIF picker in the composer, powered by Giphy: search or browse trending GIFs and tap one to send. It arrives as a message that renders inline as an image. You pick from Giphy only - no uploading or pasting your own image URLs - so it stays clean and safe.
Messages no longer get stuck on 'Sending…' until you refresh. A sent message now resolves the instant the server confirms it, independent of the realtime echo.
The thread stays live for both sent and received messages without ever reloading the whole page - a cheap background sync fills in anything realtime misses, so you see new messages within seconds.
Every game held to the standard set by Tic-Tac-Toe and Connect Four.
Simon, 15 Puzzle, Lights Out, Bubble Pop, Target Rush and Reversi rebuilt from scratch as animated, mobile-first, tactile canvas games - glowing feedback, satisfying motion and smarter opponents where it counts.
The kept nameplates, frames, effects, themes, banners, badges and boosts had their particles and animations reworked to a Discord-tier bar - flowing gradients, travelling sheens, rotating rims and layered particle systems, all reduced-motion friendly.
A curated cull of overlapping cosmetics with automatic credit refunds, and the group-creation bug fixed so groups always create cleanly.
A small polish release on top of v1.2.0: richer notifications, linkable announcements, a redesigned podium and a handful of quality-of-life fixes.
Why this is one release: Five small commits the same day v1.2.0 shipped - the corrections you only find once a release is in front of people.
Tap any notification to open it in full - the complete message, the exact date and time it was sent, and an Open button when a link is attached. Opening marks it read.
Admins can attach a call-to-action link to an announcement, and publishing with 'Notify everyone' now actually sends a notification (with that link) to every player - the toggle previously did nothing.
The global leaderboard's top three now sit on a proper tiered gold/silver/bronze podium with rank badges, a crowned #1 and equipped nameplates.
Group conversations gained a header menu to copy the invite link again or leave the group.
Gift straight from someone's wishlist, and the weakest games were pulled back to 'coming soon' rather than left on the shelf in the state they were in.
The release that makes your profile unmistakably yours and the community feel alive: Discord-grade cosmetics, expressive identity, richer friendships, group chats, a modern messenger, and a shop and inventory that are finally a pleasure to use.
Why this is one release: Fifty-nine files in one commit. The largest single release until v1.5.0.
Your profile should say who you are before you type a word. We're taking cues from Discord, Roblox, Steam and the big social platforms - layered, expressive, and never pay-to-win.
Your equipped nameplate stops living only on your profile page and follows you across the whole site - search results, friends lists, leaderboards, chat headers and message bubbles - so people recognise you instantly wherever you show up.
Discord-style avatar decorations that render everywhere your avatar appears: animated frames, orbiting particles, soft pulsing glows and looping effects. Layered above your picture and tuned to stay readable at small sizes.
Banners scale with how invested you are: a clean solid colour for email-only accounts; animated gradients and a curated library of premade art for Discord-linked players; and full custom PNG/JPEG uploads (with sensible size limits and moderation) for server boosters.
A real effects engine, not just a flair badge. Pick a background colour or gradient, add ambient animated layers (falling snow, drifting stars, aurora, embers, confetti), set intensity, and stack multiple accents together for a look that's genuinely yours.
Recolour your whole profile card - buttons, highlights and dividers - with a curated accent theme, so a visitor feels your vibe the moment the page loads. Hand-picked palettes only, so nothing ever clashes.
A curated set of display fonts plus particle, glow, shimmer and gradient treatments for your name - expressive but always legible.
Steam-style showcases: pin your rarest cosmetics, proudest achievements, favourite games and best scores to the top of your profile so the first thing people see is what you're proud of.
Choose one achievement to headline your profile with its full art and rarity.
Optional profile fields - pronouns, a one-line status, favourite game, join date and a short bio - arranged as tidy widgets you can show or hide.
Every cosmetic gets a rarity - common through mythic - with matching visual treatment and a clear label in the shop and inventory, so rare items actually feel rare.
Genuinely special, unbuyable cosmetics for admins, mods and developers - distinct animated nameplates, frames and decorations - so staff are recognisable at a glance and the role feels earned.
An optional 'Connect' button that surfaces your Discord for verified players who want it shown - off by default, entirely your call.
Make the hub somewhere you come to hang out, not just to play.
See the friends you have in common with someone - shown only for users who choose to make their friends list visible, so it's discovery without exposure.
A lightweight one-way follow alongside two-way friendships - keep up with players you admire without needing them to accept, and they're notified when you do.
Per-user control over who can see your friends list: private, friends only, followers, or fully public.
Set a private nickname for a friend that only you see, and leave a private note on anyone's profile as a personal reminder of who they are.
Create a group with a shareable invite link (e.g. /invite/<groupId>), managed by a group admin who can add, remove and promote members. Limited to boosters, mods and admins at first to keep it clean and spam-free.
Rework messaging to feel like WhatsApp - minus file/image/video/audio sharing and calls: clean threads, emoji reactions, replies, pinned and favourite chats, and delivered/seen receipts done right.
A proper emoji picker and a solid mobile typing experience across the site - including fixing the frustrating built-in keyboard in Snakes & Ladders while we're in there.
Post text or an achievement to a story that expires after a day. Boosters, mods and admins only for now while we prove out the format and moderation.
Upgrade online status to online / offline / do-not-disturb / sleep, add a 'last online' time, and even an optional 'playing now' game - each with fine-grained controls over exactly who can see it.
Add store items to a wishlist you can view and manage from your inventory, and gift items to other players at 75% of the normal price - deliberately cheaper than buying for yourself, so gifting is the generous and the smart move.
Buying, previewing and managing cosmetics should be effortless - and fun.
Click any shop item to open a full preview page (in a new tab) that renders the effect live - see exactly how a nameplate, banner, effect or decoration looks on a real profile before you spend a single credit.
Already own an item? Apply it to your profile or avatar right from the shop page - no detour through the inventory required.
A search bar plus filters - by cost, rarity/exclusivity, date acquired and item type - so even a huge collection stays easy to browse and organise.
Fix the boost countdown timer and multiplier readout so active boosts always show the correct time remaining and the true stacked multiplier at a glance.
A proper mobile hamburger menu - today's bar is far too cramped for the space - plus a cleaner desktop nav with more breathing room between items, better spacing and smoother animations. It should simply look and feel good.
A rebuilt inventory with clear 'applied' indicators on every item and one-tap apply/disable.
Spin up an event and set things like a credit multiplier, duration and banner in a couple of clicks - no fiddly config.
Give or take XP, credits and levels from the admin panel with far less friction - search a player, adjust, done, with an audit trail.
Detect the player's device and show on-screen touch controls on mobile and keyboard/desktop hints on desktop automatically - whichever they're actually using, without a manual toggle.
The rebuild: the hand-written static site replaced by a Next.js app on Supabase, deployed, corrected, and given a public plan.
The roadmap stopped being a document nobody outside the project could read.
Why this is one release: Three commits about one page - two of them edits to the page the first one added.
What is planned, grouped by release, with a status on each item - and the definition of done every shipped feature is held to, stated in public.
The first plan was a flat list of wants. Splitting it into three named releases is what made the next fortnight's work legible - and is the reason those three releases exist at all.
Five things that were visibly wrong in front of the first people to use the site.
Why this is one release: The first pass of corrections after launch, and the upload that carried them.
The headline treatment used everywhere fell back to a filled rectangle on some browsers.
There was no way to flag a mine on a phone at all - the game was unplayable on touch.
Games stretched to fill rather than scaling, and rendered at CSS pixels rather than device ones.
Turning ads off in the admin panel left several surfaces still showing them.
The two files every search engine asks for, and the site had neither.
Rebuilt from scratch as a Next.js app on Supabase: Discord-only sign-in, mobile-first games, an admin control centre, profile customisation and a living economy.
19 Jul 2026 · first deploy 20 Jul 2026
Why this is one release: The rebuild and the six commits that made deploying it repeatable. One release because none of them is any use without the others - an app that cannot be built is not a version of anything.
Accounts, sign-in and a username that is yours across the site - with Discord as the only identity provider, so there are no passwords to lose.
Touch controls, responsive canvases, per-game tuning, and safe-area and overscroll handling - the arcade built for a phone first rather than adapted to one.
Users, games, announcements, reports, economy and feature flags, in one place.
Nameplates, staff flair and profile effects - the first version of the cosmetics engine.
Credits, XP, levels, daily rewards and timed events with multipliers.
Everything a build needs to fail loudly rather than quietly - with the raster icons generated at build time from one source rather than committed as a dozen files.
Production defaults for the publishable environment values, and its own dedicated Supabase project rather than one shared with something else.
Pull request:#6
The original site, four months before the rebuild: hand-written pages, two games and no server. Its commits are no longer reachable from `main`, so the pull requests are all that is left of it.
A static arcade of hand-written pages, with scores kept in the browser because there was nowhere else to put them.
Why this is one release: Five pull requests merged in one morning, none of which stands alone. Together they are the entire first version of the site.
The two launch titles, playable in the browser with no account and no server.
One manifest describing every game, with the landing page and each game's detail content generated from it rather than written twice.
The CSS and scripts every page carried its own copy of, pulled into one place - which is what kept the pages small enough to go on hand-writing.
High scores and plays kept in localStorage: the first version of a profile, with nowhere to sign in.
Every pull request merged into main. The five oldest predate the rebuild of the site as a Next.js app, so their commits are no longer reachable from the current history - GitHub still remembers them.
Every change that reached production, newest first - whether it arrived through a pull request or as a direct commit, and which release it shipped in. 64 in total, going back to the very first one. 3 of them have not been assigned to a release yet.
Curious what's next? See the roadmap.