<?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: Selçuk Karayel</title>
    <description>The latest articles on DEV Community by Selçuk Karayel (@yudumnet).</description>
    <link>https://dev.to/yudumnet</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%2F4148119%2Fc1682f6c-e94e-4421-a924-6a46a54e9719.png</url>
      <title>DEV Community: Selçuk Karayel</title>
      <link>https://dev.to/yudumnet</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/yudumnet"/>
    <language>en</language>
    <item>
      <title>One process, one account — the architecture behind rebuilding a 28-year-old IRC network in Go</title>
      <dc:creator>Selçuk Karayel</dc:creator>
      <pubDate>Wed, 30 Sep 2026 14:41:26 +0000</pubDate>
      <link>https://dev.to/yudumnet/one-process-one-account-the-architecture-behind-rebuilding-a-28-year-old-irc-network-in-go-2ge4</link>
      <guid>https://dev.to/yudumnet/one-process-one-account-the-architecture-behind-rebuilding-a-28-year-old-irc-network-in-go-2ge4</guid>
      <description>&lt;p&gt;``In the &lt;a href="https://dev.to/yudumnet/i-rebuilt-my-28-year-old-irc-network-from-scratch-in-go-and-webassembly-2hdp"&gt;first post&lt;/a&gt; I showed what the platform does. This one is about how it's built: how the code is laid out, what Go made easy, which problems turned out to be hard, and how each one was solved.&lt;/p&gt;

&lt;p&gt;The constraint that drove everything: &lt;strong&gt;a person must be the same person no matter how they connect&lt;/strong&gt;. A raw IRC client, a browser app and a website all share one identity, one inbox, one social graph and one set of permissions, and none of them is a second-class client.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. One process instead of a fleet
&lt;/h2&gt;

&lt;p&gt;The classic IRC setup is an ircd plus a separate services daemon, linked over a server-to-server protocol, with a website off to the side reading a copy of the data. Every piece has its own view of the world, and most of the bugs live in keeping those views in sync.&lt;/p&gt;

&lt;p&gt;I went the other way. One Go binary contains the IRC server, the services (accounts, channels, social graph, admin) as ordinary Go packages, the HTTP side (website, JSON APIs, admin panel, browser gateway) and a Lua runtime for bots.&lt;/p&gt;

&lt;p&gt;Services aren't linked to the server; they &lt;em&gt;are&lt;/em&gt; the server. There's no burst on reconnect, no desync, and no protocol between them. A service handler reads the same in-memory user and channel structures the server uses and writes to the same database.&lt;/p&gt;

&lt;p&gt;The cost is a single failure domain. Deploys are built around it: a short-lived bridge process opens the same port with &lt;code&gt;SO_REUSEPORT&lt;/code&gt; while the new binary starts, so the HTTP side never goes dark.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. How the code is laid out
&lt;/h2&gt;

&lt;p&gt;About 300k lines of Go (excluding 265 test files), split by responsibility rather than by protocol:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Package&lt;/th&gt;
&lt;th&gt;Lines&lt;/th&gt;
&lt;th&gt;Role&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;core&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;23k&lt;/td&gt;
&lt;td&gt;IRC server: connections, channels, modes, SASL, IRCv3, WebRTC signalling, port sharing&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;services&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;25k&lt;/td&gt;
&lt;td&gt;NickServ, ChanServ, the social service, admin service, Lua engine&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;veri&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;4k&lt;/td&gt;
&lt;td&gt;the data layer: the only place allowed to touch shared tables&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;content&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;75k&lt;/td&gt;
&lt;td&gt;site domain logic: feed, groups, games, content, notifications&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;web/handlers&lt;/code&gt;, &lt;code&gt;web/render&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;53k&lt;/td&gt;
&lt;td&gt;HTTP handlers, templates, admin API&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;oyun&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;9k&lt;/td&gt;
&lt;td&gt;game engine: rule packages, drawing packages, bridges&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;wasm/chat&lt;/code&gt;, &lt;code&gt;wasm/tema&lt;/code&gt;, &lt;code&gt;wasm/sa&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;100k&lt;/td&gt;
&lt;td&gt;the three browser programs (TinyGo)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Two rules keep this manageable. First, &lt;strong&gt;dependencies point inwards&lt;/strong&gt;: handlers call &lt;code&gt;content&lt;/code&gt;, &lt;code&gt;content&lt;/code&gt; calls &lt;code&gt;veri&lt;/code&gt;, and nothing below reaches up. Second, &lt;strong&gt;one data layer owns the shared records&lt;/strong&gt;: accounts, settings and messages are written through &lt;code&gt;veri&lt;/code&gt;, so the IRC side and the web side can't disagree about what a record looks like.&lt;/p&gt;

