← blog · September 16, 2026 · 18 min read
I built a galaxy for eighteen people
Spacegram is an OGame-style strategy game living inside Telegram and running entirely on Cloudflare's free tier. A month of real traffic fits inside a single day of the free allowance. Getting people to play was the hard part.
I could open this with the architecture and never get round to the numbers. The numbers go first.
Eighteen people signed up. Seven of them opened it once, looked around, and never took a single action. The eleven who did play peaked at ten active in a single week, on 29 August. By the middle of September the previous seven days contained exactly one of them.
That is the whole story of a small game and I would rather put it at the top than bury it. The rest of this is how the thing was built, and why a month of a live multiplayer game fit inside one day of Cloudflare’s free tier.
What it is
Spacegram is OGame in a chat app. You get a homeworld, you queue mines and power plants and a shipyard, you research a tech tree, you build ships, and then you start sending fleets out into a procedurally generated galaxy that nobody has charted yet. Timers run in real time. You close the app and the bot messages you when the building finishes or the fleet comes home.
The galaxy is generated from a single world seed, so a system only becomes a database row the first time somebody flies to it. Everything else is a hash of the coordinates, computed identically on the server and in the browser. The map you are panning around is mostly local arithmetic.
That screenshot is a late empire. The mines are at level 16 and the planet is dry on all three resources, which is the thing I am proudest of in the whole design: planets hold a finite, surveyed amount of alloy, crystal and fuel, and when you have dug it out it is gone forever. Homeworlds get no exemption. Either you expand or you starve.
Combat is a rounds based simulation seeded from the mission id, so it is deterministic and testable, and every battle folds open into damage dealt and hulls lost per round. You can tell a rout from a near thing.
The rest of it is the usual furniture of the genre: a tech tree, a shipyard, a fleet screen with whatever you have in flight, a guild market whose prices swing between minus 75% and plus 300% and move every time somebody trades.
Why Telegram is the best onboarding funnel I have ever shipped against
I have shipped web apps. The funnel is: read the tweet, click the link, land on a marketing page, click sign up, pick a password, check your email, confirm, log in, and now finally you are looking at the product. Every one of those steps is a place to lose somebody.
Telegram’s funnel is: tap a link in a chat, and you are playing.
There is no install. A Mini App is a web view inside a chat client the player already has open. No app store, no review queue, no 40MB download. My deploy is wrangler deploy and everybody has the new version on their next tap.
Authentication comes for free, and it is real authentication. Telegram hands the web view a signed initData blob. The server verifies an HMAC of it with the bot token on every single request, which takes one Web Crypto call and no session storage, and out the other side comes a verified user id and display name. There is no login screen in Spacegram because there is nothing to log in to. First verified request creates your homeworld.
The timers only work because the bot can interrupt you. An async strategy game is nothing but timers, and a timer is worthless if the player has to remember to come back on their own. On the web that means push permissions, service workers, and a browser that throttles you anyway. Here the Durable Object that just resolved your events calls sendMessage and the player gets a normal chat message from a bot they already talk to. No permission prompt, no infrastructure.
🏗 [Homeworld] Alloy Mine reached level 16
🛰 Expedition returned with loot (1,842🔩 623💎 368🛢)
🏴☠️ [Homeworld] Pirates spotted, ~68 raiders inbound (from 9:-1), striking in ~22m!
The rule I wrote down early and never broke: reports are the truth, messages are a courtesy. Delivery is best effort, it retries with backoff and gives up after three attempts, and nothing in the game depends on a message arriving. The in-app report always exists.
There is no invite system, because a chat app already has one. You forward the bot to a group. That is the entire growth mechanic, and it is also exactly why the graph at the top looks the way it does, but I will come back to that.
Release notes go straight into the chat. Three times I wrote a changelog and ran a script that pushed it to every player as a formatted message. No app store review, no in-app update modal, no email list. node scripts/broadcast.mjs docs/changelog/rev3.html --send and everyone who had ever played knew the pirates were now a faction.
The thing Telegram does not solve is retention. It gets a person into the game at a cost of one tap, which means it also gets people in who were never going to play. Seven of my eighteen.
The whole thing runs on free
One Worker. It serves the API, the bot webhook, the cron trigger and the static assets of the React app. One Durable Object class, EmpireDO, with one instance per player keyed by Telegram user id. One D1 database.
The split between the two storage layers is the design decision everything else falls out of. The Durable Object answers “what is this player’s exact state right now”: resources, building levels, tech levels, queues, fleets in flight, pending events. It is authoritative and hot. D1 answers questions that cross players: the galaxy index, the reports inbox, the standings, the bounty board, where the pirate dens are. D1 is a projection of Durable Object state, refreshed when something meaningful happens, not a backup that anything depends on.
The load bearing rule underneath both: everything that decides a game outcome lives in a pure TypeScript package with zero Cloudflare imports. Resource accrual, cost curves, the combat simulation, the expedition resolver, galaxy generation. The Worker only orchestrates. That is why the balance tests run in plain vitest, why the client can render the galaxy with the same function the server uses, and why I could tune numbers without touching a single request handler.
The one constraint that shaped everything
D1’s free plan allows 100,000 row writes per day. Not requests, rows. That single number decided the entire tick architecture before I wrote a line of it.
The naive version of a game like this has a loop that runs every minute and updates every player’s resources. With 100 players that is 144,000 writes a day doing nothing but incrementing counters, and you are over the limit before anybody has played. So Spacegram never ticks.
Resources are stored as a snapshot plus a timestamp plus a rate. Your current alloy is snapshot + rate × elapsed, computed on demand, by the same function on the server and in your browser, which is also why the numbers in the resource bar count up smoothly while you sit there doing nothing. Nothing is written.
Writes happen when an event resolves or you commit an action. Every action computes its completion time and appends an event, and the Durable Object sets a single alarm to the earliest one. When the alarm fires, the object accrues resources to now, pops every event that is due, resolves each through the pure engine, persists its own state, and then flushes one batched transaction to D1: the planet snapshot, techs and points, any report rows. Then it sends the Telegram messages and sets the next alarm.
The universe does not run for absent players. It catches up when somebody addresses it. The only global actor is a cron trigger every five minutes that reads a bounded slice of recently active players, decides who the pirates are going to hit, and pokes a handful of Durable Objects.
The arithmetic I did up front said a hyperactive player generates roughly 100 events a day, which is 200 to 300 row writes, which puts about 300 daily active players under the ceiling before I would have to think about it.
What it actually used
From 16 August to 16 September the Worker served 108,107 requests. The free plan allows 100,000 a day. The entire life of the game, a month of it, is roughly one day of the allowance.
The busiest single day was 22 August, at 46,200 requests, which is the day rally beacons shipped and everybody was on the map at once. That is 46% of one day’s limit, at the peak, with the most people who ever played playing at the same time.
D1 wrote 155,664 rows in the month, against a ceiling of 100,000 per day. The worst day was 10,642, which is a tenth of one day’s budget. Reads were 9.94 million over the month against 5 million per day. Durable Objects handled 87,653 requests. The Worker made 32,122 outbound subrequests.
| Resource | Free plan | Busiest day | Whole month |
|---|---|---|---|
| Worker requests | 100,000 / day | 46,200 | 108,107 |
| D1 rows written | 100,000 / day | 10,642 | 155,664 |
| D1 rows read | 5,000,000 / day | 972,818 | 9,942,051 |
| Durable Object requests | 100,000 / day | 14,340 | 87,653 |
One number did move in an interesting direction. Median CPU time per invocation was under 2ms during launch week and had climbed past 25ms by mid September, while daily requests fell from 46,200 to under 300. Nothing got slower. The mix changed: once the players stopped opening the app, almost the only traffic left was the five minute cron pulse, which is the single most expensive thing the Worker does. When the humans leave, the average request is a robot doing bookkeeping.
There is a loose end in that. The free plan allows 10ms of CPU per invocation, for HTTP requests, cron triggers and Durable Object alarms alike, and those medians are well past it. The Worker also logged 653 errors over the month, none of them before 29 August and all of them in small daily clusters after it, which is roughly when the Dominion started running hundreds of pirate convoys and the pulse got heavy. I have not chased it down. It has not cost anything visible, because event resolution is idempotent and a failed pulse just means the pirates are late, but if this were a real product that is the thread I would pull first.
Four revisions in ten days
The game that shipped on 16 August was the plan: build, research, explore, get raided by pirates. Everything interesting happened afterwards, in four revisions over the following ten days.
Revision 1, the Anchor. A ship with no guns and no cargo hold whose only job is to plant a rally beacon that the whole galaxy can see. You pick the target and a window of 6, 12 or 24 hours, the anchor parks at a staging point halfway there, and anyone can tap the beacon on the map and send ships. When the window closes everyone flies the last leg together at the slowest hull’s pace and fights as one fleet. It turned a solo game into one where people talked to each other.
Revision 2, finite worlds. The deposits change. Planets hold a surveyed amount of each resource and run dry permanently. Along with it: move your capital to any safe colony, a Tracking Station that watches a zone for other commanders’ fleets, a stealth escort that hides a convoy from exactly that, and guild trades that stopped being instant swaps and became convoys you can intercept.
Revision 3, the Dominion. The pirates stopped being a spawn table and became a faction. Every den near settled space is a garrison that grows, climbs tiers, and launches its own raids, so the den nearest you is the one hitting you. Raze it and its raids stop. Dens reinforce each other with convoys that crawl between systems in the open, and you can race one to an ambush point and take its cargo. A colony sharing a system with a living den pays tribute into its hoard, and the hoard goes to whoever lands the killing blow.
Those numbers are real. Across the month the players killed 9,723 raiders, razed 74 dens and charted 65 systems, while the Dominion moved 5,021 supply convoys between its own.
Revision 4, quiet beacons. Mostly a polish pass. Joining a rally stopped consuming one of your mission slots, beacons could be private, and strike odds started reading the live garrison instead of an estimate.
The interface moved with it. The same screen, on the Revision 3 build and on today’s:


Both of those came out of playing it on a phone and being annoyed. The planet header was a paragraph you had to read; now it is a row of chips you can take in at a glance. The shipyard would happily let you queue a fifth Anchor without mentioning that four were already docked ten metres away. And the navigation bar grew text labels under the icons, because six emoji in a row is a guessing game.
What the graph actually says
The infrastructure was never the problem. That is the honest summary of the free tier section: I designed carefully around a 100,000 row per day write budget and then used 10% of it on the worst day I ever had. The constraint was real, it made the architecture better, and it was never once close to binding.
The problem is the red line.
Eighteen people is what you get when the only distribution is the share button in a chat app. Seven of them never played, which is the cost of a funnel so cheap that opening it is not a signal of anything. The eleven who did play stayed for a median of eleven days, and five of them were still launching fleets more than two weeks after they joined.
For a game with no marketing and no app store listing, I am not going to pretend that is a failure. The loop works. People built empires, formed rallies, fought each other four times, and one commander finished with 23,491 points and four planets. The thing is finished enough to play and cheap enough to leave running forever, which is more than most side projects get.
But if you take one thing from this: Telegram gives you a one tap funnel and Cloudflare gives you a free backend, and neither of them gives you players. Those two problems look adjacent and they are not remotely the same problem.
Screenshots are from a local instance running a copy of the live galaxy, with commander names changed.