<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Yitao</title>
    <description>The latest articles on DEV Community by Yitao (@yitao).</description>
    <link>https://dev.to/yitao</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3740904%2F867f72d1-0507-4a83-a1fd-a566a0f949bb.jpg</url>
      <title>DEV Community: Yitao</title>
      <link>https://dev.to/yitao</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/yitao"/>
    <language>en</language>
    <item>
      <title>Building a Multilingual Roblox Site: Lessons from +1 Loot to Forge</title>
      <dc:creator>Yitao</dc:creator>
      <pubDate>Sat, 10 Oct 2026 03:20:21 +0000</pubDate>
      <link>https://dev.to/yitao/building-a-multilingual-roblox-site-lessons-from-1-loot-to-forge-1cjb</link>
      <guid>https://dev.to/yitao/building-a-multilingual-roblox-site-lessons-from-1-loot-to-forge-1cjb</guid>
      <description>&lt;p&gt;&lt;em&gt;A WorldPlace case study based on 28 days of Google Search data.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Over 28 days, +1 Loot to Forge recorded &lt;strong&gt;20,225 Google Search clicks&lt;/strong&gt; across the language versions of its WorldPlace page. The English version received &lt;strong&gt;31&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;I run &lt;a href="https://worldplace.ai/" rel="noopener noreferrer"&gt;WorldPlace&lt;/a&gt;, a website that combines Roblox game information, discovery features, and tools for creators. Its Search Console data gives me a practical way to decide which games deserve closer attention and which questions their pages need to answer.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Frf5olx7lb2frj41kyf1f.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Frf5olx7lb2frj41kyf1f.png" alt=" " width="800" height="423"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;For this case study, the strongest signals came from localized game pages and searches for in-game reward codes. They raise concrete product questions: how to recognize a game across languages, keep changing information accurate, and help a reader move from a quick answer to deeper research.&lt;/p&gt;

&lt;p&gt;Here is what the data shows, and how I would use it to guide the next round of improvements.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the Search Console data shows
&lt;/h2&gt;

&lt;p&gt;The reporting period is &lt;strong&gt;September 8 through October 5, 2026&lt;/strong&gt;, with the search type set to Web. The property-level chart recorded &lt;strong&gt;58,664 clicks and 719,215 impressions&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fph751d0i7sd9p28pyp4k.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fph751d0i7sd9p28pyp4k.jpg" alt=" " width="799" height="283"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;To compare games, I grouped the game-page URLs in the 979-row Pages export by their game slug. That brings URLs such as &lt;code&gt;/th/games/1-loot-to-forge&lt;/code&gt; and &lt;code&gt;/vi/games/1-loot-to-forge&lt;/code&gt; into the same group.&lt;/p&gt;

&lt;p&gt;Here are the six leading games by clicks after combining their language versions:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Game&lt;/th&gt;
&lt;th&gt;Search clicks&lt;/th&gt;
&lt;th&gt;Impressions&lt;/th&gt;
&lt;th&gt;CTR&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;+1 Loot to Forge&lt;/td&gt;
&lt;td&gt;20,225&lt;/td&gt;
&lt;td&gt;100,338&lt;/td&gt;
&lt;td&gt;20.16%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Find the Needle&lt;/td&gt;
&lt;td&gt;8,472&lt;/td&gt;
&lt;td&gt;68,286&lt;/td&gt;
&lt;td&gt;12.41%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Find a Needle in a Haystack&lt;/td&gt;
&lt;td&gt;5,592&lt;/td&gt;
&lt;td&gt;70,973&lt;/td&gt;
&lt;td&gt;7.88%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Command an Army&lt;/td&gt;
&lt;td&gt;5,048&lt;/td&gt;
&lt;td&gt;93,712&lt;/td&gt;
&lt;td&gt;5.39%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Steal an Egg&lt;/td&gt;
&lt;td&gt;3,690&lt;/td&gt;
&lt;td&gt;131,125&lt;/td&gt;
&lt;td&gt;2.81%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Anime Dice&lt;/td&gt;
&lt;td&gt;3,170&lt;/td&gt;
&lt;td&gt;54,646&lt;/td&gt;
&lt;td&gt;5.80%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The Pages export totals 60,478 clicks. That differs from the property-level chart because Search Console uses different aggregation methods for different views. I use the Pages total for page contribution calculations, following &lt;a href="https://support.google.com/webmasters/answer/7576553?hl=en" rel="noopener noreferrer"&gt;Google's explanation of report aggregation&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;On that basis, +1 Loot to Forge accounts for &lt;strong&gt;33.44% of exported page clicks&lt;/strong&gt;. It is the clearest main case for this dataset.&lt;/p&gt;

