<?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: Voland</title>
    <description>The latest articles on DEV Community by Voland (@voland_hds).</description>
    <link>https://dev.to/voland_hds</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.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3875496%2F8204ab34-bcd5-44e7-8fa0-d7f382aaa442.png</url>
      <title>DEV Community: Voland</title>
      <link>https://dev.to/voland_hds</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/voland_hds"/>
    <language>en</language>
    <item>
      <title>I built a rank platform that starts where Steam achievements end</title>
      <dc:creator>Voland</dc:creator>
      <pubDate>Sun, 12 Apr 2026 20:54:55 +0000</pubDate>
      <link>https://dev.to/voland_hds/i-built-a-rank-platform-that-starts-where-steam-achievements-end-5c8m</link>
      <guid>https://dev.to/voland_hds/i-built-a-rank-platform-that-starts-where-steam-achievements-end-5c8m</guid>
      <description>&lt;p&gt;I have loved video games my entire life.&lt;br&gt;
My way of honoring a game was always the same: get 100%.&lt;br&gt;
Every achievement. Every collectible. Every secret.&lt;br&gt;
But after the last achievement popped one day, I felt something&lt;br&gt;
I didn't expect. Emptiness. The game was over. Nowhere left to go.&lt;br&gt;
I looked around. I had done something genuinely hard.&lt;br&gt;
But where could I show it? Nobody cared about a Steam profile screenshot.&lt;br&gt;
So I asked: what if 100% is not the end of a game, but only the beginning?&lt;br&gt;
That question became Pantheon HDS.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How it works&lt;/strong&gt;&lt;br&gt;
You connect Steam. You get an automatic rank based on your achievement&lt;br&gt;
completion. Then you earn higher ranks by completing community-created&lt;br&gt;
challenges, verified by real judges through anonymous blind voting.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Bronze -&amp;gt; Silver -&amp;gt; Gold   (achievement-based, automatic)
              |
   Complete community challenges
   Verified by real judges
              |
Platinum -&amp;gt; Diamond -&amp;gt; Master -&amp;gt; Grandmaster
              |
           Legend
    Community vote only. Forever.
No money. No algorithms. Just skill.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;How it's built&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;React 19 + TypeScript 5, Vite, TanStack Query v5.&lt;br&gt;
Backend is Supabase: PostgreSQL with RLS on every table,&lt;br&gt;
Edge Functions on Deno for server-side logic.&lt;br&gt;
Deployed on Vercel. Auth is Steam OpenID only.&lt;br&gt;
No passwords. No emails. Steam is already the source of truth&lt;br&gt;
for achievement data, so it made sense to use it for identity too.&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;Anonymous blind voting.&lt;/strong&gt;&lt;br&gt;
Judges are assigned randomly. They don't see the player's name.&lt;br&gt;
The player doesn't know who's judging.&lt;br&gt;
No social pressure, no favoritism. Verdicts stay honest.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;JWT with server-side revocation.&lt;/strong&gt;&lt;br&gt;
Tokens live in localStorage. On logout, the token is killed&lt;br&gt;
via an Edge Function. Leaked token after logout is useless.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;handleLogout&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;token&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nx"&gt;localStorage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;pantheon_user&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;{}&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
  &lt;span class="p"&gt;)?.&lt;/span&gt;&lt;span class="nx"&gt;token&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;token&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nf"&gt;revokeToken&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;token&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// fire and forget&lt;/span&gt;
  &lt;span class="nx"&gt;localStorage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;removeItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;pantheon_user&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nf"&gt;navigate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;One file owns all rank data.&lt;/strong&gt;&lt;br&gt;
Tier strings, colors, ordering — &lt;em&gt;all in constants/ranks.ts.&lt;/em&gt;&lt;br&gt;
Nothing hardcoded elsewhere. Refactoring rank logic&lt;br&gt;
never turns into grep-and-pray.&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="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;RANK_TIERS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Legend&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Grandmaster&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Master&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Diamond&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Platinum&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Gold&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Silver III&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Silver II&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Silver I&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Bronze III&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Bronze II&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Bronze I&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="k"&gt;as&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;RankTier&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nx"&gt;RANK_TIERS&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Lazy loading gated pages.&lt;/strong&gt;&lt;br&gt;
Dashboard, Admin, JudgePanel are lazy-loaded.&lt;br&gt;
Visitors who never log in don't pay for that code.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;Dashboard&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;lazy&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;import&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./components/pages/Dashboard&lt;/span&gt;&lt;span class="dl"&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;Admin&lt;/span&gt;      &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;lazy&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;import&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./components/pages/Admin&lt;/span&gt;&lt;span class="dl"&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;JudgePanel&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;lazy&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;import&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./components/pages/JudgePanel&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;What's live&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Steam OpenID auth&lt;/li&gt;
&lt;li&gt;Automatic rank assignment via Steam API&lt;/li&gt;
&lt;li&gt;Challenge submission and judge voting&lt;/li&gt;
&lt;li&gt;Anonymous blind voting&lt;/li&gt;
&lt;li&gt;Admin panel&lt;/li&gt;
&lt;li&gt;Public profiles at /u/username&lt;/li&gt;
&lt;li&gt;Playwright E2E + Vitest unit tests&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;What we will never do&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Sell ranks for money&lt;/li&gt;
&lt;li&gt;Use AI to verify Legend rank. Humans only, always.&lt;/li&gt;
&lt;li&gt;Sell the platform without a community vote&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These are the three principles. They don't change.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;It's open source&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;GitHub: &lt;a href="https://github.com/pantheon-hds/core" rel="noopener noreferrer"&gt;https://github.com/pantheon-hds/core&lt;/a&gt;&lt;br&gt;
Live: &lt;a href="https://pantheonhds.com" rel="noopener noreferrer"&gt;https://pantheonhds.com&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If you've ever gotten 100% in a game and wondered "now what?" —&lt;br&gt;
this was built for you.&lt;/p&gt;

</description>
      <category>react</category>
      <category>typescript</category>
      <category>opensource</category>
      <category>gamedev</category>
    </item>
  </channel>
</rss>
