<?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: PixelForge</title>
    <description>The latest articles on DEV Community by PixelForge (@pixelforge).</description>
    <link>https://dev.to/pixelforge</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%2F4202530%2F1d1f1ac8-74e3-4b35-b389-e64ca3ca1722.jpg</url>
      <title>DEV Community: PixelForge</title>
      <link>https://dev.to/pixelforge</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/pixelforge"/>
    <language>en</language>
    <item>
      <title>I open-sourced the architecture of my Discord ticket bot. One dependency, zero database, files as modules</title>
      <dc:creator>PixelForge</dc:creator>
      <pubDate>Fri, 23 Oct 2026 12:09:03 +0000</pubDate>
      <link>https://dev.to/pixelforge/i-open-sourced-the-architecture-of-my-discord-ticket-bot-one-dependency-zero-database-files-as-1jf2</link>
      <guid>https://dev.to/pixelforge/i-open-sourced-the-architecture-of-my-discord-ticket-bot-one-dependency-zero-database-files-as-1jf2</guid>
      <description>&lt;p&gt;Most "support ticket" setups on Discord are a subscription: a hosted bot, a monthly plan, a dashboard you don't control. I wrote a ticket + moderation bot called ARBITER that goes the other way. You run it, you own it, forever. One &lt;code&gt;npm install&lt;/code&gt;, one config file, no database.&lt;/p&gt;

&lt;p&gt;This post is the architecture story: the five decisions that make a self-hosted bot pleasant to own instead of a liability.&lt;/p&gt;

&lt;h2&gt;
  
  
  Decision 1: One dependency, on purpose
&lt;/h2&gt;

&lt;p&gt;The bot's entire dependency list is &lt;code&gt;discord.js&lt;/code&gt;. That's not a flex about minimalism. It's a support-load decision. Every dependency is a thing a buyer on a $4 VPS can break: native modules fail to compile, lockfiles fight, Node versions drift. A single, pure-JS, actively maintained dependency is the difference between "runs anywhere" and "runs on my machine".&lt;/p&gt;

&lt;p&gt;If you're building a tool people will self-host, count your dependencies as a cost, not a capability.&lt;/p&gt;

&lt;h2&gt;
  
  
  Decision 2: JSON over SQLite
&lt;/h2&gt;