&lt;p&gt;The external dependency list is short on purpose: &lt;code&gt;pion/webrtc&lt;/code&gt;, &lt;code&gt;gopher-lua&lt;/code&gt;, &lt;code&gt;modernc.org/sqlite&lt;/code&gt;, &lt;code&gt;bluemonday&lt;/code&gt;, &lt;code&gt;minify&lt;/code&gt;, &lt;code&gt;webpush-go&lt;/code&gt;, a GeoIP reader, and the &lt;code&gt;x/&lt;/code&gt; packages.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. What Go made easy
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;A goroutine per connection, a channel per client.&lt;/strong&gt; Each IRC client gets a reader goroutine and a bounded outbound channel. Broadcasting to a channel is just a non-blocking send into every member's queue. The select-with-default pattern gives you slow-consumer protection almost for free (see below).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;IRC and HTTP in the same process with no ceremony.&lt;/strong&gt; A raw TCP/TLS listener and &lt;code&gt;net/http&lt;/code&gt; live side by side and share memory directly. The website can ask the IRC server "is this user online right now?" with a function call, not an RPC.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;One static binary.&lt;/strong&gt; &lt;code&gt;modernc.org/sqlite&lt;/code&gt; is a pure-Go SQLite, so there's no cgo, no shared libraries and no build matrix. Deploying the server means copying one file.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The same language on the client.&lt;/strong&gt; TinyGo compiles Go to WebAssembly, so the browser programs share types and helpers with the server instead of re-describing them in another language.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Profiling a live system.&lt;/strong&gt; &lt;code&gt;pprof&lt;/code&gt; on a running server turned every performance argument into a measurement. Both of the biggest wins below came from a single profile.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Low-level control when needed.&lt;/strong&gt; &lt;code&gt;net.ListenConfig.Control&lt;/code&gt; gives access to the raw socket, which is all it takes to set &lt;code&gt;SO_REUSEPORT&lt;/code&gt; for the deploy bridge, with separate build-tagged files for Linux and BSD.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. The hard parts, and how they were solved
&lt;/h2&gt;

&lt;h3&gt;
  
  
  SQLite under concurrent load
&lt;/h3&gt;

&lt;p&gt;SQLite is a great single-file database, but it has one writer. The first version shared one connection (&lt;code&gt;SetMaxOpenConns(1)&lt;/code&gt;) for everything. A stress test with &lt;code&gt;pprof&lt;/code&gt; showed that with 32 concurrent requests, &lt;strong&gt;half of all waiting time was spent in &lt;code&gt;database/sql&lt;/code&gt; waiting for that one connection&lt;/strong&gt;. Reads were queueing behind other reads.&lt;/p&gt;

&lt;p&gt;The fix separates the two paths:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;One writer connection&lt;/strong&gt; for all writes and transactions.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A separate read pool&lt;/strong&gt; in WAL mode, where readers don't block each other or the writer. The pool is opened with &lt;code&gt;query_only(ON)&lt;/code&gt;, so an accidental write through it fails loudly instead of quietly succeeding.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A small page cache per connection plus a shared &lt;code&gt;mmap&lt;/code&gt;.&lt;/strong&gt; Sixteen connections with a 32 MB cache each had pushed memory towards 915 MB while the same cold pages were read over and over. A small per-connection cache with shared memory-mapped I/O fixed both.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  A global lock in the scripting engine
&lt;/h3&gt;

&lt;p&gt;Bots run in a single Lua state, and a Lua state isn't goroutine-safe, so it sits behind one lock. The original design called the script hook synchronously from the sender's own connection loop. A bot command that made an outbound HTTP call held the lock for seconds, and in that time &lt;strong&gt;every user on every channel was waiting behind it&lt;/strong&gt;. Chat lag with no obvious cause.&lt;/p&gt;