&lt;p&gt;The grouping itself can be expressed with a small Python function. It expects records from the exported Pages table:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;collections&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;defaultdict&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;urllib.parse&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;urlsplit&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;summarize_games&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;page_rows&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;totals&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;defaultdict&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;lambda&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;clicks&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;impressions&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;page_rows&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;path&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;urlsplit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Top pages&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]).&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;
        &lt;span class="n"&gt;match&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;search&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/games/([^/]+)/?$&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;match&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;game&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;totals&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;match&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;group&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
            &lt;span class="n"&gt;game&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;clicks&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Clicks&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
            &lt;span class="n"&gt;game&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;impressions&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Impressions&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;game&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;totals&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;values&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
        &lt;span class="n"&gt;impressions&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;game&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;impressions&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="n"&gt;game&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ctr&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;game&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;clicks&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;impressions&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;impressions&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;totals&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;CTR comes from combined clicks divided by combined impressions. Averaging the individual page percentages would give small pages too much influence.&lt;/p&gt;

&lt;p&gt;This ranking measures WorldPlace's search performance during one period. It does not establish the most popular Roblox games worldwide or the fastest-growing games this week.&lt;/p&gt;

&lt;h2&gt;
  
  
  One game can have very different search results across languages
&lt;/h2&gt;

&lt;p&gt;Across the Pages export, &lt;strong&gt;non-English URLs received 99.09% of clicks&lt;/strong&gt;. Vietnamese, Thai, and Brazilian Portuguese pages together accounted for 57.38%.&lt;/p&gt;

&lt;p&gt;The language breakdown for +1 Loot to Forge makes that distribution easy to see:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Language version&lt;/th&gt;
&lt;th&gt;Search clicks&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Thai&lt;/td&gt;
&lt;td&gt;6,187&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Brazilian Portuguese&lt;/td&gt;
&lt;td&gt;3,973&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Vietnamese&lt;/td&gt;
&lt;td&gt;2,952&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;English&lt;/td&gt;
&lt;td&gt;31&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;These are results for this site's pages. Ranking, competition, and the queries each page appeared for could all contribute to the difference. A translated URL also does not tell me a reader's native language.&lt;/p&gt;

&lt;p&gt;Still, the product implication is substantial: content maintenance has to include the localized pages receiving those clicks.&lt;/p&gt;

&lt;p&gt;For a game directory, I would organize localization around a shared game identity. The official Roblox link and developer belong to that identity. Local titles, spelling variants, and explanatory copy belong to the language version.&lt;/p&gt;

&lt;p&gt;That gives readers two useful things at once: wording they recognize and a clear way to confirm which game the page describes.&lt;/p&gt;

&lt;p&gt;It also makes updates easier to reason about. If a developer changes a reward announcement, that is a change to the underlying game information. Every language version using that information becomes a candidate for review.&lt;/p&gt;

&lt;p&gt;The translation can sound natural while still pointing back to the same source.&lt;/p&gt;

&lt;h2&gt;
  
  
  What reward code searches mean for a game page
&lt;/h2&gt;

&lt;p&gt;For readers outside Roblox, a game code is a string a player may redeem for an in-game reward. It is different from the game's source code.&lt;/p&gt;

&lt;p&gt;Of the &lt;strong&gt;1,000 exported queries&lt;/strong&gt;, 753 contained recognizable code or redemption terms, including &lt;code&gt;code&lt;/code&gt;, &lt;code&gt;codes&lt;/code&gt;, &lt;code&gt;โค้ด&lt;/code&gt;, and &lt;code&gt;코드&lt;/code&gt;. Those queries contributed &lt;strong&gt;20,379 of the sample's 22,640 clicks&lt;/strong&gt;, or about 90%.&lt;/p&gt;

&lt;p&gt;Examples included &lt;code&gt;code xúc xắc anime&lt;/code&gt;, an Anime Dice code query in Vietnamese, and &lt;code&gt;code tìm kiếm cây kim&lt;/code&gt;, a Vietnamese query about finding a needle and reward codes.&lt;/p&gt;

&lt;p&gt;This is a keyword classification of the exported sample. Search Console omits anonymized queries and limits table rows, as &lt;a href="https://support.google.com/webmasters/answer/17010575?hl=en" rel="noopener noreferrer"&gt;Google's data-discrepancy documentation&lt;/a&gt; explains. The percentage should stay attached to that sample.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F20m99kms4d6lymg7g7ln.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F20m99kms4d6lymg7g7ln.png" alt=" " width="800" height="639"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;For +1 Loot to Forge, the &lt;a href="https://www.roblox.com/games/118805555015549/1-Loot-To-Forge" rel="noopener noreferrer"&gt;official Roblox description&lt;/a&gt; gives a straightforward gameplay outline: build power by clicking, defeat enemies, collect ores, and forge equipment. The developer is Good Bro Studio.&lt;/p&gt;

&lt;p&gt;That basic loop gives the page context. The code searches suggest a more immediate task: finding reward information a player can actually evaluate.&lt;/p&gt;