&lt;p&gt;Ticket counters and warning history live in &lt;code&gt;data/*.json&lt;/code&gt;, written atomically: write a temp file, &lt;code&gt;rename()&lt;/code&gt; over the real one. On any crash, you either have the old file or the new one, never a half-written one.&lt;/p&gt;

&lt;p&gt;I'm not going to pretend JSON beats a real database at scale. It doesn't need to. A community server accumulates maybe a few hundred warnings a year. For that volume, the wins are concrete:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;data is human-readable, so owners can audit their own files with a text editor&lt;/li&gt;
&lt;li&gt;no schema migrations, ever&lt;/li&gt;
&lt;li&gt;no native modules to break on weird architectures&lt;/li&gt;
&lt;li&gt;backups are literally copying a folder&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Choosing the storage engine that matches the actual data volume, not the volume you wish you had, keeps the whole program small enough to reason about.&lt;/p&gt;

&lt;h2&gt;
  
  
  Decision 3: File-per-command
&lt;/h2&gt;

&lt;p&gt;Every slash command is one small file with the same shape: metadata, permission checks, an &lt;code&gt;execute&lt;/code&gt; function. Adding a command is adding a file. The README says exactly that, and means it.&lt;/p&gt;

&lt;p&gt;The consistency is the feature. When every file has the same shape, onboarding a new maintainer (or your future self, six months later) is pattern-matching, not archaeology. It also keeps diffs honest in code review: a change to &lt;code&gt;/warn&lt;/code&gt; is a change to one file.&lt;/p&gt;

&lt;h2&gt;
  
  
  Decision 4: Custom interaction IDs
&lt;/h2&gt;

&lt;p&gt;Buttons carry IDs like &lt;code&gt;arbiter:ticket:open&lt;/code&gt; instead of raw strings like &lt;code&gt;open&lt;/code&gt;. The namespace looks cosmetic. It isn't. Panels get cross-posted, reactions get chain-wired, and on busy servers other bots' components end up on the same messages. The prefix guarantees the bot only ever answers its own buttons, and that a collision surfaces as "ignored", never as "misfire".&lt;/p&gt;

&lt;p&gt;Small systems touch. Big systems regret skipping them.&lt;/p&gt;

&lt;h2&gt;
  
  
  Decision 5: Errors that teach
&lt;/h2&gt;

&lt;p&gt;Every failure message names the likely cause: role hierarchy, missing permission, Discord's 14-day purge window. A bot that says "error" teaches nothing; a bot that says "I can't time out a moderator above me" turns every failure into the owner learning the system. Support volume drops when the error messages do the supporting.&lt;/p&gt;

&lt;h2&gt;
  
  
  The part that sells it to owners
&lt;/h2&gt;

&lt;p&gt;On ticket close, the bot posts a transcript to the log channel: a self-contained dark-theme HTML file with avatars, timestamps, attachments. Inline CSS, no CDN, renders in any browser. The archive outlives the channel it came from, readable in five years without running anything.&lt;/p&gt;

&lt;h2&gt;
  
  
  Want the whole bot?
&lt;/h2&gt;

&lt;p&gt;Full source, one dependency, zero database at &lt;a href="https://pixelforgehq.sell.app/product/arbiter-discord-bot" rel="noopener noreferrer"&gt;pixelforgehq.sell.app/product/arbiter-discord-bot&lt;/a&gt;. It's $12.99 one-time, and the code LAUNCH20 takes 20% off until Nov 5 (the price rises after the first reviews). &lt;code&gt;config.example.json&lt;/code&gt; included: paste your token, run it.&lt;/p&gt;

&lt;p&gt;The patterns above are free, though. One dependency you can defend, storage that matches your real data volume, same-shaped files, namespaced IDs, errors that teach. Steal them for your next bot; they'll hold up.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Indie dev, building in public. More admin tools at pixelforgehq.sell.app.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>indiehackers</category>
    </item>
    <item>
      <title>Your server is online. Your website says nothing. The quiet engineering of a live status widget</title>
      <dc:creator>PixelForge</dc:creator>
      <pubDate>Fri, 23 Oct 2026 12:01:58 +0000</pubDate>
      <link>https://dev.to/pixelforge/your-server-is-online-your-website-says-nothing-the-quiet-engineering-of-a-live-status-widget-5e74</link>
      <guid>https://dev.to/pixelforge/your-server-is-online-your-website-says-nothing-the-quiet-engineering-of-a-live-status-widget-5e74</guid>
      <description>&lt;p&gt;Here's a pattern I keep meeting: a Minecraft community spends weeks on their server, gets the gameplay tight, the Discord busy, and their website is still a static poster. No signal about whether &lt;em&gt;anyone is playing right now&lt;/em&gt;. Visitors can't tell a dead server from a healthy one, so they assume the worst and bounce.&lt;/p&gt;

&lt;p&gt;The fix looks trivial ("add a status badge!") and almost everyone implements it wrong. I recently shipped a server-site template called OBSIDIAN, and the status widget ended up being the part I thought about the hardest. This post is the write-up.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why most status badges lie
&lt;/h2&gt;

&lt;p&gt;The naive implementation polls the server, and when the poll fails it shows &lt;strong&gt;0 players&lt;/strong&gt;. That's a lie in both directions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;API unreachable? The server might be perfectly up. You're showing "0 players online" on a page whose real message is &lt;em&gt;dead&lt;/em&gt;.&lt;/li&gt;
&lt;li&gt;Server restarting? "0 players" reads as "dead server" for the 90 seconds it takes to boot back.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Users don't read "0" as a number. They read it as a verdict.&lt;/p&gt;

&lt;p&gt;The honest version needs &lt;strong&gt;four states&lt;/strong&gt;, not two: checking, online, offline, and a separate &lt;em&gt;can't reach the status service&lt;/em&gt; state that explicitly refuses to guess. That fourth state is the unpopular one: it admits "I don't know" on your own production page. But it's the only configuration that never lies.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where the query actually lives
&lt;/h2&gt;

&lt;p&gt;Raw Minecraft status isn't HTTP. Java Edition answers a handshake over TCP; Bedrock speaks RakNet, a UDP protocol with its own packet framing. A static page can't speak either from the browser: no raw TCP/UDP in a JS context, by design.&lt;/p&gt;

&lt;p&gt;So the honest architecture is a two-hop: the page calls a public status API that performs the protocol handshake server-side and returns JSON. &lt;code&gt;api.mcsrvstat.us&lt;/code&gt; is free, needs no key, and speaks both editions. You give it your address, it resolves Java vs Bedrock and returns player counts, version, MOTD.&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;res&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://api.mcsrvstat.us/2/&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nf"&gt;encodeURIComponent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;address&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;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="c1"&gt;// data.online is the only field you trust for state&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Everything downstream flows from &lt;code&gt;data.online&lt;/code&gt;. Never from a player count, never from a catch block.&lt;/p&gt;

&lt;h2&gt;
  
  
  The state machine I ended up with
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;check() -&amp;gt; [checking] -&amp;gt; ok?  -&amp;gt; [online]  show players, version, MOTD
                     -&amp;gt; no?  -&amp;gt; [offline] "Server is offline" (plain, no shame)
           network error    -&amp;gt; [error]    "Can't reach the status service.
                                         The server may still be up."
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two rules make it behave under real traffic:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Never cache "online" optimistically.&lt;/strong&gt; If the API says offline, show offline &lt;em&gt;now&lt;/em&gt;; visitors respect a page that updates honestly.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The error state names the failure honestly.&lt;/strong&gt; "Could not reach the status service" tells the owner to check the API, not their server. Ambiguity here generates support questions about the wrong problem.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  One config file, because owners aren't developers
&lt;/h2&gt;

&lt;p&gt;The widget ships inside a site template, so the whole thing is driven by one file (&lt;code&gt;js/config.js&lt;/code&gt;): server name, address, Discord invite, staff list. The owner never edits logic. This matters more than any technical choice in the stack. A config typo is a question I can answer in one line; a broken &lt;code&gt;app.js&lt;/code&gt; edit is a debugging session neither of us wants.&lt;/p&gt;

&lt;p&gt;The design half of the story: the live numbers get the only warm accent color on the page. One lamp in a dark cave. And when the server is offline, the number doesn't turn red. Red says &lt;em&gt;emergency&lt;/em&gt;, and an offline hobby server isn't one. It dims to a quiet neutral. The page stays honest without yelling.&lt;/p&gt;

&lt;h2&gt;
  
  
  If you want the whole thing
&lt;/h2&gt;

&lt;p&gt;OBSIDIAN is the full template: status widget, one-click copy-IP bar, responsive layout, and a design doc explaining every decision at &lt;a href="https://pixelforgehq.sell.app/product/obsidian-mc-template" rel="noopener noreferrer"&gt;pixelforgehq.sell.app/product/obsidian-mc-template&lt;/a&gt;. It's $9.99, and the code LAUNCH20 takes 20% off until Nov 5 (the price rises after the first reviews). The demo runs the widget live against a real server: &lt;a href="https://obsidianmc.vercel.app" rel="noopener noreferrer"&gt;obsidianmc.vercel.app&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;But honestly, the four-state pattern costs nothing to copy into your own page. Poll &lt;code&gt;data.online&lt;/code&gt;, never trust player counts, refuse to guess when the chain breaks. Your website stops lying, and your players notice.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Indie dev, building in public. More admin tools at pixelforgehq.sell.app.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>frontend</category>
      <category>softwaredevelopment</category>
      <category>webdev</category>
    </item>
    <item>
      <title>I built a live MOTD designer for Minecraft servers and learned the color code system properly</title>
      <dc:creator>PixelForge</dc:creator>
      <pubDate>Fri, 23 Oct 2026 11:58:41 +0000</pubDate>
      <link>https://dev.to/pixelforge/i-built-a-live-motd-designer-for-minecraft-servers-and-learned-the-color-code-system-properly-4p6a</link>
      <guid>https://dev.to/pixelforge/i-built-a-live-motd-designer-for-minecraft-servers-and-learned-the-color-code-system-properly-4p6a</guid>
      <description>&lt;p&gt;Your server's MOTD has about two seconds to earn a click in the server list. Most of us edit it blind: change &lt;code&gt;motd=&lt;/code&gt; in &lt;code&gt;server.properties&lt;/code&gt;, restart, open the multiplayer screen, squint, repeat. I did this loop for two years before I finally sat down and learned how the formatting system actually works. Then I built a designer tool around it so nobody has to do the blind loop again.&lt;/p&gt;

&lt;h2&gt;
  
  
  The 30-second version of Minecraft color codes
&lt;/h2&gt;

&lt;p&gt;Formatting in MOTDs (and chat, and item names) runs on a single control character: &lt;code&gt;§&lt;/code&gt; (U+00A7). Whatever follows it is an instruction, not text:&lt;/p&gt;

&lt;p&gt;| Codes | Effect || Codes | Effect |&lt;br&gt;
|---|---||---|---|&lt;br&gt;
| &lt;code&gt;§0&lt;/code&gt;-&lt;code&gt;§9&lt;/code&gt; | dark colors || &lt;code&gt;§a&lt;/code&gt;-&lt;code&gt;§f&lt;/code&gt; | bright colors |&lt;br&gt;
| &lt;code&gt;§l&lt;/code&gt; | bold || &lt;code&gt;§o&lt;/code&gt; | italic |&lt;br&gt;
| &lt;code&gt;§n&lt;/code&gt; | underline || &lt;code&gt;§m&lt;/code&gt; | strikethrough |&lt;br&gt;
| &lt;code&gt;§k&lt;/code&gt; | obfuscated || &lt;code&gt;§r&lt;/code&gt; | reset |&lt;/p&gt;

&lt;p&gt;Sixteen colors, six formatters, one reset. The palette is fixed by the game: you can't invent new ones in &lt;code&gt;server.properties&lt;/code&gt;. What you &lt;em&gt;can&lt;/em&gt; do, on 1.16+, is go hex:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;§#ff6601Happy Hour §r- join now
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;§#rrggbb&lt;/code&gt; gives you any of 16.7M colors, and it survives the server list. (Bedrock accepts the codes but renders a slightly different palette, so test on your version.)&lt;/p&gt;

&lt;h2&gt;
  
  
  The three gotchas that cost me an evening
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. The two-line limit is real.&lt;/strong&gt; &lt;code&gt;server.properties&lt;/code&gt; MOTD supports &lt;code&gt;\n&lt;/code&gt; for a second line. A third line doesn't exist in the list, and no client-side mod changes that, because the list is rendered client-side.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Color carries over.&lt;/strong&gt; After &lt;code&gt;§6&lt;/code&gt;, everything stays orange until you &lt;code&gt;§r&lt;/code&gt; or pick a new color. Most "broken" MOTDs I see are missing resets: the second line inherits the first line's formatting, and bold spreads across the dash you thought was plain.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Your editor is probably lying to you.&lt;/strong&gt; The multiplayer list renders in a specific font with its own row layout. A MOTD that looks fine in a generic preview can wrap or clip in the real list. The only preview you can trust is one that reproduces the actual list row.&lt;/p&gt;

&lt;h2&gt;
  
  
  So I built the tool I wanted to exist
&lt;/h2&gt;

&lt;p&gt;One HTML file. Type on the left, a faithful server-list preview updates live on the right: same row layout, same 16-color palette, obfuscated text actually animating. All codes are one click away in a palette, hex included. When it looks right, copy the codes into &lt;code&gt;server.properties&lt;/code&gt; and you're done.&lt;/p&gt;

&lt;p&gt;It's called &lt;strong&gt;MOTD Studio&lt;/strong&gt;. It's $7.99, and the code LAUNCH20 takes 20% off until Nov 5 (the price rises after the first reviews). The demo is fully functional: &lt;a href="https://pixelforgehq.sell.app/product/motd-studio" rel="noopener noreferrer"&gt;pixelforgehq.sell.app/product/motd-studio&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  If you'd rather stay vanilla
&lt;/h2&gt;

&lt;p&gt;You honestly don't need my tool. The table above is 90% of the system. Keep three rules and you're fine:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;end every colored segment with &lt;code&gt;§r&lt;/code&gt; unless you &lt;em&gt;want&lt;/em&gt; carry-over&lt;/li&gt;
&lt;li&gt;one &lt;code&gt;\n&lt;/code&gt;, never two&lt;/li&gt;
&lt;li&gt;check it in the real list, not in a plain-text editor&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The tool is the difference between editing blind and editing with your eyes open. For something players see before they even join, I think that's worth two minutes.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Indie dev, building in public. More server-admin tools at pixelforgehq.sell.app. Questions about the code system or the preview renderer: comments are open.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>minecraft</category>
      <category>webdev</category>
      <category>tutorial</category>
      <category>indiehackers</category>
    </item>
  </channel>
</rss>