&lt;p&gt;The fix moves scripting off the hot path: every message goes into a &lt;strong&gt;bounded, ordered queue&lt;/strong&gt; drained by a single worker, which keeps message order for the bots. If the queue is full, the message simply isn't offered to the scripts (and is counted). &lt;strong&gt;A user's message is never delayed by a bot.&lt;/strong&gt; Per-account rate limits on bot commands stop anyone from flooding the queue on purpose.&lt;/p&gt;

&lt;h3&gt;
  
  
  Slow consumers
&lt;/h3&gt;

&lt;p&gt;One client on a bad mobile connection must never slow down a busy channel. Each client's outbound queue is a buffered channel whose size can be set per connection class. Sending into it is non-blocking; if the buffer is full, that client is disconnected with &lt;em&gt;SendQ exceeded&lt;/em&gt;, and everyone else carries on. The rule is simple: the server never waits for a client.&lt;/p&gt;

&lt;h3&gt;
  
  
  Constraints of Go in the browser
&lt;/h3&gt;

&lt;p&gt;TinyGo made the browser side possible, but it asks for discipline:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Binary size.&lt;/strong&gt; The webchat is 5.8 MB (1.9 MB gzipped). Reflection-heavy packages are avoided; JSON is parsed by the browser's own &lt;code&gt;JSON.parse&lt;/code&gt; and read through &lt;code&gt;syscall/js&lt;/code&gt; instead of &lt;code&gt;encoding/json&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;32-bit &lt;code&gt;int&lt;/code&gt;.&lt;/strong&gt; TinyGo's wasm target uses a 32-bit &lt;code&gt;int&lt;/code&gt;, so hashes are kept in explicitly sized unsigned types and modulo arithmetic happens on the unsigned side.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;One thread.&lt;/strong&gt; Goroutines are cooperative in the browser. Long work has to yield, and everything that talks to the network is written as callbacks from the event loop.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In return we get a &lt;strong&gt;strict Content-Security-Policy&lt;/strong&gt;: no inline scripts, no &lt;code&gt;on*&lt;/code&gt; attributes, no &lt;code&gt;eval&lt;/code&gt;. The only JavaScript on the page is the WASM loader, and every UI event goes through one delegation layer that dispatches on &lt;code&gt;data-*&lt;/code&gt; attributes.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Identity across three protocols
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Client&lt;/th&gt;
&lt;th&gt;Credential&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Raw IRC client&lt;/td&gt;
&lt;td&gt;SASL (PLAIN / EXTERNAL) or NickServ&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Website&lt;/td&gt;
&lt;td&gt;session cookie&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Browser app&lt;/td&gt;
&lt;td&gt;short-lived token&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;All three end in &lt;strong&gt;one resolver&lt;/strong&gt; that returns an account. Features never branch on the transport; they only ask &lt;em&gt;who is this&lt;/em&gt;. Account names also go through one normalisation function everywhere a name becomes a map key or a database key, because case folding differs between browsers, Go's standard library and SQL.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Messages: one store, delivery decided by presence
&lt;/h2&gt;

&lt;p&gt;There is &lt;strong&gt;one message store&lt;/strong&gt;, and every client writes to it. Where a message lands depends on &lt;strong&gt;the recipient's connection state at that moment&lt;/strong&gt;, not on the sender's client:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Situation&lt;/th&gt;
&lt;th&gt;Result&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Recipient connected&lt;/td&gt;
&lt;td&gt;Delivered live as a protocol message; the stored copy is marked read at once&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sender on the web, recipient connected&lt;/td&gt;
&lt;td&gt;Stored, then pushed into the recipient's live session through a localhost-only internal bridge&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Recipient offline&lt;/td&gt;
&lt;td&gt;Stored unread, and surfaced in the inbox on their next visit, from any client&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Deciding by recipient presence rather than sender surface removed a whole family of "the message went to the wrong place" problems. Offline history is kept only between mutual friends, as a privacy rule.&lt;/p&gt;

&lt;h2&gt;
  
  
  7. Social features on top of protocol primitives
&lt;/h2&gt;