&lt;p&gt;An October 7, 2026 source check showed why that requires ongoing attention. The &lt;a href="https://worldplace.ai/games/1-loot-to-forge" rel="noopener noreferrer"&gt;WorldPlace game page&lt;/a&gt; still included a &lt;code&gt;[Races]&lt;/code&gt; title and references to &lt;code&gt;30000CCU&lt;/code&gt; and &lt;code&gt;20000CCU&lt;/code&gt;. The official Roblox page showed &lt;code&gt;[BOSS]&lt;/code&gt; and listed &lt;code&gt;100000CCU&lt;/code&gt; and &lt;code&gt;SORRYBUG&lt;/code&gt; in its description.&lt;/p&gt;

&lt;p&gt;The pages were showing different updates. That called for a content review. It did not establish whether any of the codes still worked.&lt;/p&gt;

&lt;p&gt;For maintaining this information, I would keep three fields distinct:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Source:&lt;/strong&gt; where the claim came from, such as a developer announcement or a community report.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Verification:&lt;/strong&gt; whether it was observed in a source, tested in the game, or reported as no longer working.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Checked time:&lt;/strong&gt; when that particular claim was last reviewed.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A general page update date cannot communicate all three. Refreshing player statistics should not make an older code claim look newly tested.&lt;/p&gt;

&lt;p&gt;For this audience, a useful page should make the game identity, reward information, and supporting source easy to find. A concise gameplay explanation can then help readers continue when codes are unavailable or when they want to understand what to do next.&lt;/p&gt;

&lt;h2&gt;
  
  
  Similar names can belong to different Roblox games
&lt;/h2&gt;

&lt;p&gt;The next two games in the click ranking expose another problem: similar names can make separate records look interchangeable.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Game&lt;/th&gt;
&lt;th&gt;Developer&lt;/th&gt;
&lt;th&gt;Roblox Place ID&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Find the Needle!&lt;/td&gt;
&lt;td&gt;aarush's fruit game&lt;/td&gt;
&lt;td&gt;83238501165241&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Find A Needle in a Haystack!&lt;/td&gt;
&lt;td&gt;CrossJump Games&lt;/td&gt;
&lt;td&gt;93134232675115&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F6b02moqqqxdyykuokbps.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F6b02moqqqxdyykuokbps.png" alt=" " width="799" height="539"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In &lt;a href="https://www.roblox.com/games/83238501165241/Find-the-Needle" rel="noopener noreferrer"&gt;Find the Needle!&lt;/a&gt;, the official description tells players to collect hay, sell it for money, buy upgrades, and find the needle.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fe9yfkanjlnxgcwsm4ub3.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fe9yfkanjlnxgcwsm4ub3.png" alt=" " width="800" height="534"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In &lt;a href="https://www.roblox.com/games/93134232675115/Find-A-Needle-in-a-Haystack" rel="noopener noreferrer"&gt;Find A Needle in a Haystack!&lt;/a&gt;, players pull hay, feed a horse, upgrade tools, and search alone or with up to three friends. Each run begins with a fresh farm and a new needle location.&lt;/p&gt;

&lt;p&gt;The games share a recognizable premise, but their developers, official links, and described systems differ. Combining them could put the wrong instructions, rewards, or statistics on a page.&lt;/p&gt;

&lt;p&gt;That is why I would use explicit game identity for content records and keep name matching as a separate lookup step. A localized title can help find a candidate. The official link and developer help confirm it.&lt;/p&gt;

&lt;p&gt;The same caution applies to analytics. A query containing “needle” may be ambiguous. Separate query and page exports do not establish which page received that query's clicks. An uncertain match should remain uncertain until more specific data resolves it.&lt;/p&gt;

&lt;h2&gt;
  
  
  More impressions do not always mean more clicks
&lt;/h2&gt;

&lt;p&gt;Steal an Egg had the most impressions among the game groups: &lt;strong&gt;131,125&lt;/strong&gt;. It produced &lt;strong&gt;3,690 clicks&lt;/strong&gt;, for a CTR of &lt;strong&gt;2.81%&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;By comparison, +1 Loot to Forge recorded fewer impressions and substantially more clicks.&lt;/p&gt;

&lt;p&gt;That gives me a useful investigation target. I would break Steal an Egg down by language, query type, and position before deciding whether its titles, content, or coverage need to change.&lt;/p&gt;