&lt;p&gt;Follows, friendships, groups and notifications are a &lt;strong&gt;service inside the IRC server&lt;/strong&gt;, and they reuse IRC primitives wherever possible. A group &lt;em&gt;is&lt;/em&gt; a channel: roles are channel permissions, and announcements and events are delivered into the channel as messages. The website adds a page on top; it isn't the source of truth.&lt;/p&gt;

&lt;p&gt;So the social layer inherits everything the chat layer already has: permissions, moderation, bans and flood limits.&lt;/p&gt;

&lt;h2&gt;
  
  
  8. A pluggable transport, with the server kept honest
&lt;/h2&gt;

&lt;p&gt;The browser app reaches the IRC server through a gateway. The transport between browser and gateway is &lt;strong&gt;pluggable and picked from the admin panel&lt;/strong&gt;: HTTP long-polling (what runs in production), a WebRTC data channel, or WebSocket. Whatever the transport, the gateway connects with &lt;strong&gt;WEBIRC&lt;/strong&gt;, so the server sees the real client IP and applies the same bans and limits as for any native client.&lt;/p&gt;

&lt;h2&gt;
  
  
  9. Real-time media as separate systems
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;1:1 calls&lt;/strong&gt;: WebRTC peer to peer, with signalling relayed by the server and short-lived TURN credentials.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Moderated voice rooms per channel&lt;/strong&gt;: media is peer to peer, but room state (speaking, raised hands, mutes) lives on the server, so moderation is authoritative.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;One-to-many streaming&lt;/strong&gt; over WHIP/WHEP: screen, game or camera straight from the browser, or from OBS.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Keeping these as separate systems, each with its own signalling, state and failure modes, means a problem always points at one small system instead of one big "media" module.&lt;/p&gt;

&lt;h2&gt;
  
  
  10. Games: the server is the only authority
&lt;/h2&gt;

&lt;p&gt;The table games share one engine. Each game is a &lt;strong&gt;rule package&lt;/strong&gt; (legal moves, validation, outcome), a &lt;strong&gt;drawing package&lt;/strong&gt; that knows nothing about the game, and a thin &lt;strong&gt;bridge&lt;/strong&gt;. Seats, tables, rankings and moderation are shared.&lt;/p&gt;

&lt;p&gt;The server never sends the game state to the browser. It computes &lt;strong&gt;a separate view for each seat&lt;/strong&gt;: in a card game you get your own hand, your opponents' hands arrive only as a count, and spectators see none. The client draws the view and sends back a move chosen from the legal moves the server supplied.&lt;/p&gt;

&lt;h2&gt;
  
  
  11. Numbers
&lt;/h2&gt;

&lt;p&gt;Single VPS, 6 vCPUs and 12 GB of RAM:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Binary: &lt;strong&gt;35 MB&lt;/strong&gt;, statically linked, with no cgo&lt;/li&gt;
&lt;li&gt;Webchat WASM: &lt;strong&gt;5.8 MB&lt;/strong&gt; (1.9 MB gzipped); site WASM: &lt;strong&gt;1.5 MB&lt;/strong&gt; (0.5 MB gzipped)&lt;/li&gt;
&lt;li&gt;About &lt;strong&gt;300k lines of Go&lt;/strong&gt; and 265 test files; about 13k lines of sandboxed Lua for bots and games&lt;/li&gt;
&lt;li&gt;About &lt;strong&gt;550 MB RSS&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;One SQLite database&lt;/strong&gt;, with one writer connection and a WAL read pool&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What's next
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Load.&lt;/strong&gt; A much larger network may move onto this platform, so the gateway is being load-tested for several thousand concurrent browser clients. That will be the next post, with numbers.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Always-on sessions.&lt;/strong&gt; A bouncer built into the server, so a user stays in their channels and keeps their history with every client closed.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you've run long-polling at a few thousand clients in Go, or pushed SQLite harder than this, I'd like to hear what broke first.&lt;/p&gt;

&lt;p&gt;Try it without an account: &lt;strong&gt;&lt;a href="https://www.yudum.net/webchat/" rel="noopener noreferrer"&gt;https://www.yudum.net/webchat/&lt;/a&gt;&lt;/strong&gt;&lt;br&gt;
``&lt;/p&gt;