&lt;p&gt;The broader site trend adds context:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Period&lt;/th&gt;
&lt;th&gt;Search clicks&lt;/th&gt;
&lt;th&gt;Impressions&lt;/th&gt;
&lt;th&gt;CTR&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;September 22–28&lt;/td&gt;
&lt;td&gt;22,372&lt;/td&gt;
&lt;td&gt;212,528&lt;/td&gt;
&lt;td&gt;10.53%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;September 29–October 5&lt;/td&gt;
&lt;td&gt;13,339&lt;/td&gt;
&lt;td&gt;268,180&lt;/td&gt;
&lt;td&gt;4.97%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;In the final week, impressions increased by &lt;strong&gt;26.19%&lt;/strong&gt; while clicks fell by &lt;strong&gt;40.38%&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The site was appearing in more search results, but those appearances were producing fewer clicks overall. The export does not contain daily data for each game, so it cannot identify which game caused the change.&lt;/p&gt;

&lt;p&gt;It does show why a useful review needs both reach and response. A bigger impression count gives me another question to investigate.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where player information becomes creator research
&lt;/h2&gt;

&lt;p&gt;WorldPlace also includes creator tools. The challenge is deciding how those tools fit the needs revealed by search.&lt;/p&gt;

&lt;p&gt;Game detail pages received &lt;strong&gt;99.51% of the exported page clicks&lt;/strong&gt;. The game ideas generator received 181 search clicks across its language versions. There were also explicit queries such as &lt;code&gt;roblox game idea generator&lt;/code&gt;, which contributed 82 clicks in the query export.&lt;/p&gt;

&lt;p&gt;Those figures show an initial search signal for creative work, alongside a much larger entry point through game information. They do not describe what readers did after arriving.&lt;/p&gt;

&lt;p&gt;I would use the search data to choose research priorities, then examine the game's documented mechanics before proposing a new design.&lt;/p&gt;

&lt;p&gt;For +1 Loot to Forge, the documented sequence of gaining power, fighting, collecting materials, and forging suggests a focused question:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Can the first crafting decision offer a tradeoff that a new player understands and notices immediately?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;One possible research brief would be a small workshop prototype with two craftable tools. One tool could improve collection speed; the other could open a different route. The first test would examine whether players understand the choice and can explain why they made it.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://worldplace.ai/tools/game-ideas-generator?game=1-loot-to-forge" rel="noopener noreferrer"&gt;reference-game input in WorldPlace's generator&lt;/a&gt; provides a place to carry game context into that process. For a useful demonstration, I would record the input constraints, the actual output, and the edits that make the idea small enough to test.&lt;/p&gt;

&lt;p&gt;The human decision belongs in the record too. Which suggestion adds a meaningful choice? Which one adds production work? What can a short playtest actually answer?&lt;/p&gt;

&lt;h2&gt;
  
  
  What I want to measure next
&lt;/h2&gt;

&lt;p&gt;The next round of work should connect search entry points with what happens on the site.&lt;/p&gt;

&lt;p&gt;I want to measure how often readers open an official game source, continue to a related game, or enter a creator tool. Within the generator, I would measure three separate steps: opening the page, completing a generation, and saving a concept.&lt;/p&gt;

&lt;p&gt;I also want to compare changes at the same level as the problem. A revised Thai game page needs a view of its relevant queries and search performance, alongside on-site behavior. An increase in total site impressions would not answer whether that particular revision helped.&lt;/p&gt;

&lt;p&gt;For now, +1 Loot to Forge gives me a clear priority: review the localized pages that already bring readers in, make the source and status of changing information easier to understand, and connect deeper research to questions those pages can support.&lt;/p&gt;

&lt;p&gt;The next useful improvement may be a clearer answer on a page someone already needs.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;If you maintain a multilingual directory or content product, how do you keep translated pages in sync when the underlying information changes?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Data source: my WorldPlace Google Search Console export for September 8–October 5, 2026. Page comparisons combine language versions by game slug. CTR is calculated from summed clicks and impressions. Public game descriptions were checked on October 7, 2026.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;AI disclosure: AI tools were used for research, analysis, outlining, and drafting this article. The Search Console data came from my WorldPlace property.&lt;/em&gt;&lt;/p&gt;

</description>
    </item>
    <item>
      <title>Post-Mortem: How My Web Game Hit 130k MAU with $0 Marketing (and Survived an AdSense Ban)</title>
      <dc:creator>Yitao</dc:creator>
      <pubDate>Thu, 28 May 2026 04:12:49 +0000</pubDate>
      <link>https://dev.to/yitao/post-mortem-how-my-web-game-hit-130k-mau-with-0-marketing-and-survived-an-adsense-ban-883</link>
      <guid>https://dev.to/yitao/post-mortem-how-my-web-game-hit-130k-mau-with-0-marketing-and-survived-an-adsense-ban-883</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;A transparent case study on indie game growth: from accidental multi-language localization to a successful B2B pivot in the US market, and scaling revenue after an AdSense restriction.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;For the past few months, I’ve been fighting in the trenches of the indie game market. It has been a wild, stressful, and almost surreal ride. By the end of the 2025 holiday season, one of my projects &lt;a href="https://impostergame.net/" rel="noopener noreferrer"&gt;impostergame.net&lt;/a&gt; hit a milestone of 100,000 Monthly Active Users (MAU). For a developer who has spent years in the "newbie" phase of the global market, this was incredible validation. But the story behind the numbers is far more chaotic than the charts suggest.&lt;/p&gt;

&lt;h2&gt;
  
  
  A Happy Accident: Scaling to 130k MAU
&lt;/h2&gt;

&lt;p&gt;The initial success of this site was largely due to luck. While developing the game, I was using Claude Code. Due to some imprecise prompting on my part, Claude ended up generating a version of the site localized into five different languages.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The UI looked much better than I expected, and I didn't have the heart to roll back the code. So, I took a leap of faith and launched all five languages simultaneously. Usually, my habit was to launch in English first and indefinitely postpone other locales.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That "accidental" decision turned out to be the catalyst for our explosion. Within a month of launch, the Spanish version took off. We saw a massive influx of users from Spain, followed by a surge from Mexico and Latin America. By Christmas and New Year’s, the traffic peaked at 130,000 MAU. Looking back, if I hadn't unintentionally launched with multi-language support, this story wouldn't exist.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fw60xzo3gjber2qaa4b7b.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fw60xzo3gjber2qaa4b7b.png" alt="A surge in traffic peaking at over 130,000 MAU following multi-language launch." width="799" height="418"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The Great Pivot: From Spanish B2C to US B2B
&lt;/h2&gt;

&lt;p&gt;After the holiday craze, the Spanish traffic began its natural decline, dropping to a low of about 80,000 MAU. However, I noticed something interesting: our US-based English traffic was starting to climb.&lt;/p&gt;

&lt;p&gt;To capture this new momentum, I began adding more content and diving deep into SEO optimization. Through analysis with Gemini, I realized my multi-language implementation was riddled with bugs—specifically, my canonical tags were a disaster.&lt;/p&gt;

&lt;p&gt;I spent several rounds of deep-diving with Gemini to "perform surgery" on the code, starting with the English homepage. I optimized the SEO metadata and copy step-by-step, while leaving the high-traffic Spanish pages untouched to avoid any sudden drops.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Unexpected Target Audience&lt;/strong&gt;&lt;br&gt;
As the Spanish traffic waned, I successfully migrated our core audience to the higher-value US market. The ad value (RPM) in the US is 5 to 10 times higher than in the Spanish-speaking regions. Traffic eventually stabilized back around 90,000 MAU, but with a surprising twist: the new users weren't just casual party-goers. They were teachers and HR professionals using the game for classroom education and corporate team-building events.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  The AdSense Nightmare: When "Passive Income" Goes Flat
&lt;/h2&gt;

&lt;p&gt;With high-value traffic stabilized, I naturally applied for Google AdSense. I thought I was on my way to consistent passive income. Instead of "making money while I sleep," AdSense almost made my business "sleep" permanently.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F9rpc3gwunnenxgwglxp6.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F9rpc3gwunnenxgwglxp6.png" alt="The moment passive income dreams were put on hold by AdSense restrictions." width="800" height="196"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Initially, shortly after successfully integrating AdSense, the site was hit with a restriction because the sheer volume of excited users led to rampant accidental clicks. After the account was restored, my daily CPC and revenue began to steadily recover, and the Click-Through Rate (CTR) was maintained at a very safe 2%.&lt;/p&gt;

&lt;p&gt;But then, the "weekend effect" hit. Starting on a Friday, the CTR began to climb. It first approached 2.5% on Friday, and then by Saturday night (the first 6 hours of US time), it skyrocketed past 3%!&lt;/p&gt;

&lt;p&gt;The reason? Friction. In a fast-paced browser game, users are anxious to start playing. When ads pop up and interrupt the flow, accidental clicks become inevitable. These "accidental clicks" are flagged by Google as Invalid Traffic.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fp23scdjn6tg0922icvc1.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fp23scdjn6tg0922icvc1.png" alt="The first bittersweet payment of $145.66 received during the restriction period." width="428" height="235"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Despite my emergency efforts to reduce ad density and increase spacing, the AdSense penalty arrived. My account was restricted for the second time. Ironically, the very next morning—while my account was under restriction—I received a notification from my bank. My first-ever AdSense payment of $145.66 had arrived. It was my first taste of passive income from the global market, yet I felt no joy. Knowing my revenue had flatlined again left me in a state of constant anxiety.&lt;/p&gt;