</description>
      <category>go</category>
      <category>showdev</category>
      <category>webassembly</category>
      <category>webdev</category>
    </item>
    <item>
      <title>I rebuilt my 28-year-old IRC network from scratch in Go and WebAssembly</title>
      <dc:creator>Selçuk Karayel</dc:creator>
      <pubDate>Mon, 28 Sep 2026 22:12:14 +0000</pubDate>
      <link>https://dev.to/yudumnet/i-rebuilt-my-28-year-old-irc-network-from-scratch-in-go-and-webassembly-2hdp</link>
      <guid>https://dev.to/yudumnet/i-rebuilt-my-28-year-old-irc-network-from-scratch-in-go-and-webassembly-2hdp</guid>
      <description>&lt;p&gt;In 1998 I opened an IRC network for Turkish chatters called &lt;strong&gt;Yudum.NET&lt;/strong&gt;. It started on HybridIRC, moved to UnrealIRCd, and for more than two decades it ran on the same kind of software almost every Turkish IRC network still runs today: an old C daemon, a pile of Tcl/mIRC-script bots, and a web chat bolted on the side.&lt;/p&gt;

&lt;p&gt;This year I threw all of that away and wrote the whole thing again. Alone.&lt;/p&gt;

&lt;p&gt;Here's a 4-minute tour of what came out of it:&lt;/p&gt;

&lt;p&gt;  &lt;iframe src="https://www.youtube.com/embed/KK8vSpCU0ac" width="710" height="399"&gt;
  &lt;/iframe&gt;
&lt;/p&gt;

&lt;p&gt;You can also just open it and type — no account needed: &lt;strong&gt;&lt;a href="https://www.yudum.net/webchat/" rel="noopener noreferrer"&gt;https://www.yudum.net/webchat/&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Why rewrite something that works?
&lt;/h2&gt;

&lt;p&gt;Because it didn't, really. Not for the people I wanted to keep.&lt;/p&gt;

&lt;p&gt;Classic IRC is great if you already know it. For everyone else, it's a black window, a list of nicknames and a set of commands to memorise. Voice, images, games, "who is this person?" — all of that lived somewhere else: Discord, WhatsApp, Instagram. People came, looked around, and left for the app where their friends were.&lt;/p&gt;

&lt;p&gt;I didn't want to turn IRC into Discord. I wanted the opposite: keep the channel, the nick list and the plain-text soul of IRC, and put everything else &lt;em&gt;inside&lt;/em&gt; it — so nobody has to leave the chat to do anything.&lt;/p&gt;

&lt;h2&gt;
  
  
  The shape of it: one binary
&lt;/h2&gt;

&lt;p&gt;The whole platform is a single Go binary:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;An IRC daemon&lt;/strong&gt; — regular IRC clients (mIRC, HexChat, IRCCloud…) still connect as they always have.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Services&lt;/strong&gt; — NickServ, ChanServ, plus a social service that handles friends, follows, groups and the feed.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The website&lt;/strong&gt; — profiles, feed, Discover, games, radio pages, help centre, all in Turkish and English.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The webchat&lt;/strong&gt; — served by the same process.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;There is one database (SQLite), one identity and one admin panel. Your nickname &lt;em&gt;is&lt;/em&gt; your site account; a friend you add in chat is a friend on the website. A post you share on the site shows up in the chat's feed.&lt;/p&gt;

&lt;p&gt;Running everything in one process sounds old-fashioned, but for a solo developer on one VPS it's a gift: no network hops between services, no "which service is down?", one place to look when something breaks.&lt;/p&gt;

&lt;h2&gt;
  
  
  The webchat is WebAssembly — and there is no plain JavaScript
&lt;/h2&gt;

&lt;p&gt;This was the decision that surprised people most.&lt;/p&gt;

&lt;p&gt;The webchat — chat, feed, profiles, games, voice rooms, streaming controls — is written in Go and compiled to WebAssembly with TinyGo. The site's own interactive bits and the admin panel are two more WASM modules. Apart from the tiny loader that boots WASM, there's no hand-written JavaScript shipped to the browser.&lt;/p&gt;