&lt;h2&gt;
  
  
  The AI Intervention and the 80-Email Comeback
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;"In the depths of this frustration, I started questioning if the site was even worth keeping. I asked Gemini, "Based on my current traffic, what is the estimated valuation of this site if I were to sell it?""&lt;br&gt;
Gemini: Gemini analyzed the data and gave me a surprisingly firm answer: "If I were you, I absolutely wouldn't sell right now." Instead, it recommended moving away from AdSense and looking for a gaming-specific ad platform.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ferajqze90s9pz89en0lv.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ferajqze90s9pz89en0lv.png" alt="Gemini's game-changing advice to hold and pivot to specialized ad platforms." width="800" height="438"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;That was the wake-up call I needed. I abandoned AdSense and reached out to Venatus, a premium ad network for the gaming industry. Over the next week, I exchanged nearly 80 emails with their account managers to get the integration right.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;$1,279.55 Revenue&lt;/strong&gt;&lt;br&gt;
We got it live just before the Spring Festival. Since then, revenue has stabilized. Even as traffic naturally dipped to 50,000 MAU after the peak season, the site generated $1,279.55 over the last three months. As the Venatus "onboarding" period ends and eCPMs improve, there is significant room for growth.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fx2hnqsoij0wnln73g7p7.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fx2hnqsoij0wnln73g7p7.png" alt="Revenue recovery and stability through the Venatus gaming ad network." width="799" height="294"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Long-termism: The Future of Web Games
&lt;/h2&gt;

&lt;p&gt;Reflecting on this "roller-coaster" journey, I’ve realized that web-based games still have incredible vitality in today’s ecosystem.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Frictionless is a Superpower: The "no download, just play" nature of web games is a massive discoverability weapon. It allowed me to leverage SEO and accidental localization to reach 130k users with zero marketing spend.&lt;/li&gt;
&lt;li&gt;Intent Matters More Than Volume: Raw traffic is great, but the intent of that traffic defines your ceiling. Moving from "pure entertainment" (Spanish casual users) to "utility/tool" (US educators and HR) is what gave the product long-term sustainability.&lt;/li&gt;
&lt;li&gt;Built for Resilience: A monetization system must be built on a deep understanding of user behavior. Moving from the AdSense nightmare to a specialized partner like Venatus taught me that the urge to maximize short-term revenue must always be balanced against platform rules and user experience.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Today, &lt;a href="https://impostergame.net/" rel="noopener noreferrer"&gt;ImposterGame&lt;/a&gt; covers its server costs and generates a healthy profit through ads and a small number of subscriptions. This is just the beginning. For indie devs, real success isn't just about going viral once—it’s about building a stable, compliant, and sustainable business loop in a volatile market.&lt;/p&gt;

&lt;p&gt;I hope these lessons help other developers exploring non-traditional growth paths. If you want to experience the frictionless web game mentioned in this case study, try a round with your friends at &lt;a href="https://impostergame.net/" rel="noopener noreferrer"&gt;ImposterGame.net&lt;/a&gt;. You can also find more of my technical deep-dives on my &lt;a href="https://impostergame.net/devlog/130k-mau-zero-marketing" rel="noopener noreferrer"&gt;personal devlog&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>indiedev</category>
      <category>growthhacking</category>
      <category>webmonetization</category>
      <category>buildinpublic</category>
    </item>
    <item>
      <title>Next.js 15 + Supabase: I Accidentally Blew Past My Quota by 1000% (and How “Local‑First” Saved It)</title>
      <dc:creator>Yitao</dc:creator>
      <pubDate>Fri, 30 Jan 2026 06:25:47 +0000</pubDate>
      <link>https://dev.to/yitao/nextjs-15-supabase-i-accidentally-blew-past-my-quota-by-1000-and-how-local-first-saved-it-1leh</link>
      <guid>https://dev.to/yitao/nextjs-15-supabase-i-accidentally-blew-past-my-quota-by-1000-and-how-local-first-saved-it-1leh</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;TL;DR&lt;/strong&gt;: My “perfectly reasonable” real-time architecture for an online party game turned into a billing horror story — first with Supabase Realtime broadcasts, then with Redis + polling. The fix wasn’t “optimize the server.” It was &lt;strong&gt;stop needing the server&lt;/strong&gt; for the common use case.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  1. The morning my dashboard tried to jump-scare me
&lt;/h2&gt;

&lt;p&gt;One morning I woke up to warning emails from Supabase and Vercel:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;“Your project has significantly exceeded its Realtime message quota.”&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fkbyi1f5mlff39pkvjy8i.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fkbyi1f5mlff39pkvjy8i.jpg" alt=" " width="788" height="484"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I opened the dashboard and had to re-check that I was looking at the right project.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;In just a 10-day span&lt;/strong&gt;, Supabase Realtime (Broadcast + Presence) had processed roughly &lt;strong&gt;50,000,000 messages&lt;/strong&gt; (the bill line item showed &lt;strong&gt;54,557,731&lt;/strong&gt; Realtime Messages).&lt;br&gt;&lt;br&gt;
That wasn’t “a little over.” It was &lt;strong&gt;1000%+ over&lt;/strong&gt; the included quota.&lt;/p&gt;

&lt;p&gt;For context: I run &lt;strong&gt;&lt;a href="https://impostergame.net" rel="noopener noreferrer"&gt;Imposter Game&lt;/a&gt;&lt;/strong&gt; — a browser-based party game (think “Liar Game” / social deduction) that works with &lt;strong&gt;3 to 99 players&lt;/strong&gt;. No installs, no logins — just open a URL and play.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fkyqb4ijms34d1dsx33vq.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fkyqb4ijms34d1dsx33vq.jpg" alt=" " width="799" height="620"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;User growth is great… until your side project starts throwing punches at your wallet.&lt;/p&gt;


&lt;h2&gt;
  
  
  2. Technical post-mortem: two failures back-to-back
&lt;/h2&gt;
&lt;h3&gt;
  
  
  The stack
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Framework&lt;/strong&gt;: Next.js 15 (App Router)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Database&lt;/strong&gt;: Supabase (Postgres)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Realtime&lt;/strong&gt;: Supabase Realtime (Broadcast + Presence)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;State&lt;/strong&gt;: Upstash Redis (Vercel KV)&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;
  
  
  Failure 1: Broadcast everything (Supabase Realtime)
&lt;/h3&gt;

&lt;p&gt;My first approach was the classic “real-time multiplayer” instinct:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Any state change? &lt;strong&gt;Broadcast it immediately.&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Timer? &lt;strong&gt;Send updates every second.&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Presence? &lt;strong&gt;Track joins/leaves live.&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Here’s the core math that bit me: Supabase charges on &lt;strong&gt;egress messages&lt;/strong&gt; — effectively:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;1 event × number of subscribers in the room (N)&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;So with &lt;strong&gt;N = 50&lt;/strong&gt; players:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Every second: &lt;code&gt;1 timer tick × 50 recipients = 50 messages/sec&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;One 15-minute round (900 sec): &lt;code&gt;50 × 900 = 45,000 messages&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Add votes, reactions, and Presence traffic…and the number explodes.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Result: &lt;strong&gt;~50M messages in about 10 days&lt;/strong&gt;, quota obliterated.&lt;/p&gt;
&lt;h3&gt;
  
  
  Failure 2: “Fine, I’ll use Redis + polling”
&lt;/h3&gt;

&lt;p&gt;My next thought was also extremely common:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;“Realtime is expensive. Let’s store state in Redis and have clients poll.”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;So I turned off the broadcast approach and switched to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;State stored in Upstash Redis&lt;/li&gt;
&lt;li&gt;Client polls &lt;code&gt;GET /api/game-state&lt;/code&gt; &lt;strong&gt;once per second&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This looked “cheaper” in my head. It wasn’t.&lt;/p&gt;

&lt;p&gt;If each poll triggers ~3 Redis commands (Room/Round/Player):&lt;/p&gt;

&lt;p&gt;&lt;code&gt;10 concurrent users × 1 poll/sec × 3 commands = 1,800 commands/min&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;And Upstash’ &lt;strong&gt;free monthly quota (500k commands)&lt;/strong&gt;?&lt;br&gt;&lt;br&gt;
It evaporated &lt;strong&gt;in less than half a day&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;I ended up adding a credit card for &lt;strong&gt;Pay As You Go&lt;/strong&gt; just to keep the app alive.&lt;/p&gt;

&lt;p&gt;At that point I had to admit it:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;“Congrats. I just wrote my own DDoS script.”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;(Also: Vercel and Upstash being in different regions increased RTT and made the whole thing feel even worse.)&lt;/p&gt;


&lt;h2&gt;
  
  
  3. The real realization: I was solving the wrong problem
&lt;/h2&gt;