&lt;p&gt;Why?&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;One language.&lt;/strong&gt; The same Go types describe a message on the server and in the browser. The game engine that validates a move on the server is the same code that draws the table in the chat.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A strict Content-Security-Policy.&lt;/strong&gt; No inline scripts, no &lt;code&gt;onclick&lt;/code&gt; attributes, no &lt;code&gt;eval&lt;/code&gt;. Every click goes through a small event-delegation layer inside WASM. On a chat site where anyone can type anything, this closes a whole family of XSS mistakes by design.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It's harder to lift.&lt;/strong&gt; Not a security feature, just a nice side effect.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The trade-offs are real: TinyGo's &lt;code&gt;syscall/js&lt;/code&gt; has sharp edges (numbers coming back as strings, 32-bit ints, very careful memory use), the WASM file is several megabytes, and a full build takes minutes. But the result is one codebase I can hold in my head.&lt;/p&gt;

&lt;h2&gt;
  
  
  Features, without leaving the chat
&lt;/h2&gt;

&lt;p&gt;Everything below opens &lt;em&gt;inside&lt;/em&gt; the chat window:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Social feed&lt;/strong&gt; — posts, likes, comments, trends, 24-hour picture statuses.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Discover&lt;/strong&gt; — news, videos, films and series, a TV guide, football score predictions, quizzes and polls.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Table games&lt;/strong&gt; — Backgammon, 101 Okey, Checkers, Mangala and Pişti. When a table has an empty seat, the channel bot announces it and people join with one click.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Voice rooms and calls&lt;/strong&gt; — join a voice channel, raise your hand for the floor, or call a friend 1-to-1.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Live streaming&lt;/strong&gt; — stream your camera, your screen or the game you're playing, with viewers chatting next to the stream.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Radio&lt;/strong&gt; — our station YudumFM plays at the top of the chat: request songs, vote on what's playing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Moderation&lt;/strong&gt; — kick, ban, voice, op, kill… all behind a right-click (or long-press on mobile). No command memorising.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And none of this breaks plain IRC. Someone on mIRC sees normal channel messages; they just can't be called on video. (The server politely tells you that instead of spamming them with signalling junk — a bug I fixed only this week.)&lt;/p&gt;

&lt;h2&gt;
  
  
  The part nobody sees: staying up
&lt;/h2&gt;

&lt;p&gt;Here's the honest lesson of this rewrite.&lt;/p&gt;

&lt;p&gt;I spent months shipping features, and the community barely noticed. What they &lt;em&gt;did&lt;/em&gt; notice was every restart. On IRC, a restart means everyone gets disconnected at once, and for some people that's the moment they close the tab and don't come back.&lt;/p&gt;

&lt;p&gt;So the most valuable work turned out to be boring:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Zero-downtime web deploys.&lt;/strong&gt; During a release, a short-lived bridge process keeps serving HTTP while the new binary starts, so the website and webchat never go blank. IRC connections still drop once — that's the next thing to solve.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Batching.&lt;/strong&gt; Templates, styles, translations, WASM and settings can all change without a restart. Go code changes are collected and shipped together, deliberately, instead of "just one more quick deploy".&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Settings in the database, not in code.&lt;/strong&gt; Nearly every limit, text and toggle is editable from the admin panel and takes effect within seconds.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The next big piece is a built-in bouncer, so your connection stays in the channel even when your browser tab doesn't.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'd tell someone doing the same
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Features don't bring people back; reliability keeps them.&lt;/strong&gt; I'd have done the zero-downtime work first if I could go back.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Keep the old door open.&lt;/strong&gt; Old users still connect with the IRC client they've used for 20 years. Nothing new was allowed to break that.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;One process is fine.&lt;/strong&gt; Until it isn't — but for a community of this size, it very much still is.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Guest access matters.&lt;/strong&gt; Being able to type in a channel within ten seconds, without signing up, is still the best onboarding there is.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If you've ever run an IRC network, a forum or any small community that refuses to die — I'd love to hear how you keep it alive. And if you want to poke at the result, drop by: &lt;strong&gt;&lt;a href="https://www.yudum.net" rel="noopener noreferrer"&gt;https://www.yudum.net&lt;/a&gt;&lt;/strong&gt; 👋&lt;/p&gt;

</description>
      <category>showdev</category>
      <category>go</category>
      <category>webassembly</category>
      <category>webdev</category>
    </item>
  </channel>
</rss>