&lt;p&gt;My initial “solutions” were all server-side optimizations:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;batch Redis reads (&lt;code&gt;MGET&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;reduce timer update frequency (1s → 5s)&lt;/li&gt;
&lt;li&gt;compress payloads&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Then I paused and pictured the real-world usage.&lt;/p&gt;

&lt;p&gt;Most people play party games… &lt;strong&gt;in the same room&lt;/strong&gt;, &lt;strong&gt;around the same table&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;So why were 10 friends at a campsite burning LTE data and battery life, constantly syncing with a server across the planet?&lt;/p&gt;

&lt;p&gt;The problem wasn’t “how do I scale my server cheaper?”&lt;/p&gt;

&lt;p&gt;It was:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;“How do I remove the server from the default experience?”&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;


&lt;h2&gt;
  
  
  4. The pivot: Local‑First, client‑only (“no server” mode)
&lt;/h2&gt;

&lt;p&gt;I made a bold call:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;For in-person play, don’t use the network at all.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Not “serverless.” Not “edge.”&lt;br&gt;&lt;br&gt;
Just &lt;strong&gt;0 API calls&lt;/strong&gt;.&lt;/p&gt;
&lt;h3&gt;
  
  
  New architecture: client-only pass-and-play
&lt;/h3&gt;

&lt;p&gt;One phone acts as the host. Players pass the device around to confirm roles (“pass and play”), then play together locally.&lt;/p&gt;

&lt;p&gt;The Local Mode component is a &lt;code&gt;use client&lt;/code&gt; Next.js client component, but internally it behaves like a little state machine.&lt;/p&gt;
&lt;h3&gt;
  
  
  Local timer (no drift, no server)
&lt;/h3&gt;

&lt;p&gt;Instead of server &lt;code&gt;setInterval&lt;/code&gt;, I use &lt;code&gt;requestAnimationFrame&lt;/code&gt; + &lt;code&gt;Date.now()&lt;/code&gt; to compute time-left deterministically.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// useGameTimer.ts (simplified)&lt;/span&gt;
&lt;span class="nf"&gt;useEffect&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;startTime&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="na"&gt;animationFrameId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;tick&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;elapsed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;floor&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;startTime&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;newTimeLeft&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;duration&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;elapsed&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="nf"&gt;setTimeLeft&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;newTimeLeft&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;newTimeLeft&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nx"&gt;animationFrameId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;requestAnimationFrame&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;tick&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;};&lt;/span&gt;

    &lt;span class="nx"&gt;animationFrameId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;requestAnimationFrame&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;tick&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;cancelAnimationFrame&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;animationFrameId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;duration&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;em&gt;(Yes, background tabs have constraints — but this is optimized for in-person local play, where the app stays in the foreground.)&lt;/em&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  State transitions happen in memory
&lt;/h3&gt;

&lt;p&gt;Role assignment (3–99 players), voting, win conditions — everything runs in browser memory.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;nextPhase&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;setGame&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;prev&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;prev&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;phase&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;voting&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;calculateWinner&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;prev&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;votes&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// local compute&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;prev&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;phase&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;result&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;winner&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="c1"&gt;// ...&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No network round trip means phase transitions feel instant.&lt;/p&gt;

&lt;h3&gt;
  
  
  INP optimization for 99 players
&lt;/h3&gt;

&lt;p&gt;Rendering and updating a 99-player list can get janky fast.&lt;/p&gt;

&lt;p&gt;React 18’s &lt;code&gt;useTransition&lt;/code&gt; helped keep heavy updates non-blocking:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;addPlayer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;startTransition&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nf"&gt;setGame&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;prev&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;newPlayers&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[...&lt;/span&gt;&lt;span class="nx"&gt;prev&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;players&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;createNewPlayer&lt;/span&gt;&lt;span class="p"&gt;()];&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;balanceRoles&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;newPlayers&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Security note (because someone will ask)
&lt;/h3&gt;

&lt;p&gt;Online mode still requires server-side validation.&lt;/p&gt;

&lt;p&gt;But in Local Mode, the person holding the phone is effectively authenticated by physics.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Your friends’ eyeballs are the anti-cheat.&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Result: costs down, UX up
&lt;/h2&gt;

&lt;p&gt;I restructured the site:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Local Game (Offline Mode)&lt;/strong&gt; became the main CTA on the homepage.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Online Game&lt;/strong&gt; stayed as a backup feature (“Remote Mode”).&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  What changed
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Realtime messages&lt;/strong&gt;: ~50M → &lt;strong&gt;near zero&lt;/strong&gt; (because most sessions moved to Local Mode)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Redis usage&lt;/strong&gt;: easily kept within free/low tiers&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reliability&lt;/strong&gt;: no more games dying due to disconnects (even in basements, mountains, and spotty areas)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The best part: users &lt;em&gt;preferred&lt;/em&gt; the version with no installs, no logins, and no dependency on good internet.&lt;/p&gt;




&lt;h2&gt;
  
  
  Takeaway
&lt;/h2&gt;

&lt;p&gt;As developers, we’re often drawn to “real-time,” “websockets,” and “edge everything.”&lt;/p&gt;

&lt;p&gt;But the best scaling strategy I’ve learned recently is:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Don’t optimize the server — make the server unnecessary.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Sometimes an &lt;code&gt;Array.map&lt;/code&gt; beats a Redis cluster.&lt;/p&gt;




&lt;h3&gt;
  
  
  Try it
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ffi4waqgxtezjgj21llr1.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ffi4waqgxtezjgj21llr1.jpg" alt=" " width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;👉 &lt;strong&gt;&lt;a href="https://impostergame.net" rel="noopener noreferrer"&gt;Play Imposter Game&lt;/a&gt;&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
It’s a real demo of a smooth 99-player local party game built with React — no app install required.&lt;/p&gt;

</description>
      <category>nextjs</category>
      <category>webdev</category>
      <category>react</category>
      <category>impostergame</category>
    </item>
  </channel>
</rss>
