<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>iercan&apos;s dev blog</title><description>Backend developer building multiplayer web games. Notes on netcode, game engines, and whatever I&apos;m hacking on.</description><link>https://iercan.com/</link><language>en</language><lastBuildDate>Tue, 06 Oct 2026 00:00:00 GMT</lastBuildDate><atom:link href="https://iercan.com/rss.xml" rel="self" type="application/rss+xml"/><docs>https://www.rssboard.org/rss-specification</docs><generator>Astro (@astrojs/rss)</generator><item><title>homelab 101</title><link>https://iercan.com/posts/homelab/</link><guid isPermaLink="true">https://iercan.com/posts/homelab/</guid><description>Two second-hand mini PCs, one RAM swap, a very loud fan and my first Proxmox VMs</description><pubDate>Tue, 06 Oct 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;I’ve been messing with servers for a while: random VPSes for my own projects, and more and more cloud deployments at work. It went step by step: plain Linux and nginx, then Docker (mostly on hetzner), then cloud providers like AWS, Azure. Turns out I kinda like that type of work, why not have a little setup at home to play with&lt;/p&gt;
&lt;h2 id=&quot;the-hardware&quot;&gt;the hardware&lt;/h2&gt;
&lt;p&gt;Many years ago I wanted to buy Raspberry Pis, but nowadays they’re way too expensive… still good pieces of hardware though.&lt;/p&gt;
&lt;p&gt;&lt;img alt=&quot;A Raspberry Pi 5 with 4 GB of memory listed at €119.16&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; data-astro-image=&quot;constrained&quot; data-astro-image-pos=&quot;center&quot; width=&quot;1026&quot; height=&quot;590&quot; src=&quot;https://iercan.com/_astro/image.CjjBDAdL_1jF6cX.webp&quot;&gt;
&lt;em&gt;4 GB of memory for €120 (without delivery).&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;So I bought 2 “mini PCs” instead (I think the M700 isn’t really a mini PC?):&lt;/p&gt;
&lt;p&gt;&lt;img alt=&quot;My orders: a Lenovo ThinkCentre M700 Tiny for €133.68 and an Intel NUC 8 for €107.43&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; data-astro-image=&quot;constrained&quot; data-astro-image-pos=&quot;center&quot; width=&quot;929&quot; height=&quot;616&quot; src=&quot;https://iercan.com/_astro/image-1.BwId7san_p121q.webp&quot;&gt;&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Intel NUC 8&lt;/th&gt;
&lt;th&gt;Lenovo ThinkCentre M700 Tiny&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Price&lt;/td&gt;
&lt;td&gt;€107&lt;/td&gt;
&lt;td&gt;€133&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CPU&lt;/td&gt;
&lt;td&gt;i5-8365U&lt;/td&gt;
&lt;td&gt;i5-6500T&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;RAM&lt;/td&gt;
&lt;td&gt;16 GB DDR4&lt;/td&gt;
&lt;td&gt;32 GB DDR4&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Storage&lt;/td&gt;
&lt;td&gt;120 GB SATA SSD&lt;/td&gt;
&lt;td&gt;512 GB SATA SSD&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;I didn’t plan on buying 2 pieces of hardware, but when I saw the amount of DDR4 RAM the M700 comes with, it was insane value&lt;/p&gt;
&lt;p&gt;so here they are:&lt;/p&gt;
&lt;p&gt;&lt;img alt=&quot;The NUC 8 and the M700 Tiny next to each other on my desk&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; data-astro-image=&quot;constrained&quot; data-astro-image-pos=&quot;center&quot; width=&quot;768&quot; height=&quot;600&quot; src=&quot;https://iercan.com/_astro/image-2.q31tCj6r_Zrz2on.webp&quot;&gt;&lt;/p&gt;
&lt;p&gt;The CPU of the NUC 8 is better. One mistake: I thought the CPU would be very important for a homelab (Proxmox), but usually your stuff runs at a low % CPU when idle. What’s more important is the RAM (how many VMs and containers you can run) and the storage.&lt;/p&gt;
&lt;h2 id=&quot;opening-them-up&quot;&gt;opening them up&lt;/h2&gt;
&lt;p&gt;opening the NUC 8:&lt;/p&gt;
&lt;p&gt;&lt;img alt=&quot;Inside the NUC 8: a WD Green M.2 SSD and two Crucial 8 GB RAM sticks&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; data-astro-image=&quot;constrained&quot; data-astro-image-pos=&quot;center&quot; width=&quot;680&quot; height=&quot;760&quot; src=&quot;https://iercan.com/_astro/image-3.1cz15IEh_Z1YiXcI.webp&quot;&gt;
&lt;em&gt;I thought it was an NVMe drive at first but nop, it’s just the form factor. it’s SATA&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;opening the M700:&lt;/p&gt;
&lt;p&gt;&lt;img alt=&quot;Inside the M700 Tiny: a Micron 512 GB 2.5” SATA SSD over the motherboard&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; data-astro-image=&quot;constrained&quot; data-astro-image-pos=&quot;center&quot; width=&quot;728&quot; height=&quot;780&quot; src=&quot;https://iercan.com/_astro/image-4.EbfoFSOZ_2pU1yF.webp&quot;&gt;
&lt;em&gt;a 512 GB SATA SSD (I’ll add it to the NUC 8), and under there is the RAM!&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;RAM swap. I’ll only use the NUC 8 for now since it has the better CPU:&lt;/p&gt;
&lt;p&gt;&lt;img alt=&quot;The RAM swap: two Crucial 8 GB sticks and two Corsair Vengeance 16 GB sticks on the desk&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; data-astro-image=&quot;constrained&quot; data-astro-image-pos=&quot;center&quot; width=&quot;482&quot; height=&quot;640&quot; src=&quot;https://iercan.com/_astro/image-5.DoHHN3Vu_Z173qiC.webp&quot;&gt;
&lt;em&gt;out: 2× 8 GB Crucial. in: 2× 16 GB Corsair Vengeance.&lt;/em&gt;&lt;/p&gt;
&lt;h2 id=&quot;very-loud-fan&quot;&gt;very loud fan&lt;/h2&gt;
&lt;p&gt;First thing to test was flashing the latest Ubuntu on the NUC 8… and it felt like I got the loudest PC ever. This is very common on the NUC 8 apparently. There are 2 problems:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;the fan is low quality (&lt;a href=&quot;https://www.youtube.com/shorts/l2m9lxBDuUY&quot;&gt;here’s a short about it&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;the BIOS is faulty and the fan control doesn’t work, so it should be updated to the latest version. common problem (&lt;a href=&quot;https://www.reddit.com/r/intelnuc/comments/jeucnf/nuc8i5_fan_always_at_3500rpm_regardless_of_fan/&quot;&gt;reddit thread&lt;/a&gt;), you just need to update the BIOS.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;So I had to do both: replace the fan, and update the BIOS.&lt;/p&gt;
&lt;p&gt;&lt;img alt=&quot;My order of a replacement CPU fan for the NUC 8, collected on October 2nd&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; data-astro-image=&quot;constrained&quot; data-astro-image-pos=&quot;center&quot; width=&quot;845&quot; height=&quot;274&quot; src=&quot;https://iercan.com/_astro/image-8.CjTNSv-Z_Z24DvYI.webp&quot;&gt;
&lt;em&gt;mine is the NUC8i5BEH (Intel NUC gen 8, i5).&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;and now everything is ok. good temperatures too!&lt;/p&gt;
&lt;p&gt;&lt;img alt=&quot;The sensors output in the Proxmox shell: CPU package and cores at 31 to 32 °C&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; data-astro-image=&quot;constrained&quot; data-astro-image-pos=&quot;center&quot; width=&quot;1414&quot; height=&quot;1117&quot; src=&quot;https://iercan.com/_astro/image-9.BXDrI7vO_1slDNf.webp&quot;&gt;&lt;/p&gt;
&lt;h2 id=&quot;proxmox&quot;&gt;proxmox&lt;/h2&gt;
&lt;p&gt;let’s learn Proxmox! :)&lt;/p&gt;
&lt;p&gt;&lt;img alt=&quot;The Proxmox installer’s network screen, with the server at 192.168.0.15&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; data-astro-image=&quot;constrained&quot; data-astro-image-pos=&quot;center&quot; width=&quot;1080&quot; height=&quot;1440&quot; src=&quot;https://iercan.com/_astro/image-6.CbYOj9K2_Z9hOhp.webp&quot;&gt;&lt;/p&gt;
&lt;p&gt;everything works:&lt;/p&gt;
&lt;p&gt;&lt;img alt=&quot;Proxmox VE 9.2.2 with a Debian 13 VM open in the browser console, “debian vm hi” typed in a text editor&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; data-astro-image=&quot;constrained&quot; data-astro-image-pos=&quot;center&quot; width=&quot;1642&quot; height=&quot;1095&quot; src=&quot;https://iercan.com/_astro/image-7.DKF2B5u__1qXRsf.webp&quot;&gt;&lt;/p&gt;
&lt;p&gt;btw to avoid the no subscription pop-up and be able to “apt update” you can paste a community script.&lt;/p&gt;
&lt;p&gt;&lt;img alt=&quot;btw&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; data-astro-image=&quot;constrained&quot; data-astro-image-pos=&quot;center&quot; width=&quot;1640&quot; height=&quot;412&quot; src=&quot;https://iercan.com/_astro/image-10.JRUgjnwY_1w6BeQ.webp&quot;&gt;&lt;/p&gt;
&lt;pre class=&quot;astro-code github-light&quot; style=&quot;background-color:#fff;color:#24292e; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;bash&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6F42C1&quot;&gt;bash&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt; -c&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt; &quot;$(&lt;/span&gt;&lt;span style=&quot;color:#6F42C1&quot;&gt;curl&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt; -fsSL&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt; https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/tools/pve/post-pve-install.sh)&quot;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;next up: I wanna test the Proxmox Terraform provider, learn more about k8s on real hardware, try out random software, and maybe self-host an RSS reader…&lt;/p&gt;
</content:encoded><category>homelab</category><category>hardware</category><category>infra</category></item><item><title>should you put Tailscale everywhere?</title><link>https://iercan.com/posts/tailscale-untrusted/</link><guid isPermaLink="true">https://iercan.com/posts/tailscale-untrusted/</guid><description>By default, every device on a Tailscale network can reach every other one. should you trust it?</description><pubDate>Tue, 29 Sep 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;A while back I found a hidden link on Google for a limited sale: a VPS in the US for $2/month. I bought it pretty much immediately, which is how I ended up owning 4 VPSes: 3 “proper” Hetzner ones in the EU and this “shady” one.&lt;/p&gt;
&lt;p&gt;&lt;img alt=&quot;screenfetch output&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; data-astro-image=&quot;constrained&quot; data-astro-image-pos=&quot;center&quot; width=&quot;2954&quot; height=&quot;966&quot; src=&quot;https://iercan.com/_astro/image.cD5BzaSz_1OVUEs.webp&quot;&gt;&lt;/p&gt;
&lt;p&gt;$2 is quite cheap, especially considering what AI is doing to hardware prices, and the catch is that the host “oversubscribes” its CPUs. My 3 vCPUs are shared with other people, so a noisy neighbour can steal my CPU time. It’s common enough that &lt;code&gt;top&lt;/code&gt; has a column for it: &lt;code&gt;st&lt;/code&gt;, “time stolen from this vm by the hypervisor”&lt;sup&gt;&lt;a href=&quot;https://iercan.com/posts/tailscale-untrusted/#user-content-fn-1&quot; id=&quot;user-content-fnref-1&quot; data-footnote-ref aria-describedby=&quot;footnote-label&quot;&gt;1&lt;/a&gt;&lt;/sup&gt;.&lt;/p&gt;
&lt;p&gt;It runs an old project of mine, &lt;a href=&quot;https://multi.notstrike.online/&quot;&gt;notstrike&lt;/a&gt; (a multiplayer Counter-Strike game engine and WebSocket server), which is basically just a Caddy reverse proxy for SSL and a few Docker containers for the game servers:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-light&quot; style=&quot;background-color:#fff;color:#24292e; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;text&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;CONTAINER ID   IMAGE                    COMMAND                  CREATED         STATUS                  PORTS                                                                                             NAMES&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;5820602e249c   us-game-server-1         &quot;docker-entrypoint.s…&quot;   12 months ago   Up 9 months             4011/tcp                                                                                          notstrike-server-eu-1&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;3214c954edc6   us-game-server-2         &quot;docker-entrypoint.s…&quot;   12 months ago   Up 9 months             4011/tcp                                                                                          notstrike-server-eu-2&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;9830c7ce706f   louislam/uptime-kuma:1   &quot;/usr/bin/dumb-init …&quot;   12 months ago   Up 9 months (healthy)   0.0.0.0:3001-&amp;gt;3001/tcp, [::]:3001-&amp;gt;3001/tcp                                                       uptime-kuma&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;e61f5e8016fe   caddy:2-alpine           &quot;caddy run --config …&quot;   13 months ago   Up 9 months             0.0.0.0:80-&amp;gt;80/tcp, [::]:80-&amp;gt;80/tcp, 0.0.0.0:443-&amp;gt;443/tcp, [::]:443-&amp;gt;443/tcp, 443/udp, 2019/tcp   notstrike-caddy-eu&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Here’s the US server browser:&lt;/p&gt;
&lt;p&gt;&lt;img alt=&quot;notstrike server browser&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; data-astro-image=&quot;constrained&quot; data-astro-image-pos=&quot;center&quot; width=&quot;3072&quot; height=&quot;1232&quot; src=&quot;https://iercan.com/_astro/image-1.BA5bZTgE_2r4yhk.webp&quot;&gt;&lt;/p&gt;
&lt;p&gt;Honestly, it’s never given me any trouble. I just wouldn’t trust it with anything sensitive (the price is kind of the reason why).&lt;/p&gt;
&lt;h2 id=&quot;securing-a-vps&quot;&gt;securing a VPS&lt;/h2&gt;
&lt;p&gt;I’ve had VPSes for 6 years now:&lt;/p&gt;
&lt;p&gt;&lt;img alt=&quot;Hetzner servers&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; data-astro-image=&quot;constrained&quot; data-astro-image-pos=&quot;center&quot; width=&quot;2408&quot; height=&quot;670&quot; src=&quot;https://iercan.com/_astro/image-2.CVbF0_uG_ZPsdVO.webp&quot;&gt;&lt;/p&gt;
&lt;p&gt;classical ways of securing your vps:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;disabling root password authentication&lt;/li&gt;
&lt;li&gt;moving SSH off port 22&lt;/li&gt;
&lt;li&gt;installing fail2ban&lt;/li&gt;
&lt;li&gt;putting a firewall in front of it, at the Hetzner level&lt;/li&gt;
&lt;li&gt;not exposing databases (under Docker) on 0.0.0.0&lt;/li&gt;
&lt;li&gt;not running things as root&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;the-new-way&quot;&gt;the new way&lt;/h2&gt;
&lt;p&gt;But the new way I do it (and many people do!) is &lt;a href=&quot;https://tailscale.com/&quot;&gt;Tailscale&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Tailscale puts all your devices on one private network (a “tailnet”) and lets them talk to each other directly over encrypted WireGuard connections, even through NAT and firewalls, without forwarding a single port. There’s still a central “coordination server”, but it’s basically a shared drop box for public keys, and the private keys never leave the device that generated them.&lt;/p&gt;
&lt;p&gt;So I can close every port except 80 and 443, leave the firewall at the Hetzner level and drop SSH exposure completely. And on my own computer, &lt;code&gt;tailscale status&lt;/code&gt; gives me a line for every device on my account with its Tailscale IP, name, owner and OS.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Diagram: The internet reaches Caddy on ports 80 and 443. Ports 22 and 3001 are blocked from the internet and only reachable from my laptop over Tailscale. &lt;a href=&quot;https://iercan.com/posts/tailscale-untrusted/&quot;&gt;See it in the post.&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;h2 id=&quot;the-danger&quot;&gt;the danger&lt;/h2&gt;
&lt;p&gt;I was putting Tailscale on the shady VPS and thought, maybe I should actually check how this works before connecting it to all my stuff&lt;/p&gt;
&lt;p&gt;Can the VPS SSH into my laptop? Into my other machines? if someone gets into this VPS, can they get into my other machines too? (or maybe I’m dumb, idk)&lt;/p&gt;
&lt;p&gt;Tailscale’s docs even have a warning about it:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Although access control policies in Tailscale follow a deny-by-default security model, the default access control policy is to allow all devices within a tailnet to connect to each other.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;em&gt;Tailscale docs, “What devices can connect to or know mine?”&lt;sup&gt;&lt;a href=&quot;https://iercan.com/posts/tailscale-untrusted/#user-content-fn-2&quot; id=&quot;user-content-fnref-2&quot; data-footnote-ref aria-describedby=&quot;footnote-label&quot;&gt;2&lt;/a&gt;&lt;/sup&gt;&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;here’s what Tailscale allows by default:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-light&quot; style=&quot;background-color:#fff;color:#24292e; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;json&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;{&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;  &quot;grants&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;: [&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;    {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;      &quot;src&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;: [&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;*&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;],&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;      &quot;dst&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;: [&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;*&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;],&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;      &quot;ip&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;: [&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;*&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;    }&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;  ]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;code&gt;*&lt;/code&gt; source “lets any device or user start connections”, to any device, with any protocol on any port&lt;sup&gt;&lt;a href=&quot;https://iercan.com/posts/tailscale-untrusted/#user-content-fn-3&quot; id=&quot;user-content-fnref-3&quot; data-footnote-ref aria-describedby=&quot;footnote-label&quot;&gt;3&lt;/a&gt;&lt;/sup&gt;.&lt;/p&gt;
&lt;p&gt;So yes: with the default policy, the shady VPS can open a connection to any port on my laptop and on my other VPSes. A dev server on 0.0.0.0 or a database without a password would just answer 🥲​&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Diagram: With the default policy, the shady VPS can connect to every other device: my laptop, my two computers and my three Hetzner VPSes. &lt;a href=&quot;https://iercan.com/posts/tailscale-untrusted/&quot;&gt;See it in the post.&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;And SSH? A regular SSH server still asks for a key or a password. But on my VPSes I use Tailscale SSH (I just type &lt;code&gt;tailscale ssh&lt;/code&gt;), and there’s no key or password there. Its server “already knows who the remote party is”, so it uses the SSH authentication type &lt;code&gt;none&lt;/code&gt;, and the &lt;code&gt;ssh&lt;/code&gt; rules in the policy decide who gets in&lt;sup&gt;&lt;a href=&quot;https://iercan.com/posts/tailscale-untrusted/#user-content-fn-4&quot; id=&quot;user-content-fnref-4&quot; data-footnote-ref aria-describedby=&quot;footnote-label&quot;&gt;4&lt;/a&gt;&lt;/sup&gt;.&lt;/p&gt;
&lt;p&gt;And if the VPS is logged in with my account, for Tailscale it’s one of &lt;em&gt;my&lt;/em&gt; devices:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;it sees every other device I own, even the ones the policy doesn’t let it connect to&lt;sup&gt;&lt;a href=&quot;https://iercan.com/posts/tailscale-untrusted/#user-content-fn-2&quot; id=&quot;user-content-fnref-2-2&quot; data-footnote-ref aria-describedby=&quot;footnote-label&quot;&gt;2&lt;/a&gt;&lt;/sup&gt;&lt;/li&gt;
&lt;li&gt;Taildrop (if it’s turned on) can send files between it and my devices, “even if access control policies are used to restrict access to the devices”&lt;sup&gt;&lt;a href=&quot;https://iercan.com/posts/tailscale-untrusted/#user-content-fn-5&quot; id=&quot;user-content-fnref-5&quot; data-footnote-ref aria-describedby=&quot;footnote-label&quot;&gt;5&lt;/a&gt;&lt;/sup&gt; (on macOS, they land in &lt;code&gt;~/Downloads&lt;/code&gt;)&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;my-policy&quot;&gt;my policy&lt;/h2&gt;
&lt;p&gt;I found tags while reading the docs. I hadn’t heard of them before, but they let me give the VPS different permissions from my laptop. Here’s the policy I ended up with:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-light&quot; style=&quot;background-color:#fff;color:#24292e; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;jsonc&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;{&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;  // who can tag (only admins so just me)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;  &quot;tagOwners&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;: {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;    &quot;tag:untrusted&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;: [&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;autogroup:admin&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;],&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;  },&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;  &quot;grants&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;: [&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;    // autogroup:member = devices logged in as a person. A tagged device isn&apos;t one, so this grant skips it.&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;    { &lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;&quot;src&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;: [&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;autogroup:member&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;], &lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;&quot;dst&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;: [&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;*&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;], &lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;&quot;ip&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;: [&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;*&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;] },&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;  ],&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;  &quot;ssh&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;: [&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;    // &quot;check&quot; = log in again in the browser before SSH, then you&apos;re good for 12 hours.&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;    {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;      &quot;action&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;check&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;      &quot;src&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;: [&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;autogroup:member&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;],&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;      &quot;dst&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;: [&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;autogroup:self&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;],&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;      &quot;users&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;: [&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;autogroup:nonroot&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;root&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;],&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;    },&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;    // &quot;accept&quot; = straight in, Tailscale already knows it&apos;s me.&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;    {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;      &quot;action&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;accept&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;      &quot;src&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;: [&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;autogroup:member&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;],&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;      &quot;dst&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;: [&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;tag:untrusted&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;],&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;      &quot;users&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;: [&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;autogroup:nonroot&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;root&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;],&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;    },&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;  ],&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And then every VPS got the tag. Even the “proper” Hetzner ones: I trust them more, but they still have no reason to reach my laptop.&lt;/p&gt;
&lt;p&gt;&lt;img alt=&quot;put-tag&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; data-astro-image=&quot;constrained&quot; data-astro-image-pos=&quot;center&quot; width=&quot;2130&quot; height=&quot;1290&quot; src=&quot;https://iercan.com/_astro/image-4.DaMjSgWF_Z8UJ1U.webp&quot;&gt;&lt;/p&gt;
&lt;h3 id=&quot;the-tag&quot;&gt;the tag&lt;/h3&gt;
&lt;pre class=&quot;astro-code github-light&quot; style=&quot;background-color:#fff;color:#24292e; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;jsonc&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;tagOwners&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;: {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;  &quot;tag:untrusted&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;: [&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;autogroup:admin&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;],&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;},&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;When I logged in to Tailscale on the VPS, it got added to my account, like my laptop. So it counted as one of my devices, and could reach everything my laptop can, including my laptop.&lt;/p&gt;
&lt;p&gt;Tagging it takes it off my account.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;tagOwners&lt;/code&gt; says who’s allowed to put &lt;code&gt;tag:untrusted&lt;/code&gt; on a device. Only admins, so only me.&lt;/p&gt;
&lt;h3 id=&quot;the-grant&quot;&gt;the grant&lt;/h3&gt;
&lt;pre class=&quot;astro-code github-light&quot; style=&quot;background-color:#fff;color:#24292e; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;jsonc&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;grants&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;: [&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;  { &lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;&quot;src&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;: [&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;autogroup:member&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;], &lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;&quot;dst&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;: [&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;*&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;], &lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;&quot;ip&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;: [&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;*&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;] },&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;],&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Grants are just allow rules: who can connect to what. There’s no “deny” rule, everything starts out blocked, so “the only possible &lt;code&gt;action&lt;/code&gt; is &lt;code&gt;accept&lt;/code&gt;”&lt;sup&gt;&lt;a href=&quot;https://iercan.com/posts/tailscale-untrusted/#user-content-fn-6&quot; id=&quot;user-content-fnref-6&quot; data-footnote-ref aria-describedby=&quot;footnote-label&quot;&gt;6&lt;/a&gt;&lt;/sup&gt;. To block something, you just leave it out.&lt;/p&gt;
&lt;p&gt;My one grant only lists my devices as the source, and the VPS isn’t one of them anymore. So it’s left out, and that’s the block. My laptop can still reach it (and the VPS can reply), the VPS just can’t start anything.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Diagram: With the tag, my laptop can connect to both VPSes, but the VPSes can&apos;t connect to my laptop or to each other. &lt;a href=&quot;https://iercan.com/posts/tailscale-untrusted/&quot;&gt;See it in the post.&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Even if someone hacks the VPS and tells Tailscale to ignore the rules, it doesn’t help them: my laptop checks incoming traffic itself.&lt;/p&gt;
&lt;h3 id=&quot;the-ssh-rules&quot;&gt;the SSH rules&lt;/h3&gt;
&lt;pre class=&quot;astro-code github-light&quot; style=&quot;background-color:#fff;color:#24292e; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;jsonc&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;ssh&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;: [&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;  {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;    &quot;action&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;check&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;    &quot;src&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;: [&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;autogroup:member&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;],&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;    &quot;dst&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;: [&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;autogroup:self&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;],&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;    &quot;users&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;: [&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;autogroup:nonroot&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;root&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;],&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;  },&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;  {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;    &quot;action&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;accept&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;    &quot;src&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;: [&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;autogroup:member&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;],&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;    &quot;dst&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;: [&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;tag:untrusted&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;],&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;    &quot;users&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;: [&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;autogroup:nonroot&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;root&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;],&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;  },&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;],&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The first rule is Tailscale’s default, and it’s mostly there for the “someone stole my laptop” case: SSH into my other devices needs a fresh browser login every 12 hours, so a thief gets cut off from SSH pretty fast (not from the other ports though, that’s what the grant is for).&lt;/p&gt;
&lt;p&gt;The second rule is pure convenience: from my laptop, I SSH into my VPSes without any key, password or browser, because Tailscale already knows it’s me.&lt;/p&gt;
&lt;p&gt;on the vps no rule lets it SSH anywhere, so it can’t.&lt;/p&gt;
&lt;h2 id=&quot;the-test&quot;&gt;the test&lt;/h2&gt;
&lt;p&gt;ok enough talk does it work?&lt;/p&gt;
&lt;p&gt;I SSHed into the VPS (&lt;code&gt;tailscale ssh ncr@shady-vps&lt;/code&gt;) and tried to reach my MacBook from there. First, &lt;code&gt;tailscale ping&lt;/code&gt;:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-light&quot; style=&quot;background-color:#fff;color:#24292e; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;bash&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6F42C1&quot;&gt;$&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt; tailscale&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt; ping&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt; --c&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt; 3&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt; --timeout&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt; 3s&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt; macbook-pro-de-ercan&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6F42C1&quot;&gt;pong&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt; from&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt; macbook-pro-de-ercan&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; (100.72.247.81) via [my home IPv6]:41641 in 26ms&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;It finds it, which confused me for a second. Turns out &lt;code&gt;tailscale ping&lt;/code&gt; only checks whether the two Tailscale clients can find each other, it doesn’t send any real traffic. With &lt;code&gt;--icmp&lt;/code&gt; it sends a normal ping through the tunnel, and this time my Mac drops it:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-light&quot; style=&quot;background-color:#fff;color:#24292e; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;bash&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6F42C1&quot;&gt;$&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt; tailscale&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt; ping&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt; --icmp&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt; --c&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt; 3&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt; --timeout&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt; 3s&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt; macbook-pro-de-ercan&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6F42C1&quot;&gt;ping&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt; &quot;100.72.247.81&quot;&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt; timed&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt; out&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6F42C1&quot;&gt;ping&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt; &quot;100.72.247.81&quot;&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt; timed&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt; out&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6F42C1&quot;&gt;ping&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt; &quot;100.72.247.81&quot;&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt; timed&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt; out&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6F42C1&quot;&gt;no&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt; reply&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;SSH times out too:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-light&quot; style=&quot;background-color:#fff;color:#24292e; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;bash&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6F42C1&quot;&gt;$&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt; timeout&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt; 20&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt; ssh&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt; -o&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt; BatchMode=yes&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt; -o&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt; ConnectTimeout=&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;5&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt; iercan@macbook-pro-de-ercan&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt; true&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6F42C1&quot;&gt;ssh:&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt; connect&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt; to&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt; host&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt; macbook-pro-de-ercan&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt; port&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt; 22:&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt; Connection&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt; timed&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt; out&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And &lt;code&gt;tailscale ssh iercan@macbook-pro-de-ercan&lt;/code&gt; just hangs until &lt;code&gt;timeout&lt;/code&gt; kills it, 20 seconds later.&lt;/p&gt;
&lt;p&gt;(Nothing listens on port 22 on my Mac anyway. From the Mac itself it says “Connection refused”, but from the VPS it’s “timed out”, because the packets never even reach macOS.)&lt;/p&gt;
&lt;p&gt;The other way around works fine. From my Mac:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-light&quot; style=&quot;background-color:#fff;color:#24292e; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;bash&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6F42C1&quot;&gt;$&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt; tailscale&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt; ping&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt; --icmp&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt; --c&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt; 3&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt; --timeout&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt; 3s&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt; shady-vps&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6F42C1&quot;&gt;pong&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt; from&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt; shady-vps&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; (100.124.31.123) via ICMP in 31ms&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;whats-left&quot;&gt;what’s left&lt;/h2&gt;
&lt;h3 id=&quot;risk-1-it-still-sees-my-devices&quot;&gt;risk 1: it still sees my devices&lt;/h3&gt;
&lt;p&gt;If I run &lt;code&gt;tailscale status&lt;/code&gt; on the VPS, I can still see my laptop and my other computers. They’re allowed to connect to it, so it knows about them&lt;sup&gt;&lt;a href=&quot;https://iercan.com/posts/tailscale-untrusted/#user-content-fn-2&quot; id=&quot;user-content-fnref-2-3&quot; data-footnote-ref aria-describedby=&quot;footnote-label&quot;&gt;2&lt;/a&gt;&lt;/sup&gt;:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-light&quot; style=&quot;background-color:#fff;color:#24292e; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;bash&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6F42C1&quot;&gt;$&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt; tailscale&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt; status&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6F42C1&quot;&gt;100.124.31.123&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;  shady-vps&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;             shady-vps.&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;tailne&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;t&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;&amp;gt;&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;.ts.net&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;  linux&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;  -&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6F42C1&quot;&gt;100.72.247.81&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;   macbook-pro-de-ercan&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;  me@&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;                         macOS&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;  active&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;; &lt;/span&gt;&lt;span style=&quot;color:#6F42C1&quot;&gt;direct&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; [my &lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;home&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt; IPv6]:41641,&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt; tx&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt; 66272&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt; rx&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt; 62936&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6F42C1&quot;&gt;100.82.19.89&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;    ncr-z490m&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;             me@&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;                         linux&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;  offline,&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt; last&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt; seen&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt; 1h&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt; ago&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6F42C1&quot;&gt;100.76.7.249&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;    pop-os&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;                me@&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;                         linux&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;  offline,&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt; last&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt; seen&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt; 1h&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt; ago&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If that bothers you, only let the devices that need the VPS connect to it.&lt;/p&gt;
&lt;p&gt;But a VPS can’t see other VPSes! My other VPSes are tagged too, and no grant lets a tagged device connect to another one, so they don’t know about each other&lt;sup&gt;&lt;a href=&quot;https://iercan.com/posts/tailscale-untrusted/#user-content-fn-2&quot; id=&quot;user-content-fnref-2-4&quot; data-footnote-ref aria-describedby=&quot;footnote-label&quot;&gt;2&lt;/a&gt;&lt;/sup&gt;.&lt;/p&gt;
&lt;h3 id=&quot;risk-2-my-own-connections&quot;&gt;risk 2: my own connections&lt;/h3&gt;
&lt;p&gt;The VPS can’t start connections, but my browser can, and my laptop can reach everything.&lt;/p&gt;
&lt;p&gt;With a trick called &lt;a href=&quot;https://en.wikipedia.org/wiki/DNS_rebinding&quot;&gt;DNS rebinding&lt;/a&gt;, any website I open can point its own domain at one of my tailnet IPs, and the browser then lets its JavaScript talk to that device as if it were the same site. That’s not specific to the VPS, but the VPS makes it easier: I actually open pages it serves, and it already knows my tailnet IPs (risk 1).&lt;/p&gt;
&lt;p&gt;Tailscale’s best practices mention this one: “a Tailscale node visiting a malicious web page can be triggered to run a client-side script and attack other nodes in the tailnet”&lt;sup&gt;&lt;a href=&quot;https://iercan.com/posts/tailscale-untrusted/#user-content-fn-7&quot; id=&quot;user-content-fnref-7&quot; data-footnote-ref aria-describedby=&quot;footnote-label&quot;&gt;7&lt;/a&gt;&lt;/sup&gt;.&lt;/p&gt;
&lt;h3 id=&quot;risk-3-my-tailscale-account&quot;&gt;risk 3: my Tailscale account&lt;/h3&gt;
&lt;p&gt;Whoever gets into my Tailscale account can rewrite the policy, remove the tag or add devices. Tailscale recommends MFA on your login, “ideally using a hardware token”&lt;sup&gt;&lt;a href=&quot;https://iercan.com/posts/tailscale-untrusted/#user-content-fn-7&quot; id=&quot;user-content-fnref-7-2&quot; data-footnote-ref aria-describedby=&quot;footnote-label&quot;&gt;7&lt;/a&gt;&lt;/sup&gt;.&lt;/p&gt;
&lt;p&gt;And if you don’t trust Tailscale itself: with Tailnet Lock, a new device needs a signature from one of your devices, so Tailscale can’t add one behind your back&lt;sup&gt;&lt;a href=&quot;https://iercan.com/posts/tailscale-untrusted/#user-content-fn-8&quot; id=&quot;user-content-fnref-8&quot; data-footnote-ref aria-describedby=&quot;footnote-label&quot;&gt;8&lt;/a&gt;&lt;/sup&gt;. The policy still comes from Tailscale’s servers, though&lt;sup&gt;&lt;a href=&quot;https://iercan.com/posts/tailscale-untrusted/#user-content-fn-9&quot; id=&quot;user-content-fnref-9&quot; data-footnote-ref aria-describedby=&quot;footnote-label&quot;&gt;9&lt;/a&gt;&lt;/sup&gt;.&lt;/p&gt;
&lt;h3 id=&quot;risk-4-my-own-devices&quot;&gt;risk 4: my own devices&lt;/h3&gt;
&lt;p&gt;if someone gets into my laptop, they get root on every VPS too. the tag doesn’t help here: I allowed that laptop to SSH into all my VPSes, so whoever’s using it can too.&lt;/p&gt;
&lt;p&gt;I could make Tailscale ask for a browser login before SSHing into a VPS. That’s what &lt;code&gt;check&lt;/code&gt; does, and Tailscale recommends it for root&lt;sup&gt;&lt;a href=&quot;https://iercan.com/posts/tailscale-untrusted/#user-content-fn-7&quot; id=&quot;user-content-fnref-7-3&quot; data-footnote-ref aria-describedby=&quot;footnote-label&quot;&gt;7&lt;/a&gt;&lt;/sup&gt;:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-light&quot; style=&quot;background-color:#fff;color:#24292e; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;diff&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;     // SSH into untrusted machines.&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;     {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B31D28&quot;&gt;&lt;span style=&quot;user-select: none;&quot;&gt;-&lt;/span&gt;      &quot;action&quot;: &quot;accept&quot;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#22863A&quot;&gt;&lt;span style=&quot;user-select: none;&quot;&gt;+&lt;/span&gt;      &quot;action&quot;: &quot;check&quot;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;       &quot;src&quot;: [&quot;autogroup:member&quot;],&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;       &quot;dst&quot;: [&quot;tag:untrusted&quot;],&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;       &quot;users&quot;: [&quot;autogroup:nonroot&quot;, &quot;root&quot;],&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;     },&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;With &lt;code&gt;check&lt;/code&gt;, SSH asks you to log in again in your browser, and then you’re good for 12 hours. (i mean, if they have access to your computer, they can also just use your browser to accept this, but you get the point.)&lt;/p&gt;
&lt;h2 id=&quot;thats-all&quot;&gt;that’s all!&lt;/h2&gt;
&lt;p&gt;So, should you put Tailscale everywhere? I’d say yes, as long as every machine you don’t trust gets a tag, and that tag isn’t allowed to connect to anything.&lt;/p&gt;
&lt;p&gt;more details here: &lt;a href=&quot;https://tailscale.com/docs/concepts/device-visibility&quot;&gt;What devices can connect to or know mine?&lt;/a&gt;&lt;/p&gt;
&lt;section data-footnotes class=&quot;footnotes&quot;&gt;&lt;h2 class=&quot;sr-only&quot; id=&quot;footnote-label&quot;&gt;Footnotes&lt;/h2&gt;
&lt;ol&gt;
&lt;li id=&quot;user-content-fn-1&quot;&gt;
&lt;p&gt;&lt;a href=&quot;https://man7.org/linux/man-pages/man1/top.1.html&quot;&gt;top(1)&lt;/a&gt;, procps-ng manual. &lt;a href=&quot;https://iercan.com/posts/tailscale-untrusted/#user-content-fnref-1&quot; data-footnote-backref=&quot;&quot; aria-label=&quot;Back to reference 1&quot; class=&quot;data-footnote-backref&quot;&gt;↩&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id=&quot;user-content-fn-2&quot;&gt;
&lt;p&gt;Tailscale docs, &lt;a href=&quot;https://tailscale.com/docs/concepts/device-visibility&quot;&gt;What devices can connect to or know mine?&lt;/a&gt; &lt;a href=&quot;https://iercan.com/posts/tailscale-untrusted/#user-content-fnref-2&quot; data-footnote-backref=&quot;&quot; aria-label=&quot;Back to reference 2&quot; class=&quot;data-footnote-backref&quot;&gt;↩&lt;/a&gt; &lt;a href=&quot;https://iercan.com/posts/tailscale-untrusted/#user-content-fnref-2-2&quot; data-footnote-backref=&quot;&quot; aria-label=&quot;Back to reference 2-2&quot; class=&quot;data-footnote-backref&quot;&gt;↩&lt;sup&gt;2&lt;/sup&gt;&lt;/a&gt; &lt;a href=&quot;https://iercan.com/posts/tailscale-untrusted/#user-content-fnref-2-3&quot; data-footnote-backref=&quot;&quot; aria-label=&quot;Back to reference 2-3&quot; class=&quot;data-footnote-backref&quot;&gt;↩&lt;sup&gt;3&lt;/sup&gt;&lt;/a&gt; &lt;a href=&quot;https://iercan.com/posts/tailscale-untrusted/#user-content-fnref-2-4&quot; data-footnote-backref=&quot;&quot; aria-label=&quot;Back to reference 2-4&quot; class=&quot;data-footnote-backref&quot;&gt;↩&lt;sup&gt;4&lt;/sup&gt;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id=&quot;user-content-fn-3&quot;&gt;
&lt;p&gt;Tailscale docs, &lt;a href=&quot;https://tailscale.com/docs/reference/examples/grants#allow-all&quot;&gt;Grant examples: Allow all&lt;/a&gt; &lt;a href=&quot;https://iercan.com/posts/tailscale-untrusted/#user-content-fnref-3&quot; data-footnote-backref=&quot;&quot; aria-label=&quot;Back to reference 3&quot; class=&quot;data-footnote-backref&quot;&gt;↩&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id=&quot;user-content-fn-4&quot;&gt;
&lt;p&gt;Tailscale docs, &lt;a href=&quot;https://tailscale.com/docs/features/tailscale-ssh&quot;&gt;Tailscale SSH&lt;/a&gt; &lt;a href=&quot;https://iercan.com/posts/tailscale-untrusted/#user-content-fnref-4&quot; data-footnote-backref=&quot;&quot; aria-label=&quot;Back to reference 4&quot; class=&quot;data-footnote-backref&quot;&gt;↩&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id=&quot;user-content-fn-5&quot;&gt;
&lt;p&gt;Tailscale docs, &lt;a href=&quot;https://tailscale.com/docs/features/taildrop&quot;&gt;Taildrop&lt;/a&gt; &lt;a href=&quot;https://iercan.com/posts/tailscale-untrusted/#user-content-fnref-5&quot; data-footnote-backref=&quot;&quot; aria-label=&quot;Back to reference 5&quot; class=&quot;data-footnote-backref&quot;&gt;↩&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id=&quot;user-content-fn-6&quot;&gt;
&lt;p&gt;Tailscale docs, &lt;a href=&quot;https://tailscale.com/docs/reference/syntax/policy-file&quot;&gt;Syntax reference for the tailnet policy file&lt;/a&gt; &lt;a href=&quot;https://iercan.com/posts/tailscale-untrusted/#user-content-fnref-6&quot; data-footnote-backref=&quot;&quot; aria-label=&quot;Back to reference 6&quot; class=&quot;data-footnote-backref&quot;&gt;↩&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id=&quot;user-content-fn-7&quot;&gt;
&lt;p&gt;Tailscale docs, &lt;a href=&quot;https://tailscale.com/docs/reference/best-practices/security&quot;&gt;Best practices to secure your tailnet&lt;/a&gt; &lt;a href=&quot;https://iercan.com/posts/tailscale-untrusted/#user-content-fnref-7&quot; data-footnote-backref=&quot;&quot; aria-label=&quot;Back to reference 7&quot; class=&quot;data-footnote-backref&quot;&gt;↩&lt;/a&gt; &lt;a href=&quot;https://iercan.com/posts/tailscale-untrusted/#user-content-fnref-7-2&quot; data-footnote-backref=&quot;&quot; aria-label=&quot;Back to reference 7-2&quot; class=&quot;data-footnote-backref&quot;&gt;↩&lt;sup&gt;2&lt;/sup&gt;&lt;/a&gt; &lt;a href=&quot;https://iercan.com/posts/tailscale-untrusted/#user-content-fnref-7-3&quot; data-footnote-backref=&quot;&quot; aria-label=&quot;Back to reference 7-3&quot; class=&quot;data-footnote-backref&quot;&gt;↩&lt;sup&gt;3&lt;/sup&gt;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id=&quot;user-content-fn-8&quot;&gt;
&lt;p&gt;Tailscale docs, &lt;a href=&quot;https://tailscale.com/docs/features/tailnet-lock&quot;&gt;Tailnet Lock&lt;/a&gt; &lt;a href=&quot;https://iercan.com/posts/tailscale-untrusted/#user-content-fnref-8&quot; data-footnote-backref=&quot;&quot; aria-label=&quot;Back to reference 8&quot; class=&quot;data-footnote-backref&quot;&gt;↩&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id=&quot;user-content-fn-9&quot;&gt;
&lt;p&gt;Tailscale blog, &lt;a href=&quot;https://tailscale.com/blog/how-tailscale-works&quot;&gt;How Tailscale works&lt;/a&gt;, March 2020. &lt;a href=&quot;https://iercan.com/posts/tailscale-untrusted/#user-content-fnref-9&quot; data-footnote-backref=&quot;&quot; aria-label=&quot;Back to reference 9&quot; class=&quot;data-footnote-backref&quot;&gt;↩&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/section&gt;
</content:encoded><category>infra</category><category>networking</category><category>security</category></item><item><title>this blog runs on astro now</title><link>https://iercan.com/posts/astro-now/</link><guid isPermaLink="true">https://iercan.com/posts/astro-now/</guid><description>This blog ran on Hugo for eight months. Here is how I moved it to Astro, and why the first deploy failed.</description><pubDate>Sun, 20 Sep 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;I started this blog in January on &lt;a href=&quot;https://gohugo.io/&quot;&gt;Hugo&lt;/a&gt;, with the PaperMod theme. Really cool tool. But since then I wanted a bit more control, and a few interactive pages under my domain.&lt;/p&gt;
&lt;p&gt;what I want:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;markdown first, with each post’s images inside the same folder (simple)&lt;/li&gt;
&lt;li&gt;static files, no React or Vue (for a blog, even plain HTML could be enough)&lt;/li&gt;
&lt;li&gt;one push to deploy, on Cloudflare or anywhere else&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;so I moved it to &lt;a href=&quot;https://astro.build/&quot;&gt;Astro&lt;/a&gt;.&lt;/p&gt;
&lt;h2 id=&quot;why-astro&quot;&gt;why Astro?&lt;/h2&gt;
&lt;p&gt;I value simplicity more and more when it comes to the things I host.&lt;/p&gt;
&lt;p&gt;I was using Next.js a lot, and I went down their SSR route. But Vercel (the company behind Next.js) often changes their ways. For example, the App Router replaced &lt;code&gt;getStaticPaths&lt;/code&gt; with &lt;code&gt;generateStaticParams&lt;/code&gt; and often I get dependabots alerts about random CVEs (boring when you self host under your own vps).&lt;/p&gt;
&lt;p&gt;I want something so dumb that I can forget about it and throw it on Cloudflare.&lt;/p&gt;
&lt;p&gt;under Astro, posts are markdown files in folders, like in Hugo. i can just edit them with vim &amp;amp; do a &lt;code&gt;git push&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;Also tbh it was very easy to migrate  😅&lt;/p&gt;
&lt;h2 id=&quot;markdown-all-the-way&quot;&gt;markdown all the way&lt;/h2&gt;
&lt;p&gt;Each post moved from &lt;code&gt;content/posts/&lt;/code&gt; to &lt;code&gt;src/content/blog/&lt;/code&gt;. It’s still one folder with an &lt;code&gt;index.md&lt;/code&gt; and its images:&lt;/p&gt;
&lt;p&gt;&lt;img alt=&quot;post folders&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; data-astro-image=&quot;constrained&quot; data-astro-image-pos=&quot;center&quot; width=&quot;303&quot; height=&quot;140&quot; src=&quot;https://iercan.com/_astro/folders.CxZh4kpZ_ZrVwXL.webp&quot;&gt;&lt;/p&gt;
&lt;h2 id=&quot;content&quot;&gt;content&lt;/h2&gt;
&lt;p&gt;The homepage is an &lt;code&gt;.astro&lt;/code&gt; file. The part between the &lt;code&gt;---&lt;/code&gt; lines runs once, at build time:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-light&quot; style=&quot;background-color:#fff;color:#24292e; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;astro&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;---&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;import&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; Base &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;from&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt; &quot;../layouts/Base.astro&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;import&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; PostList &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;from&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt; &quot;../components/PostList.astro&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;import&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; { getPosts } &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;from&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt; &quot;../lib/posts&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;const&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt; posts&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt; await&lt;/span&gt;&lt;span style=&quot;color:#6F42C1&quot;&gt; getPosts&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;(); &lt;/span&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;// runs at build time&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;---&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;Base&lt;/span&gt;&lt;span style=&quot;color:#6F42C1&quot;&gt; title&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;iercan&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;  &amp;lt;&lt;/span&gt;&lt;span style=&quot;color:#22863A&quot;&gt;p&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;&amp;gt;writing about computer science, backend, infra, linux, and webgames. it&apos;s as boring as it sounds :)&amp;lt;/&lt;/span&gt;&lt;span style=&quot;color:#22863A&quot;&gt;p&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;  &amp;lt;&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;PostList&lt;/span&gt;&lt;span style=&quot;color:#6F42C1&quot;&gt; posts&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;={posts} /&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;&amp;lt;/&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;Base&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;So what comes out is plain HTML. Out of 287 pages, exactly one ships JavaScript: the &lt;a href=&quot;https://iercan.com/thinkpads/&quot;&gt;ThinkPad table&lt;/a&gt;, for its search and sorting&lt;/p&gt;
&lt;p&gt;Everything else works with JavaScript turned off&lt;/p&gt;
&lt;h2 id=&quot;adding-rss&quot;&gt;adding RSS&lt;/h2&gt;
&lt;p&gt;RSS lets you follow a blog from an app (an RSS reader) instead of visiting the site. I’ve started using it to follow random blogs.&lt;/p&gt;
&lt;p&gt;But a lot of feeds only show a one-line summary. So this blog’s feed (&lt;a href=&quot;https://iercan.com/rss.xml&quot;&gt;/rss.xml&lt;/a&gt;) has the full posts, images included. Astro renders each post to HTML at build time, the same way it renders the pages, starting from &lt;a href=&quot;https://docs.astro.build/en/recipes/rss/&quot;&gt;its RSS guide&lt;/a&gt;.&lt;/p&gt;
&lt;h2 id=&quot;build-and-deploy&quot;&gt;build and deploy&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;pnpm run build&lt;/code&gt; writes one HTML file per page into &lt;code&gt;dist/&lt;/code&gt;:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-light&quot; style=&quot;background-color:#fff;color:#24292e; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;text&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;$ pnpm run build&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt; generating static routes&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;22:55:13   ├─ /404.html (+24ms)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;22:55:13   ├─ /posts/astro-now/index.html (+5ms)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;22:55:13   ├─ /posts/thinkpads/index.html (+25ms)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;22:55:14   ├─ /posts/web-roblox/index.html (+8ms)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;...&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;22:55:14 [build] 287 page(s) built in 1.80s&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Here’s the homepage it writes:&lt;/p&gt;
&lt;p&gt;&lt;img alt=&quot;dist index.html&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; data-astro-image=&quot;constrained&quot; data-astro-image-pos=&quot;center&quot; width=&quot;1256&quot; height=&quot;888&quot; src=&quot;https://iercan.com/_astro/image.Cxv74h_5_Z1asNEC.webp&quot;&gt;&lt;/p&gt;
&lt;h3 id=&quot;problem-the-first-deploy-failed&quot;&gt;problem: the first deploy failed&lt;/h3&gt;
&lt;p&gt;Cloudflare has an Astro preset:&lt;/p&gt;
&lt;p&gt;&lt;img alt=&quot;Cloudflare build settings&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; data-astro-image=&quot;constrained&quot; data-astro-image-pos=&quot;center&quot; width=&quot;969&quot; height=&quot;562&quot; src=&quot;https://iercan.com/_astro/Screenshot_2026-09-20_20-38-13.DWJsF45y_myJH5.webp&quot;&gt;&lt;/p&gt;
&lt;p&gt;But the first build failed with &lt;code&gt;ERR_PNPM_OUTDATED_LOCKFILE&lt;/code&gt;. My &lt;code&gt;pnpm-lock.yaml&lt;/code&gt; still listed packages I had removed from &lt;code&gt;package.json&lt;/code&gt;, and Cloudflare installs with a frozen lockfile.&lt;/p&gt;
&lt;p&gt;The fix was three small changes:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;sync &lt;code&gt;pnpm-lock.yaml&lt;/code&gt; with &lt;code&gt;package.json&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;pin Node 22 in a &lt;code&gt;.node-version&lt;/code&gt; file&lt;/li&gt;
&lt;li&gt;delete a leftover &lt;code&gt;package-lock.json&lt;/code&gt; (4,897 lines!)&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;thats-all&quot;&gt;that’s all!&lt;/h2&gt;
&lt;p&gt;source &lt;a href=&quot;https://astro.build/&quot;&gt;astro.build&lt;/a&gt;&lt;/p&gt;
</content:encoded><category>webdev</category><category>frontend</category><category>blog</category></item><item><title>Every ThinkPad in one table</title><link>https://iercan.com/posts/thinkpads/</link><guid isPermaLink="true">https://iercan.com/posts/thinkpads/</guid><description>Lenovo publishes every spec of every ThinkPad, one page per model. I turned it into one table.</description><pubDate>Sun, 20 Sep 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;I’ve been using ThinkPads for a long time… They’ve been the most cost-effective laptops I could find on my local used market.&lt;/p&gt;
&lt;p&gt;Here are the ThinkPads I’ve had so far:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;X220&lt;/li&gt;
&lt;li&gt;T440s&lt;/li&gt;
&lt;li&gt;L14 Gen 3&lt;/li&gt;
&lt;li&gt;T14 Gen 2&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Compared to MacBooks, ThinkPads lose their value pretty quickly. That’s actually great if you’re buying one: every year, you get more for your money on the refurbished market.&lt;/p&gt;
&lt;p&gt;But I also know what a rabbit hole it can be to find &lt;em&gt;the&lt;/em&gt; model you want. Maybe you want an older one, or a collector’s model, or just whatever has the best specs for your budget.&lt;/p&gt;
&lt;p&gt;I would love to be able to browse every ThinkPad in one place, but Lenovo’s website has never really been my thing. Here’s what it looks like when I open it (2026):&lt;/p&gt;
&lt;p&gt;&lt;img alt=&quot;Lenovo website popup&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; data-astro-image=&quot;constrained&quot; data-astro-image-pos=&quot;center&quot; width=&quot;2704&quot; height=&quot;1516&quot; src=&quot;https://iercan.com/_astro/image.CWBPaTbj_Z1BSp8U.webp&quot;&gt;&lt;/p&gt;
&lt;p&gt;so I made a static table instead : &lt;a href=&quot;https://iercan.com/thinkpads/&quot;&gt;iercan.com/thinkpads&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Think of it as &lt;a href=&quot;https://diskprices.com/&quot;&gt;diskprices.com&lt;/a&gt;, but for ThinkPads. Right now it has 235 ThinkPads in it, from 2018 to 2026. Here’s what it looks like:&lt;/p&gt;
&lt;p&gt;&lt;img alt=&quot;the thinkpad table&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; data-astro-image=&quot;constrained&quot; data-astro-image-pos=&quot;center&quot; width=&quot;1440&quot; height=&quot;900&quot; src=&quot;https://iercan.com/_astro/table.Db86xvVj_2snfgp.webp&quot;&gt;&lt;/p&gt;
&lt;h2 id=&quot;searching-and-filtering&quot;&gt;searching and filtering&lt;/h2&gt;
&lt;p&gt;You can search by model or CPU, sort by any column, and filter for things like “RAM upgradable”, “Ethernet” or “OLED option”. For example, here’s what you get if you search for “L14”:&lt;/p&gt;
&lt;p&gt;&lt;img alt=&quot;L14 search results&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; data-astro-image=&quot;constrained&quot; data-astro-image-pos=&quot;center&quot; width=&quot;1878&quot; height=&quot;1138&quot; src=&quot;https://iercan.com/_astro/image-2.DGiKmzWg_Z21NbNx.webp&quot;&gt;&lt;/p&gt;
&lt;h2 id=&quot;every-generation-side-by-side&quot;&gt;every generation side by side&lt;/h2&gt;
&lt;p&gt;If a line has more than one generation (like the &lt;a href=&quot;https://iercan.com/thinkpads/t14/&quot;&gt;T14&lt;/a&gt; or the &lt;a href=&quot;https://iercan.com/thinkpads/l14/&quot;&gt;L14&lt;/a&gt;), it also gets a page with all of its generations side by side, so you can see how one model changed over the years.&lt;/p&gt;
&lt;p&gt;The cells that changed since the previous generation are highlighted in pink. For example, the T14’s RAM went soldered on the Gen 3 (AMD), and then came back as 2× SODIMM on Gen 5:&lt;/p&gt;
&lt;p&gt;&lt;img alt=&quot;every T14 generation&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; data-astro-image=&quot;constrained&quot; data-astro-image-pos=&quot;center&quot; width=&quot;1440&quot; height=&quot;900&quot; src=&quot;https://iercan.com/_astro/t14.BfCMJ5IN_aKHET.webp&quot;&gt;&lt;/p&gt;
&lt;h2 id=&quot;one-page-per-model&quot;&gt;one page per model&lt;/h2&gt;
&lt;p&gt;And every model gets its own page with all of its specs. Here’s the one for the &lt;a href=&quot;https://iercan.com/thinkpads/t14-gen-2-amd/&quot;&gt;T14 Gen 2 (AMD)&lt;/a&gt; (the T14 Gen 2 is one of the ThinkPads I’ve had!):&lt;/p&gt;
&lt;p&gt;&lt;img alt=&quot;T14 spec page&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; data-astro-image=&quot;constrained&quot; data-astro-image-pos=&quot;center&quot; width=&quot;1600&quot; height=&quot;1406&quot; src=&quot;https://iercan.com/_astro/image-1.XpF1V7l3_21QFqP.webp&quot;&gt;&lt;/p&gt;
&lt;h2 id=&quot;you-can-also-ask-an-llm&quot;&gt;you can also ask an LLM&lt;/h2&gt;
&lt;p&gt;LLMs can browse the web with their tools now. And iercan.com is a fully static site on Cloudflare, so it’s very easy to scrape&lt;/p&gt;
&lt;p&gt;you can give a newer model the link and just ask it a question.&lt;/p&gt;
&lt;p&gt;&lt;img alt=&quot;llm&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; data-astro-image=&quot;constrained&quot; data-astro-image-pos=&quot;center&quot; width=&quot;772&quot; height=&quot;688&quot; src=&quot;https://iercan.com/_astro/image-6.WCEsxcf1_Z2s6uoP.webp&quot;&gt;&lt;/p&gt;
&lt;p&gt;Here’s the answer it came back with. The little “iercan” tags are its sources, and they link to the model pages on my site:&lt;/p&gt;
&lt;p&gt;&lt;img alt=&quot;row citing iercan&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; data-astro-image=&quot;constrained&quot; data-astro-image-pos=&quot;center&quot; width=&quot;814&quot; height=&quot;773&quot; src=&quot;https://iercan.com/_astro/image-5.BNZ0NnGD_11rx98.webp&quot;&gt;&lt;/p&gt;
&lt;p&gt;I put a “Source” line at the bottom of every model page, with links to Lenovo’s PSREF page and PDF for that model, so LLMs (or you!) can double check everything:&lt;/p&gt;
&lt;p&gt;&lt;img alt=&quot;PSREF source line&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; data-astro-image=&quot;constrained&quot; data-astro-image-pos=&quot;center&quot; width=&quot;461&quot; height=&quot;255&quot; src=&quot;https://iercan.com/_astro/image-7.BWieYBBU_AzCKq.webp&quot;&gt;&lt;/p&gt;
&lt;p&gt;This might not be the best use case, but I think it’s good to know. It works with DeepSeek/other models too.&lt;/p&gt;
&lt;h2 id=&quot;how-does-it-work&quot;&gt;how does it work?&lt;/h2&gt;
&lt;p&gt;The specs come from &lt;a href=&quot;https://psref.lenovo.com/&quot;&gt;PSREF&lt;/a&gt;, which is Lenovo’s “Product Specifications Reference”. It turns out PSREF has a JSON API that anyone can use. No login, no API key!&lt;/p&gt;
&lt;p&gt;These are the two endpoints I use:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://psref.lenovo.com/api/ph/ProductCategoryTree&quot;&gt;https://psref.lenovo.com/api/ph/ProductCategoryTree&lt;/a&gt; lists every product&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://psref.lenovo.com/api/product/Compare/LoadSpecData?ProductKey=ThinkPad_T14_Gen_5_Intel&quot;&gt;https://psref.lenovo.com/api/product/Compare/LoadSpecData?ProductKey=ThinkPad_T14_Gen_5_Intel&lt;/a&gt; has all the specs for one model&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;A note on older models: the API doesn’t include all of the older ThinkPads (like the X220 or the T440p). The oldest ones in the table right now are from 2018, so my old X220 and T440s aren’t here.&lt;/p&gt;
&lt;h3 id=&quot;problem-1-json-inside-json&quot;&gt;problem 1: JSON inside JSON&lt;/h3&gt;
&lt;p&gt;The first weird thing I ran into: the specs are a string of JSON inside the JSON response. So you parse the JSON, and then you parse the JSON again.&lt;/p&gt;
&lt;p&gt;&lt;img alt=&quot;PSREF stringified JSON&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; data-astro-image=&quot;constrained&quot; data-astro-image-pos=&quot;center&quot; width=&quot;1801&quot; height=&quot;972&quot; src=&quot;https://iercan.com/_astro/image-3.VmJePiRl_Zvabe7.webp&quot;&gt;&lt;/p&gt;
&lt;h3 id=&quot;problem-2-the-specs-are-sentences&quot;&gt;problem 2: the specs are sentences&lt;/h3&gt;
&lt;p&gt;Once you get to the actual values, they’re sentences written for humans, like “One drive, up to 2TB M.2 2280 SSD”:&lt;/p&gt;
&lt;p&gt;&lt;img alt=&quot;raw storage value&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; data-astro-image=&quot;constrained&quot; data-astro-image-pos=&quot;center&quot; width=&quot;1739&quot; height=&quot;309&quot; src=&quot;https://iercan.com/_astro/image-4.BYL3nDbJ_2raKxD.webp&quot;&gt;&lt;/p&gt;
&lt;p&gt;A couple more examples:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;“Two DDR5 SODIMM slots, dual-channel capable”&lt;/li&gt;
&lt;li&gt;“One drive, up to 128GB M.2 2242 SSD or 1TB M.2 2280 SSD”&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;After fighting with Claude to parse every edge case in this JSON (regexes!), here’s the kind of output I store, as plain JSON:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-light&quot; style=&quot;background-color:#fff;color:#24292e; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;json&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;{&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;  &quot;key&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;ThinkPad_T14_Gen_5_Intel&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;  &quot;memory&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;: {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;    &quot;maxGb&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;64&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;    &quot;types&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;: [&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;DDR5-5600&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;],&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;    &quot;kind&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;sodimm&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;    &quot;slots&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;2&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;  },&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;  &quot;storage&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;: {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;    &quot;slots&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;1&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;    &quot;formFactors&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;: [&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;2280&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;],&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;    &quot;pcie&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;: [&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;4.0&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;],&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;    &quot;maxTb&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;2&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;  },&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;  &quot;batteryWh&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;: [&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;39.3&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;52.5&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;],&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;  &quot;weightKg&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;1.31&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If you want that data for every model in one file, it’s at &lt;a href=&quot;https://iercan.com/thinkpads.json&quot;&gt;/thinkpads.json&lt;/a&gt;.&lt;/p&gt;
&lt;h2 id=&quot;thats-all&quot;&gt;that’s all!&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://iercan.com/thinkpads/&quot;&gt;iercan.com/thinkpads&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Hopefully it’s useful to people other than me :)&lt;/p&gt;
&lt;p&gt;(btw, the L14 Gen 3 is the best one)&lt;/p&gt;
</content:encoded><category>hardware</category><category>webdev</category><category>data</category></item><item><title>Building a multiplayer Roblox-like engine in the browser</title><link>https://iercan.com/posts/web-roblox/</link><guid isPermaLink="true">https://iercan.com/posts/web-roblox/</guid><description>impossible project but learnt a lot!</description><pubDate>Tue, 06 Jan 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;img src=&quot;https://iercan.com/_astro/thumb.CY5g4I84.png&quot; alt=&quot;Roblox in the browser&quot;&gt;&lt;/p&gt;&lt;p&gt;DRAFT POST NOT FINISHED.&lt;/p&gt;
&lt;p&gt;I’ve been making web games for a while now. My first ones were around 2017, following YouTube tutorials by &lt;a href=&quot;https://www.youtube.com/channel/UCvjgXvBlbQiydffZU7m1_aw&quot;&gt;Daniel Shiffman&lt;/a&gt;, which introduced me to &lt;a href=&quot;https://p5js.org/&quot;&gt;p5.js&lt;/a&gt; a JavaScript library that makes it easy to create graphics and interactive content in the browser.&lt;/p&gt;
&lt;p&gt;&lt;img alt=&quot;My old p5.js projects&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; data-astro-image=&quot;constrained&quot; data-astro-image-pos=&quot;center&quot; width=&quot;1371&quot; height=&quot;979&quot; src=&quot;https://iercan.com/_astro/p5.CJjdoR8f_Z1CF8xH.webp&quot;&gt;
&lt;em&gt;Some of my old p5.js projects from 2017-2018&lt;/em&gt;&lt;/p&gt;
&lt;h2 id=&quot;tcp-or-udp&quot;&gt;TCP or UDP?&lt;/h2&gt;
&lt;p&gt;One of the first things you learn when making websites are REST APIs. The client sends a request, the server responds with data. You &lt;em&gt;could&lt;/em&gt; use this for multiplayer games, but it’s not ideal.&lt;/p&gt;
&lt;p&gt;Older web games used &lt;strong&gt;HTTP long polling&lt;/strong&gt;. The client asks for data, the server waits until it has some, answers, and the client asks again. Slow, but it was the only option since Internet Explorer had no WebSockets.&lt;/p&gt;
&lt;p&gt;&lt;img alt=&quot;old IE versions&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; data-astro-image=&quot;constrained&quot; data-astro-image-pos=&quot;center&quot; width=&quot;1146&quot; height=&quot;485&quot; src=&quot;https://iercan.com/_astro/ie.CrvBs60g_R5zTs.webp&quot;&gt;&lt;/p&gt;
&lt;p&gt;Libraries like &lt;a href=&quot;https://socket.io/fr/&quot;&gt;Socket.io&lt;/a&gt; abstracted the transport layer, using WebSockets when available and falling back to long polling otherwise.&lt;/p&gt;
&lt;p&gt;WebSockets give you a persistent, real-time connection between client and server. They’re TCP-based, so packets are guaranteed to arrive in order. That sounds great but it’s not perfect for fast-paced games.&lt;/p&gt;
&lt;p&gt;If a packet is lost, every subsequent packet gets delayed until the lost one is retransmitted and acknowledged. This is called &lt;strong&gt;head-of-line blocking&lt;/strong&gt;, and packet loss happens all the time on real networks, leading to spikes in latency and jitter.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;ℹ️ You can simulate packet drop with tools like &lt;a href=&quot;https://jagt.github.io/clumsy/&quot;&gt;clumsy&lt;/a&gt; on Windows to see exactly how badly TCP can behave in a game context.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;UDP is the standard for multiplayer games. It’s faster than TCP because it skips all the reliability overhead (no ACKs, no retransmits), but it isn’t natively available in browsers. You’d have to use &lt;strong&gt;WebRTC DataChannels&lt;/strong&gt;, which are more complex to set up.&lt;/p&gt;
&lt;h3 id=&quot;scaling-websocket-game-servers&quot;&gt;Scaling WebSocket game servers&lt;/h3&gt;
&lt;p&gt;Scaling WebSocket game servers is genuinely hard. Connections are stateful and persistent, which complicates everything. You need to:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Spin up new servers on demand&lt;/li&gt;
&lt;li&gt;Route connections correctly through a load balancer&lt;/li&gt;
&lt;li&gt;Run a room manager or master server to track which players are in which rooms&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Geographical latency matters a lot in fast-paced games too, so you ideally want servers in Europe, North America, Asia, and so on. You could orchestrate all of this with Kubernetes and Docker, but it’s a serious amount of work.&lt;/p&gt;
&lt;p&gt;There’s a premade solution called &lt;a href=&quot;https://agones.dev/&quot;&gt;Agones&lt;/a&gt; built specifically for scaling game servers on Kubernetes, but I didn’t try it.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Bandwidth&lt;/strong&gt; is another concern. Providers like Hetzner give you a monthly allowance (around 20 TB), and exceeding it means extra fees. (highly depends on the location, US hetzner servers give less bandwidth)&lt;/p&gt;
&lt;p&gt;&lt;img alt=&quot;hetzner&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; data-astro-image=&quot;constrained&quot; data-astro-image-pos=&quot;center&quot; width=&quot;883&quot; height=&quot;192&quot; src=&quot;https://iercan.com/_astro/hetzner.Doo50mcO_22Rl5A.webp&quot;&gt;&lt;/p&gt;
&lt;p&gt;Fast-paced games can burn through bandwidth quickly. If you’re curious, open a Source engine game and run &lt;code&gt;net_graph 1&lt;/code&gt; in the console to see your real-time network usage.&lt;/p&gt;
&lt;p&gt;&lt;img alt=&quot;tf2&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; data-astro-image=&quot;constrained&quot; data-astro-image-pos=&quot;center&quot; width=&quot;458&quot; height=&quot;301&quot; src=&quot;https://iercan.com/_astro/tf2.izMO0MM4_hrvns.webp&quot;&gt;&lt;/p&gt;
&lt;p&gt;The “in” and “out” values represent:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;in&lt;/strong&gt; data received per second (server → you)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;out&lt;/strong&gt; data sent per second (you → server)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;To put some numbers on it: assume a 24-player server where each player consumes around 7.21 KB/s inbound. That’s roughly 173 KB/s total, which adds up to about &lt;strong&gt;450 GB per month&lt;/strong&gt;.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;⚠️ That estimate is based on a rough player count approximation, but the math scales linearly with player count and tickrate. High-tickrate servers like 128-tick CS2 use significantly more.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;For a cheap Hetzner VPS with 20 TB included, this isn’t a major concern. In my tests, the bottlenecks were CPU and RAM long before bandwidth became an issue.&lt;/p&gt;
&lt;h3 id=&quot;scaling-webrtc-game-servers-p2p-model&quot;&gt;Scaling WebRTC game servers (P2P model)&lt;/h3&gt;
&lt;p&gt;WebRTC &lt;em&gt;can&lt;/em&gt; be peer-to-peer, which is appealing, no server relaying game data means much lower infrastructure costs at scale. Right?&lt;/p&gt;
&lt;p&gt;The basic flow is straightforward:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Client A wants to connect to Client B&lt;/li&gt;
&lt;li&gt;Both connect to a &lt;strong&gt;signaling server&lt;/strong&gt; (usually a simple WebSocket)&lt;/li&gt;
&lt;li&gt;The signaling server helps them exchange connection metadata (SDP offers/answers, ICE candidates)&lt;/li&gt;
&lt;li&gt;Using that information, they attempt to establish a direct connection&lt;/li&gt;
&lt;li&gt;If successful, data flows directly between clients via WebRTC DataChannel&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The catch: if either client sits behind a NAT or a restrictive firewall, direct communication may fail. In that case, WebRTC falls back to a &lt;strong&gt;TURN server&lt;/strong&gt;, which relays all traffic between peers and suddenly you’re paying for bandwidth again.&lt;/p&gt;
&lt;p&gt;&lt;img alt=&quot;Turn&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; data-astro-image=&quot;constrained&quot; data-astro-image-pos=&quot;center&quot; width=&quot;979&quot; height=&quot;308&quot; src=&quot;https://iercan.com/_astro/WebRTC.BudKZ3_9_12JOIx.webp&quot;&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;ℹ️ Free TURN servers do exist, like the &lt;a href=&quot;https://developers.cloudflare.com/realtime/turn/&quot;&gt;Cloudflare one&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h4 id=&quot;the-cheating-problem&quot;&gt;The cheating problem&lt;/h4&gt;
&lt;p&gt;Without a central authoritative server, data flows directly between clients. This is dangerous, what if a client just lies? &lt;strong&gt;Never trust the client.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;If you ever played the old Call of Duty games, you’ve seen this in action. Sometimes you’d join a match and quickly realize it was a hacked lobby. One player was the host, and the host was the authority.&lt;/p&gt;
&lt;p&gt;&lt;img alt=&quot;fly&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; data-astro-image=&quot;constrained&quot; data-astro-image-pos=&quot;center&quot; width=&quot;958&quot; height=&quot;540&quot; src=&quot;https://iercan.com/_astro/fly.CJ_lHDFE_ZK3Fub.webp&quot;&gt;
&lt;em&gt;Hacked COD4 lobby with fly hack, most common on console&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;This is why authoritative servers exist.&lt;/p&gt;
&lt;h3 id=&quot;what-do-other-web-games-use&quot;&gt;What do other web games use?&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Krunker.io&lt;/strong&gt; → WebSockets (Rust backend)
&lt;img alt=&quot;Krunker.io&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; data-astro-image=&quot;constrained&quot; data-astro-image-pos=&quot;center&quot; width=&quot;884&quot; height=&quot;316&quot; src=&quot;https://iercan.com/_astro/krunker.BGxl_5ps_ZeCcPs.webp&quot;&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Slither.io&lt;/strong&gt; → WebSockets
&lt;img alt=&quot;Slither&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; data-astro-image=&quot;constrained&quot; data-astro-image-pos=&quot;center&quot; width=&quot;884&quot; height=&quot;316&quot; src=&quot;https://iercan.com/_astro/slither.Bw50GfKI_12uHVC.webp&quot;&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Haxball&lt;/strong&gt; → WebRTC (UDP)
&lt;img alt=&quot;haxball&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; data-astro-image=&quot;constrained&quot; data-astro-image-pos=&quot;center&quot; width=&quot;1185&quot; height=&quot;298&quot; src=&quot;https://iercan.com/_astro/haxball.C30jkdJY_Z1nu7Lf.webp&quot;&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Most modern browsers support WebRTC well at this point. That said, I’m already familiar with WebSockets, I prefer an authoritative server model, and I already have VPSes ready to go. I decided to stick with WebSockets for now, and let’s be honest, I don’t expect this to scale to millions of players 💀&lt;/p&gt;
&lt;p&gt;WebSockets will be fine for my use case. I’ll hit other bottlenecks long before I hit the limits of the transport layer.&lt;/p&gt;
&lt;h2 id=&quot;starting-small-multiplayer-pong&quot;&gt;Starting small: multiplayer Pong&lt;/h2&gt;
&lt;p&gt;&lt;img alt=&quot;pong&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; data-astro-image=&quot;constrained&quot; data-astro-image-pos=&quot;center&quot; width=&quot;1226&quot; height=&quot;596&quot; src=&quot;https://iercan.com/_astro/goty.DQRWA0Ed_ZD8NRf.webp&quot;&gt;
&lt;em&gt;2017. Definitely a GOTY.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Before tackling anything complex, this was my first multiplayer project. A classic Pong game using Node.js and Socket.io, built in high school around ~2017&lt;/p&gt;
&lt;p&gt;The game state was minimal:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Paddle positions (y-coordinate)&lt;/li&gt;
&lt;li&gt;Ball position (x, y)&lt;/li&gt;
&lt;li&gt;Ball velocity (vx, vy)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Since your client controls the left paddle, I could instantly update its Y position based on mouse movement, without waiting for the server to confirm. This made the paddle feel responsive, even if the local position briefly diverged from the server’s authoritative state.&lt;/p&gt;
&lt;p&gt;Client-to-server messages looked like this:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-light&quot; style=&quot;background-color:#fff;color:#24292e; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;json&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;{&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;  &quot;type&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;move_paddle&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;  &quot;y&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;150&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Server trusts the client here. Not great, but the worst you can do is teleport a paddle in Pong.&lt;/p&gt;
&lt;p&gt;The server held the authoritative game state with a simple &lt;code&gt;clients&lt;/code&gt; array:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-light&quot; style=&quot;background-color:#fff;color:#24292e; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;typescript&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;// server.ts&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;class&lt;/span&gt;&lt;span style=&quot;color:#6F42C1&quot;&gt; Client&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E36209&quot;&gt;  gameStarted&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt; boolean&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;; &lt;/span&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;// used for matchmaking&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E36209&quot;&gt;  posX&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt; number&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E36209&quot;&gt;  posY&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt; number&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;  constructor&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;public&lt;/span&gt;&lt;span style=&quot;color:#E36209&quot;&gt; id&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt; string&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;) {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;    // socket id&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;    this&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;.gameStarted &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt; false&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;    this&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;.posX &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt; 0&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;    this&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;.posY &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt; 0&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;  }&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;I built a basic matchmaking system where players enter their name and get paired together.&lt;br&gt;
&lt;img alt=&quot;matchmaking&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; data-astro-image=&quot;constrained&quot; data-astro-image-pos=&quot;center&quot; width=&quot;720&quot; height=&quot;405&quot; src=&quot;https://iercan.com/_astro/waiting.BCVECOO8_Z4WOYX.webp&quot;&gt;&lt;/p&gt;
&lt;p&gt;The client used &lt;code&gt;requestAnimationFrame&lt;/code&gt; to render at the browser’s refresh rate (60 FPS, handled internally by p5.js), while the server ran a game loop at a fixed 60 ticks per second with &lt;code&gt;setInterval&lt;/code&gt;. At the time I didn’t have a high-refresh-rate monitor, so I didn’t realize how mismatched tickrates could degrade the experience.&lt;/p&gt;
&lt;p&gt;Server-to-client messages looked like this:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-light&quot; style=&quot;background-color:#fff;color:#24292e; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;json&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;{&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;  &quot;type&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;game_state&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;  &quot;ball&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;: { &lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;&quot;x&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;200&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;&quot;y&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;150&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; },&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;  &quot;paddles&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;: [&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;    { &lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;&quot;id&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;player1&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;&quot;y&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;120&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; },&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;    { &lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;&quot;id&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;player2&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;&quot;y&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;180&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; }&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;  ]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This approach had a real flaw: the server’s tickrate didn’t match the client’s framerate, leading to choppy ball movement. At minimum, we could interpolate between server updates to smooth things out.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;⚠️ We’re sending stringified JSON here. In a real game, you’d want binary serialization (Protocol Buffers, FlatBuffers, MessagePack) to shrink packet size, plus &lt;strong&gt;delta compression&lt;/strong&gt; to only send what actually changed since the last update.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 id=&quot;its-alive&quot;&gt;It’s alive!&lt;/h3&gt;
&lt;p&gt;This was my first multiplayer game. Simple, but it worked!&lt;/p&gt;
&lt;p&gt;&lt;img alt=&quot;pong gif&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; data-astro-image=&quot;constrained&quot; data-astro-image-pos=&quot;center&quot; width=&quot;720&quot; height=&quot;405&quot; src=&quot;https://iercan.com/_astro/multipong.B9T3x7_H_WWPaP.webp&quot;&gt;&lt;/p&gt;
&lt;p&gt;I added a chat later, easy to do since players were already exchanging packets with the server.&lt;/p&gt;
&lt;p&gt;I hosted it on a free Heroku dyno, back when Heroku had a generous free tier.&lt;/p&gt;
&lt;p&gt;&lt;img alt=&quot;multiPong&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; data-astro-image=&quot;constrained&quot; data-astro-image-pos=&quot;center&quot; width=&quot;1002&quot; height=&quot;373&quot; src=&quot;https://iercan.com/_astro/multiPong.CfTywvdm_Gb9dz.webp&quot;&gt;&lt;/p&gt;
&lt;p&gt;The dyno would sleep after 30 minutes of inactivity, so the first player to join had to wait for it to wake up. A bit annoying, but it was free.&lt;/p&gt;
&lt;h2 id=&quot;why-i-ended-up-building-my-own-roblox-like-engine&quot;&gt;Why I ended up building my own Roblox-like engine&lt;/h2&gt;
&lt;p&gt;After Pong, I kept making other multiplayer games like a top-down 2D shooter called “Akrely” that I never released (~2018). They were fun, and I learned a ton about networking&lt;/p&gt;
&lt;p&gt;But every time I started a new multiplayer project, I found myself reimplementing the same boilerplate: networking code, the game loop, rendering, serialization, event systems, input handling you name it.&lt;/p&gt;
&lt;p&gt;I play a lot of Source engine games, which are famous for their massive library of player-made mods and custom servers. Then I looked at Roblox a game engine with a built-in multiplayer system. Even though I don’t really play it, the concept was immediately appealing: just write a few lines of script (Lua in Roblox’s case) and have a multiplayer game running.&lt;/p&gt;
&lt;p&gt;I was sold. Let’s build that, but for the browser.&lt;/p&gt;
&lt;p&gt;But wait, how? What architecture? How much will it cost? How long will it take?&lt;/p&gt;
&lt;p&gt;Only one way to find out.&lt;/p&gt;
&lt;h2 id=&quot;why-not-use-an-existing-engine&quot;&gt;Why not use an existing engine?&lt;/h2&gt;
&lt;p&gt;I don’t use Unity, Unreal, or Godot.&lt;/p&gt;
&lt;p&gt;I want to build this from “scratch” (using libraries, obviously, but not a full engine), because that gives me:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Fast first-load times&lt;/li&gt;
&lt;li&gt;A native web experience&lt;/li&gt;
&lt;li&gt;No Unity splash screen, no Godot export bloat, no Unreal on Linux headaches&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Those engines produce WebGL exports via &lt;a href=&quot;https://emscripten.org/&quot;&gt;Emscripten&lt;/a&gt;, which generates a WASM binary plus JS glue code that’s typically several megabytes. It works fine for many use cases Unity WebGL games are pretty common, but that overhead isn’t something I want to carry.&lt;/p&gt;
&lt;h2 id=&quot;prototype-1-oop&quot;&gt;Prototype 1: OOP&lt;/h2&gt;
&lt;h3 id=&quot;project-setup&quot;&gt;Project setup&lt;/h3&gt;
&lt;p&gt;I started with separate repos for frontend and backend. The initial prototype used a simple OOP approach: a base &lt;code&gt;GameObject&lt;/code&gt; class with subclasses for players, NPCs, items, and so on.&lt;/p&gt;
&lt;p&gt;The split looked like this:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;multiroblox-back&lt;/strong&gt; Node.js, cannon.js (physics), WebSocket server&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;multiroblox-front&lt;/strong&gt; TypeScript, Three.js (rendering), WebSocket client&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img alt=&quot;MultiRoblox&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; data-astro-image=&quot;constrained&quot; data-astro-image-pos=&quot;center&quot; width=&quot;1467&quot; height=&quot;882&quot; src=&quot;https://iercan.com/_astro/multiroblox.Cn7mrGkL_pEThI.webp&quot;&gt;
&lt;em&gt;2022 prototype&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Everything was cubes. Not exactly a graphical showcase.&lt;/p&gt;
&lt;h3 id=&quot;backend-base-gameobject-class&quot;&gt;Backend: base GameObject class&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;GameObject&lt;/code&gt; is an abstract class that all game objects extend. It handles the basic properties (position, rotation) and physics wiring (shape for collisions, body for simulation).&lt;/p&gt;
&lt;p&gt;The &lt;code&gt;Serializable&lt;/code&gt; interface marks classes that can be converted to a network-friendly format (JSON or binary).&lt;/p&gt;
&lt;pre class=&quot;astro-code github-light&quot; style=&quot;background-color:#fff;color:#24292e; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;typescript&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;// GameObject.ts&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;// BACKEND&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;export&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt; abstract&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt; class&lt;/span&gt;&lt;span style=&quot;color:#6F42C1&quot;&gt; GameObject&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt; implements&lt;/span&gt;&lt;span style=&quot;color:#6F42C1&quot;&gt; Serializable&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E36209&quot;&gt;  type&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#6F42C1&quot;&gt; TypeGameObject&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;  protected&lt;/span&gt;&lt;span style=&quot;color:#E36209&quot;&gt; body&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;?:&lt;/span&gt;&lt;span style=&quot;color:#6F42C1&quot;&gt; Body&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;  protected&lt;/span&gt;&lt;span style=&quot;color:#E36209&quot;&gt; shape&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;?:&lt;/span&gt;&lt;span style=&quot;color:#6F42C1&quot;&gt; Shape&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;  public&lt;/span&gt;&lt;span style=&quot;color:#E36209&quot;&gt; id&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt; string&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;  constructor&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;();&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;  abstract&lt;/span&gt;&lt;span style=&quot;color:#6F42C1&quot;&gt; createShape&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;()&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt; void&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;  abstract&lt;/span&gt;&lt;span style=&quot;color:#6F42C1&quot;&gt; createBody&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;()&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt; void&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6F42C1&quot;&gt;  getPosition&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;()&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#6F42C1&quot;&gt; Vec3&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6F42C1&quot;&gt;  getRotation&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;()&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#6F42C1&quot;&gt; Quaternion&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6F42C1&quot;&gt;  setPosition&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#E36209&quot;&gt;vec3&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#6F42C1&quot;&gt; Vec3&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;)&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt; void&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6F42C1&quot;&gt;  addToWorld&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#E36209&quot;&gt;world&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#6F42C1&quot;&gt; World&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;)&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt; void&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6F42C1&quot;&gt;  serialize&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;()&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt; any&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;  abstract&lt;/span&gt;&lt;span style=&quot;color:#6F42C1&quot;&gt; update&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#E36209&quot;&gt;dt&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt; number&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;)&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt; void&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;// Simplified  full implementation omitted&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;From there, I could extend it to create specific game objects. Here’s the &lt;code&gt;Cube&lt;/code&gt; class:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-light&quot; style=&quot;background-color:#fff;color:#24292e; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;typescript&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;// Cube.ts&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;// BACKEND&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;export&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt; interface&lt;/span&gt;&lt;span style=&quot;color:#6F42C1&quot;&gt; SerializedCube&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E36209&quot;&gt;  position&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E36209&quot;&gt;    x&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt; number&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E36209&quot;&gt;    y&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt; number&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E36209&quot;&gt;    z&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt; number&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;  };&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E36209&quot;&gt;  rotation&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E36209&quot;&gt;    x&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt; number&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E36209&quot;&gt;    y&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt; number&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E36209&quot;&gt;    z&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt; number&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E36209&quot;&gt;    w&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt; number&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;  };&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E36209&quot;&gt;  size&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt; number&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;export&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt; class&lt;/span&gt;&lt;span style=&quot;color:#6F42C1&quot;&gt; Cube&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt; extends&lt;/span&gt;&lt;span style=&quot;color:#6F42C1&quot;&gt; GameObject&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt; implements&lt;/span&gt;&lt;span style=&quot;color:#6F42C1&quot;&gt; Serializable&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E36209&quot;&gt;  type&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#6F42C1&quot;&gt; TypeGameObject&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; TypeGameObject.&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;CUBE&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E36209&quot;&gt;  size&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt; number&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E36209&quot;&gt;  color&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt; number&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;  constructor&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#E36209&quot;&gt;position&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#6F42C1&quot;&gt; Vec3&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; Vec3.&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;ZERO&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#E36209&quot;&gt;size&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt; 1&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#E36209&quot;&gt;color&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt; 0xffffff&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;) {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;    super&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;();&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;    this&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;.size &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; size;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;    this&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;.color &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; color;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;    this&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#6F42C1&quot;&gt;createShape&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;();&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;    this&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#6F42C1&quot;&gt;createBody&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;();&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;    this&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#6F42C1&quot;&gt;setPosition&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;(position);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;  }&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6F42C1&quot;&gt;  createShape&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;()&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt; void&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;    this&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;.shape &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt; new&lt;/span&gt;&lt;span style=&quot;color:#6F42C1&quot;&gt; Box&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;new&lt;/span&gt;&lt;span style=&quot;color:#6F42C1&quot;&gt; Vec3&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;this&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;.size, &lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;this&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;.size, &lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;this&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;.size));&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;  }&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6F42C1&quot;&gt;  createBody&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;()&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt; void&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;    this&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;.body &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt; new&lt;/span&gt;&lt;span style=&quot;color:#6F42C1&quot;&gt; Body&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;({ mass: &lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;10000&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; });&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;    this&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;.body.&lt;/span&gt;&lt;span style=&quot;color:#6F42C1&quot;&gt;addShape&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;this&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;.shape&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;!&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;  }&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6F42C1&quot;&gt;  serialize&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;()&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#6F42C1&quot;&gt; SerializedCube&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;    return&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;      position: &lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;this&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#6F42C1&quot;&gt;getPosition&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;(),&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;      rotation: &lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;this&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#6F42C1&quot;&gt;getRotation&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;(),&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;      color: &lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;this&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;.color,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;      size: &lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;this&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;.size,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;      type: &lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;this&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;.type, &lt;/span&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;// used to know which class to instantiate on deserialization&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;      id: &lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;this&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;.id,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;    };&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;  }&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6F42C1&quot;&gt;  update&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#E36209&quot;&gt;dt&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt; number&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;) {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;    return&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;; &lt;/span&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;// no special update logic for now&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;  }&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;frontend-networkgameobject&quot;&gt;Frontend: NetworkGameObject&lt;/h3&gt;
&lt;p&gt;On the frontend, I defined a &lt;code&gt;NetworkGameObject&lt;/code&gt; interface to mark objects that can be synchronized over the network:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-light&quot; style=&quot;background-color:#fff;color:#24292e; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;typescript&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;// gameobject.ts&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;// FRONTEND - src/gameobjects/gameobject.ts&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;export&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt; interface&lt;/span&gt;&lt;span style=&quot;color:#6F42C1&quot;&gt; NetworkGameObject&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6F42C1&quot;&gt;  sync&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#E36209&quot;&gt;data&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt; any&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;)&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt; void&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6F42C1&quot;&gt;  update&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#E36209&quot;&gt;lerpValue&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt; number&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;)&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt; void&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;; &lt;/span&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;// The lerp is handled at higher level in the game loop&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;code&gt;Cube&lt;/code&gt; class creates the Three.js mesh. &lt;code&gt;NetworkCube&lt;/code&gt; extends it to handle server sync, it stores &lt;code&gt;nextPosition&lt;/code&gt; and &lt;code&gt;nextRotation&lt;/code&gt; from incoming updates, then smoothly interpolates using &lt;code&gt;lerp&lt;/code&gt; and &lt;code&gt;slerp&lt;/code&gt; to avoid jitter between ticks.&lt;/p&gt;
&lt;pre class=&quot;astro-code github-light&quot; style=&quot;background-color:#fff;color:#24292e; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;typescript&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;// cube.ts&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;// FRONTEND - src/gameobjects/cube.ts&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;import&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt; *&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt; as&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; THREE &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;from&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt; &quot;three&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;import&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; { Renderable } &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;from&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt; &quot;../interfaces&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;import&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; { Renderer } &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;from&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt; &quot;../renderer&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;import&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; { NetworkGameObject } &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;from&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt; &quot;./gameobject&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;export&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt; class&lt;/span&gt;&lt;span style=&quot;color:#6F42C1&quot;&gt; Cube&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt; implements&lt;/span&gt;&lt;span style=&quot;color:#6F42C1&quot;&gt; Renderable&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E36209&quot;&gt;  mesh&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#6F42C1&quot;&gt; THREE&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#6F42C1&quot;&gt;Mesh&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E36209&quot;&gt;  material&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#6F42C1&quot;&gt; THREE&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#6F42C1&quot;&gt;MeshPhongMaterial&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;  constructor&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#E36209&quot;&gt;size&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt; number&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt; 1&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#E36209&quot;&gt;color&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt; number&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;) {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;    // Rendered size = Physics size * 2&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;    const&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt; geometry&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt; new&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt; THREE&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#6F42C1&quot;&gt;BoxGeometry&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;(size &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;*&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt; 2&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;, size &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;*&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt; 2&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;, size &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;*&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt; 2&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;    this&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;.material &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt; new&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt; THREE&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#6F42C1&quot;&gt;MeshPhongMaterial&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;();&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;    this&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;.material.color &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt; new&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt; THREE&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#6F42C1&quot;&gt;Color&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;(color);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;    this&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;.mesh &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt; new&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt; THREE&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#6F42C1&quot;&gt;Mesh&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;(geometry, &lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;this&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;.material);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;    this&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;.mesh.receiveShadow &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt; true&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;    this&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;.mesh.castShadow &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt; true&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;  }&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6F42C1&quot;&gt;  addToScene&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;() {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;    Renderer.instance.scene.&lt;/span&gt;&lt;span style=&quot;color:#6F42C1&quot;&gt;add&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;this&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;.mesh);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;  }&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;export&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt; class&lt;/span&gt;&lt;span style=&quot;color:#6F42C1&quot;&gt; NetworkCube&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt; extends&lt;/span&gt;&lt;span style=&quot;color:#6F42C1&quot;&gt; Cube&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt; implements&lt;/span&gt;&lt;span style=&quot;color:#6F42C1&quot;&gt; NetworkGameObject&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;  public&lt;/span&gt;&lt;span style=&quot;color:#E36209&quot;&gt; id&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#6F42C1&quot;&gt; String&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;  public&lt;/span&gt;&lt;span style=&quot;color:#E36209&quot;&gt; nextPosition&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt; new&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt; THREE&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#6F42C1&quot;&gt;Vector3&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;();&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;  public&lt;/span&gt;&lt;span style=&quot;color:#E36209&quot;&gt; nextRotation&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt; new&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt; THREE&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#6F42C1&quot;&gt;Quaternion&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;();&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;  constructor&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#E36209&quot;&gt;id&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#6F42C1&quot;&gt; String&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#E36209&quot;&gt;size&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt; number&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#E36209&quot;&gt;color&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt; number&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;) {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;    super&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;(size, color);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;    this&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;.id &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; id;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;  }&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6F42C1&quot;&gt;  sync&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#E36209&quot;&gt;data&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#6F42C1&quot;&gt; SerializedCube&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;) {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;    this&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;.nextPosition &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt; new&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt; THREE&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#6F42C1&quot;&gt;Vector3&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;(&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;      data.position.x,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;      data.position.y,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;      data.position.z,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;    );&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;    this&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;.nextRotation &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt; new&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt; THREE&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#6F42C1&quot;&gt;Quaternion&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;(&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;      data.rotation.x,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;      data.rotation.y,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;      data.rotation.z,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;      data.rotation.w,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;    );&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;  }&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6F42C1&quot;&gt;  update&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#E36209&quot;&gt;lerpValue&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt; number&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;) {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;    // Without interpolation, movement is visibly choppy:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;    // this.mesh.position.copy(this.nextPosition);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;    // this.mesh.rotation.setFromQuaternion(this.nextRotation);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;    this&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;.mesh.position.&lt;/span&gt;&lt;span style=&quot;color:#6F42C1&quot;&gt;lerp&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;this&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;.nextPosition, lerpValue);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;    this&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;.mesh.quaternion.&lt;/span&gt;&lt;span style=&quot;color:#6F42C1&quot;&gt;slerp&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;this&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;.nextRotation, lerpValue);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;  }&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;netcode-snapshot-based&quot;&gt;Netcode: snapshot-based&lt;/h3&gt;
&lt;p&gt;I went with &lt;strong&gt;snapshot-based netcode&lt;/strong&gt;, sending the full game state to clients on every tick.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Tickrate&lt;/strong&gt; is how many times per second the server updates and broadcasts the game state. Higher tickrate means smoother gameplay, but also higher bandwidth usage and server load.&lt;/p&gt;
&lt;h4 id=&quot;a-naive-snapshot&quot;&gt;A naive snapshot&lt;/h4&gt;
&lt;p&gt;Here’s what a raw snapshot looked like in this prototype:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-light&quot; style=&quot;background-color:#fff;color:#24292e; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;json&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;{&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;  &quot;position&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;: {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;    &quot;x&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;165.89569158479551&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;    &quot;y&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;-0.800051185115476&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;    &quot;z&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;47.85383001858688&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;  },&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;  &quot;rotation&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;: {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;    &quot;x&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;-0.00001084003292835272&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;    &quot;y&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;-0.03619627233611027&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;    &quot;z&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;0.000027550383514014802&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;    &quot;w&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;0.9993446997870356&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;  },&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;  &quot;color&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;16777215&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;  &quot;size&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;0.2&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;  &quot;type&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;CUBE&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;  &quot;id&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;fvipvHJY8W&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;{&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;  &quot;position&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;: {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;    &quot;x&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;225.74027673479415&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;    &quot;y&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;-0.800055555679223&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;    &quot;z&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;45.81825443369147&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;  },&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;  &quot;rotation&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;: {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;    &quot;x&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;0.000008149242908244306&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;    &quot;y&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;0.9108704402144395&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;    &quot;z&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;0.000024894037988968568&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;    &quot;w&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;0.4126924283984742&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;  },&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;  &quot;color&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;16777215&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;  &quot;size&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;0.2&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;  &quot;type&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;CUBE&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;  &quot;id&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;4OpTLQ8ANU&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;{&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;  &quot;position&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;: {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;    &quot;x&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;0&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;    &quot;y&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;-1&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;    &quot;z&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;0&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;  },&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;  &quot;rotation&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;: {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;    &quot;x&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;-0.7071067811865475&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;    &quot;y&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;0&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;    &quot;z&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;0&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;    &quot;w&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;0.7071067811865476&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;  },&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;  &quot;type&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;GROUND&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;  &quot;id&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt;&quot;1VXYDT4O6p&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This was naive for a few reasons:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Sending 16 decimal places for a coordinate like &lt;code&gt;165.8956915...&lt;/code&gt; is overkill. Three decimal places (&lt;code&gt;165.896&lt;/code&gt;) is fine for most games.&lt;/li&gt;
&lt;li&gt;Repeating keys like &lt;code&gt;&quot;position&quot;&lt;/code&gt; and &lt;code&gt;&quot;rotation&quot;&lt;/code&gt; in every packet wastes bytes compared to binary formats like FlatBuffers or MessagePack.&lt;/li&gt;
&lt;li&gt;No &lt;strong&gt;delta compression&lt;/strong&gt;, if a cube barely moved, the server still sent its full state. Even the ground, which &lt;em&gt;never moves&lt;/em&gt;, got broadcasted every tick.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;em&gt;20 ticks per second&lt;/em&gt;
&lt;img alt=&quot;netcode&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; data-astro-image=&quot;constrained&quot; data-astro-image-pos=&quot;center&quot; width=&quot;2560&quot; height=&quot;1440&quot; src=&quot;https://iercan.com/_astro/netcode.3cB3y3hd_2neM85.webp&quot;&gt;&lt;/p&gt;
&lt;p&gt;At 20 ticks per second with 100 objects, this prototype was hitting &lt;strong&gt;500+ KB/s per player&lt;/strong&gt;. Binary encoding and delta compression could realistically cut that by an order of magnitude.&lt;/p&gt;
&lt;p&gt;It worked… barely. I had a game loop, 3D rendering, and server-authoritative state. That was enough to keep going.&lt;/p&gt;
&lt;p&gt;&lt;img alt=&quot;multiroblox demo&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; data-astro-image=&quot;constrained&quot; data-astro-image-pos=&quot;center&quot; width=&quot;2560&quot; height=&quot;1440&quot; src=&quot;https://iercan.com/_astro/robloxoldphysics.DzFDuJXF_21uscb.webp&quot;&gt;&lt;/p&gt;
&lt;h3 id=&quot;moving-away-from-oop&quot;&gt;Moving away from OOP&lt;/h3&gt;
&lt;p&gt;Class hierarchies are fine until your project grows. Then they become a liability.&lt;/p&gt;
&lt;p&gt;Want a cube that moves? Extend &lt;code&gt;Cube&lt;/code&gt;.
Now you want a cube that moves &lt;em&gt;and&lt;/em&gt; shoots. Do you extend &lt;code&gt;MovingCube&lt;/code&gt;? &lt;code&gt;ShootingCube&lt;/code&gt;? &lt;code&gt;MovingShootingCube&lt;/code&gt;?&lt;/p&gt;
&lt;p&gt;Rigid inheritance trees work against you in games, where concepts rarely fit neatly into a single hierarchy. A &lt;code&gt;Player&lt;/code&gt; is a &lt;code&gt;Character&lt;/code&gt; is a &lt;code&gt;GameObject&lt;/code&gt;, but what about a possessable turret, or an AI that behaves like a player? You end up with deep fragile trees, duplicated logic, and special-case workarounds everywhere just to share behavior.&lt;/p&gt;
&lt;p&gt;&lt;img alt=&quot;OOP&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; data-astro-image=&quot;constrained&quot; data-astro-image-pos=&quot;center&quot; width=&quot;1948&quot; height=&quot;858&quot; src=&quot;https://iercan.com/_astro/oop.BIEkIqYn_Z1t5scW.webp&quot;&gt;
&lt;strong&gt;Credit: &lt;a href=&quot;https://www.reddit.com/r/ProgrammerHumor/comments/60lm55/oop_what_actually_happens/&quot;&gt;OOP hell&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;What I needed was something more modular, where behaviors could be mixed, matched, and replaced per mini-game without rewriting large chunks of the codebase. It also needed to integrate cleanly with networking and state synchronization.&lt;/p&gt;
&lt;p&gt;I put this prototype on hold for a few months, but the idea kept nagging at me.&lt;/p&gt;
&lt;h2 id=&quot;prototype-2-ecs&quot;&gt;Prototype 2: ECS&lt;/h2&gt;
&lt;p&gt;After hitting the limits of inheritance-based design, I started looking for an architecture that could scale with complexity. That’s when I found the &lt;strong&gt;Entity Component System (ECS)&lt;/strong&gt; pattern.&lt;/p&gt;
&lt;p&gt;Most of us default to OOP because it feels natural, objects bundle data and behavior together. But in game development, that coupling quickly becomes a problem. Game objects change roles, gain or lose abilities, and share behaviors in unpredictable ways, exactly the kind of flexibility that rigid class hierarchies struggle to handle.&lt;/p&gt;
&lt;p&gt;ECS approaches the problem differently.&lt;/p&gt;
&lt;p&gt;Instead of modelling &lt;em&gt;what something is&lt;/em&gt;, it focuses on &lt;em&gt;what it has&lt;/em&gt; and &lt;em&gt;what systems act on it&lt;/em&gt;. This turns out to be a much better fit for dynamic game logic and networking.&lt;/p&gt;
&lt;h3 id=&quot;ecs-in-a-nutshell&quot;&gt;ECS in a nutshell&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Entities&lt;/strong&gt; are just unique IDs, they represent &lt;em&gt;existence&lt;/em&gt;, nothing more.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Components&lt;/strong&gt; are pure data attached to entities (position, velocity, health, input state…).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Systems&lt;/strong&gt; contain behavior and operate on all entities that have a matching set of components.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This makes behavior composable. An entity that can move has a &lt;code&gt;Position&lt;/code&gt; and &lt;code&gt;Velocity&lt;/code&gt;. An entity that can shoot has a &lt;code&gt;Weapon&lt;/code&gt; component. An entity that does both has both, no inheritance, no special cases.&lt;/p&gt;
&lt;pre class=&quot;astro-code github-light&quot; style=&quot;background-color:#fff;color:#24292e; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;typescript&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;// Component&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;class&lt;/span&gt;&lt;span style=&quot;color:#6F42C1&quot;&gt; Position&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;  constructor&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;(&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;    public&lt;/span&gt;&lt;span style=&quot;color:#E36209&quot;&gt; x&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt; number&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;    public&lt;/span&gt;&lt;span style=&quot;color:#E36209&quot;&gt; y&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt; number&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;    public&lt;/span&gt;&lt;span style=&quot;color:#E36209&quot;&gt; z&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt; number&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;  ) {}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;class&lt;/span&gt;&lt;span style=&quot;color:#6F42C1&quot;&gt; Velocity&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;  constructor&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;(&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;    public&lt;/span&gt;&lt;span style=&quot;color:#E36209&quot;&gt; vx&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt; number&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;    public&lt;/span&gt;&lt;span style=&quot;color:#E36209&quot;&gt; vy&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt; number&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;    public&lt;/span&gt;&lt;span style=&quot;color:#E36209&quot;&gt; vz&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt; number&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;  ) {}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;// Entity&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;const&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt; entity&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; world.&lt;/span&gt;&lt;span style=&quot;color:#6F42C1&quot;&gt;createEntity&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;();&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;world.&lt;/span&gt;&lt;span style=&quot;color:#6F42C1&quot;&gt;addComponent&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;(entity, &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;new&lt;/span&gt;&lt;span style=&quot;color:#6F42C1&quot;&gt; Position&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;0&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;0&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;0&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;));&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;world.&lt;/span&gt;&lt;span style=&quot;color:#6F42C1&quot;&gt;addComponent&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;(entity, &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;new&lt;/span&gt;&lt;span style=&quot;color:#6F42C1&quot;&gt; Velocity&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;1&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;0&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;0&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;));&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;// System&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;function&lt;/span&gt;&lt;span style=&quot;color:#6F42C1&quot;&gt; movementSystem&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#E36209&quot;&gt;dt&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt; number&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;) {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;  const&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt; entities&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; world.&lt;/span&gt;&lt;span style=&quot;color:#6F42C1&quot;&gt;getEntitiesWithComponents&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;(Position, Velocity);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;  for&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; (&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;const&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt; entity&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt; of&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; entities) {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;    const&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt; pos&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; world.&lt;/span&gt;&lt;span style=&quot;color:#6F42C1&quot;&gt;getComponent&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;(entity, Position);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;    const&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt; vel&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; world.&lt;/span&gt;&lt;span style=&quot;color:#6F42C1&quot;&gt;getComponent&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;(entity, Velocity);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;    pos.x &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;+=&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; vel.vx &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;*&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; dt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;    pos.y &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;+=&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; vel.vy &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;*&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; dt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;    pos.z &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;+=&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; vel.vz &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;*&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; dt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;  }&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;what-i-wanted-from-the-engine&quot;&gt;What I wanted from the engine&lt;/h3&gt;
&lt;p&gt;I restarted the project from scratch with a clear list of requirements. The engine needed to provide:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;A networking layer (WebSocket-based)&lt;/li&gt;
&lt;li&gt;A game loop&lt;/li&gt;
&lt;li&gt;3D rendering&lt;/li&gt;
&lt;li&gt;3D physics&lt;/li&gt;
&lt;li&gt;Compatibility with low-end browsers&lt;/li&gt;
&lt;li&gt;High flexibility and modularity&lt;/li&gt;
&lt;li&gt;A scripting layer to inject game logic at runtime (think Lua scripts in Roblox)&lt;/li&gt;
&lt;li&gt;Scalability (multiple server instances, room management, load balancing)&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;monorepo-setup&quot;&gt;Monorepo setup&lt;/h3&gt;
&lt;p&gt;In the first prototype, I split things across two separate repos. That created friction every time I needed to share types between the frontend and backend, you’d normally codegen or manually duplicate them. I didn’t want that overhead.&lt;/p&gt;
&lt;p&gt;So: monorepo.&lt;/p&gt;
&lt;p&gt;&lt;img alt=&quot;MonoRepo&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; data-astro-image=&quot;constrained&quot; data-astro-image-pos=&quot;center&quot; width=&quot;1007&quot; height=&quot;369&quot; src=&quot;https://iercan.com/_astro/MonoRepo.DL4K4pQR_Z1N7RfP.webp&quot;&gt;&lt;/p&gt;
&lt;p&gt;The structure:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;back&lt;/strong&gt; Node.js + &lt;a href=&quot;https://github.com/uNetworking/uWebSockets.js&quot;&gt;uWebSockets.js&lt;/a&gt; + &lt;a href=&quot;https://rapier.rs/&quot;&gt;Rapier.js&lt;/a&gt; (physics)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;front&lt;/strong&gt; TypeScript + &lt;a href=&quot;https://threejs.org/&quot;&gt;Three.js&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;shared&lt;/strong&gt; netcode types, ECS core, serialization&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;netcode-architecture-decisions&quot;&gt;Netcode architecture decisions&lt;/h3&gt;
&lt;p&gt;I went with &lt;strong&gt;server authority only&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;Everything is decided by the server. Clients do nothing more than send inputs and render the game state they receive. If you know netcode at all, you know this is the simplest possible architecture and also the laggiest if you’re far from the server.&lt;/p&gt;
&lt;p&gt;And yes, that is &lt;em&gt;exactly&lt;/em&gt; how this implementation works. 🤫&lt;/p&gt;
&lt;p&gt;Despite that, the tradeoffs are worth it:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;No cheating&lt;/strong&gt; the server is authoritative. Clients can’t lie about their position or state.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Simplicity&lt;/strong&gt; no client-side prediction, no reconciliation, no divergence to debug.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Consistency&lt;/strong&gt; every player sees the same world.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Proper collisions&lt;/strong&gt; with server-side physics, two cars colliding at 100 km/h resolve correctly. Client-side prediction would have each client extrapolating their own position, leading to rubber-banding when the server corrects them.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Server-only game logic&lt;/strong&gt; the game &lt;em&gt;script&lt;/em&gt; runs entirely on the server.&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;ℹ️ We’ll get into what &lt;em&gt;scripts&lt;/em&gt; are later. Think of them as the runtime game logic attached when a game starts, similar to Lua scripts in Roblox.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 id=&quot;basic-ecs-setup&quot;&gt;Basic ECS setup&lt;/h3&gt;
&lt;p&gt;I wrote my own ECS implementation in TypeScript. The &lt;code&gt;back&lt;/code&gt;, &lt;code&gt;front&lt;/code&gt;, and &lt;code&gt;shared&lt;/code&gt; packages all import ECS code from the shared folder.&lt;/p&gt;
&lt;p&gt;Entity management is handled by an &lt;code&gt;EntityManager&lt;/code&gt; singleton that creates/destroys entities and manages component attachment.&lt;/p&gt;
&lt;p&gt;Components are simple, they just hold data:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-light&quot; style=&quot;background-color:#fff;color:#24292e; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;typescript&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;// Component.ts&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;export&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt; class&lt;/span&gt;&lt;span style=&quot;color:#6F42C1&quot;&gt; Component&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;  constructor&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;public&lt;/span&gt;&lt;span style=&quot;color:#E36209&quot;&gt; entityId&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt; number&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;) {}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;A &lt;code&gt;PositionComponent&lt;/code&gt; can extend this base class. Depending on whether a component needs to be synchronized over the network, it either extends &lt;code&gt;NetworkComponent&lt;/code&gt; or plain &lt;code&gt;Component&lt;/code&gt;.&lt;/p&gt;
&lt;h3 id=&quot;introducing-networkcomponents&quot;&gt;Introducing NetworkComponents&lt;/h3&gt;
&lt;p&gt;Let’s take a simple cube.&lt;/p&gt;
&lt;p&gt;&lt;img alt=&quot;cube&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; data-astro-image=&quot;constrained&quot; data-astro-image-pos=&quot;center&quot; width=&quot;961&quot; height=&quot;584&quot; src=&quot;https://iercan.com/_astro/cube.UTeE4yuD_ZaCpG6.webp&quot;&gt;&lt;/p&gt;
&lt;p&gt;To render it with &lt;a href=&quot;https://threejs.org/docs/#BoxGeometry&quot;&gt;Three.js&lt;/a&gt;:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-light&quot; style=&quot;background-color:#fff;color:#24292e; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;typescript&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;const&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt; size&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; { width: &lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;1&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;, height: &lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;1&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;, depth: &lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;1&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; };&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;const&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt; geometry&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt; new&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt; THREE&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#6F42C1&quot;&gt;BoxGeometry&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;(size.width, size.height, size.depth);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;const&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt; material&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt; new&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt; THREE&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#6F42C1&quot;&gt;MeshBasicMaterial&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;({ color: &lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;0x00ff00&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; });&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;const&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt; cube&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt; new&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt; THREE&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#6F42C1&quot;&gt;Mesh&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;(geometry, material);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;scene.&lt;/span&gt;&lt;span style=&quot;color:#6F42C1&quot;&gt;add&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;(cube);&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If we think about what the frontend actually needs to represent this cube, we can decompose it into components:&lt;/p&gt;
&lt;p&gt;&lt;img alt=&quot;ecs&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; data-astro-image=&quot;constrained&quot; data-astro-image-pos=&quot;center&quot; width=&quot;1536&quot; height=&quot;1024&quot; src=&quot;https://iercan.com/_astro/ecs.CQE9zZQF_1UDH1D.webp&quot;&gt;&lt;/p&gt;
&lt;p&gt;Each visual property maps to an ECS component:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Color&lt;/strong&gt;:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-light&quot; style=&quot;background-color:#fff;color:#24292e; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;typescript&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;// ECS Component&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6F42C1&quot;&gt;ColorComponent&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;: {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6F42C1&quot;&gt;  hex&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;: string;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;// Three.js mapping&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;const&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt; material&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt; new&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt; THREE&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#6F42C1&quot;&gt;MeshBasicMaterial&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;({ color: &lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;0x00ff00&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; });&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Size&lt;/strong&gt;:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-light&quot; style=&quot;background-color:#fff;color:#24292e; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;typescript&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;// ECS Component&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6F42C1&quot;&gt;SizeComponent&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;: { &lt;/span&gt;&lt;span style=&quot;color:#6F42C1&quot;&gt;width&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;: number, &lt;/span&gt;&lt;span style=&quot;color:#6F42C1&quot;&gt;height&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;: number, &lt;/span&gt;&lt;span style=&quot;color:#6F42C1&quot;&gt;depth&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;: number }&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;// Three.js mapping&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;const&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt; geometry&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt; new&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt; THREE&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#6F42C1&quot;&gt;BoxGeometry&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;1&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;1&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;1&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;);&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Position&lt;/strong&gt;:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-light&quot; style=&quot;background-color:#fff;color:#24292e; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;typescript&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;// ECS Component&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6F42C1&quot;&gt;PositionComponent&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;: { &lt;/span&gt;&lt;span style=&quot;color:#6F42C1&quot;&gt;x&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;: number, &lt;/span&gt;&lt;span style=&quot;color:#6F42C1&quot;&gt;y&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;: number, &lt;/span&gt;&lt;span style=&quot;color:#6F42C1&quot;&gt;z&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;: number }&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;// Three.js mapping&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;cube.position.&lt;/span&gt;&lt;span style=&quot;color:#6F42C1&quot;&gt;set&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;(x, y, z);&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Rotation&lt;/strong&gt;:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-light&quot; style=&quot;background-color:#fff;color:#24292e; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;typescript&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;// ECS Component&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6F42C1&quot;&gt;RotationComponent&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;: { &lt;/span&gt;&lt;span style=&quot;color:#6F42C1&quot;&gt;x&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;: number, &lt;/span&gt;&lt;span style=&quot;color:#6F42C1&quot;&gt;y&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;: number, &lt;/span&gt;&lt;span style=&quot;color:#6F42C1&quot;&gt;z&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;: number, &lt;/span&gt;&lt;span style=&quot;color:#6F42C1&quot;&gt;w&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;: number } &lt;/span&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;// quaternion&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;// Three.js mapping&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;cube.quaternion.&lt;/span&gt;&lt;span style=&quot;color:#6F42C1&quot;&gt;set&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;(x, y, z, w);&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;networkcomponents-beyond-primitive-shapes&quot;&gt;NetworkComponents: beyond primitive shapes&lt;/h3&gt;
&lt;p&gt;I didn’t want to be limited to cubes and spheres. In Notblox, I use a &lt;code&gt;ServerMeshComponent&lt;/code&gt; that references a 3D model file:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-light&quot; style=&quot;background-color:#fff;color:#24292e; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;typescript&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;// ECS Component&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6F42C1&quot;&gt;ServerMeshComponent&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;: {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6F42C1&quot;&gt;  filePath&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;: string;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;} &lt;/span&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;// path to .glb, .obj, etc.&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;// Three.js loading&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;const&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt; loader&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt; new&lt;/span&gt;&lt;span style=&quot;color:#6F42C1&quot;&gt; GLTFLoader&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;();&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;loader.&lt;/span&gt;&lt;span style=&quot;color:#6F42C1&quot;&gt;load&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;(filePath, (&lt;/span&gt;&lt;span style=&quot;color:#E36209&quot;&gt;gltf&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;) &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;=&amp;gt;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;  scene.&lt;/span&gt;&lt;span style=&quot;color:#6F42C1&quot;&gt;add&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;(gltf.scene);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;});&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Models can be hosted on S3 with CDN caching for fast delivery, giving the flexibility to load any 3D asset dynamically rather than bundling everything upfront.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;⚠️ This creates a dependency on external assets. You need to handle CDN downtime, slow connections, and missing files gracefully, otherwise your game silently has invisible objects.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Three.js provides the &lt;a href=&quot;https://threejs.org/docs/#examples/en/loaders/GLTFLoader&quot;&gt;GLTFLoader&lt;/a&gt; for loading glTF/GLB models. GLB (binary glTF) is more efficient for web delivery. File size matters a lot for first-load experience.&lt;/p&gt;
&lt;h3 id=&quot;networkcomponents-syncing-over-the-wire&quot;&gt;NetworkComponents: syncing over the wire&lt;/h3&gt;
&lt;p&gt;I intentionally separated &lt;code&gt;NetworkComponent&lt;/code&gt; from regular &lt;code&gt;Component&lt;/code&gt;. A &lt;code&gt;NetworkComponent&lt;/code&gt; is one that needs to be synchronized between server and clients, it exposes &lt;code&gt;serialize()&lt;/code&gt; and &lt;code&gt;deserialize()&lt;/code&gt; methods.&lt;/p&gt;
&lt;pre class=&quot;astro-code github-light&quot; style=&quot;background-color:#fff;color:#24292e; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;typescript&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;// NetworkComponent.ts&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;// shared/component/NetworkComponent.ts&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;export&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt; abstract&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt; class&lt;/span&gt;&lt;span style=&quot;color:#6F42C1&quot;&gt; NetworkComponent&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt; extends&lt;/span&gt;&lt;span style=&quot;color:#6F42C1&quot;&gt; Component&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E36209&quot;&gt;  updated&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt; boolean&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt; true&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;  constructor&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;(&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E36209&quot;&gt;    entityId&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt; number&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;    public&lt;/span&gt;&lt;span style=&quot;color:#E36209&quot;&gt; type&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#6F42C1&quot;&gt; SerializedComponentType&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; SerializedComponentType.&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;NONE&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;  ) {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;    super&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;(entityId);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;  }&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;  abstract&lt;/span&gt;&lt;span style=&quot;color:#6F42C1&quot;&gt; serialize&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;()&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt; any&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;  abstract&lt;/span&gt;&lt;span style=&quot;color:#6F42C1&quot;&gt; deserialize&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#E36209&quot;&gt;data&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt; any&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;)&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt; void&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Here’s the &lt;code&gt;PositionComponent&lt;/code&gt; as a concrete example:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-light&quot; style=&quot;background-color:#fff;color:#24292e; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;typescript&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;// PositionComponent.ts&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;// shared/component/PositionComponent.ts&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;export&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt; class&lt;/span&gt;&lt;span style=&quot;color:#6F42C1&quot;&gt; PositionComponent&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt; extends&lt;/span&gt;&lt;span style=&quot;color:#6F42C1&quot;&gt; NetworkComponent&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;  constructor&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;(&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E36209&quot;&gt;    entityId&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt; number&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;    public&lt;/span&gt;&lt;span style=&quot;color:#E36209&quot;&gt; x&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt; number&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;    public&lt;/span&gt;&lt;span style=&quot;color:#E36209&quot;&gt; y&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt; number&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;    public&lt;/span&gt;&lt;span style=&quot;color:#E36209&quot;&gt; z&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt; number&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;  ) {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;    super&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;(entityId, SerializedComponentType.&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;POSITION&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;  }&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6F42C1&quot;&gt;  deserialize&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#E36209&quot;&gt;data&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#6F42C1&quot;&gt; SerializedPositionComponent&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;)&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt; void&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;    this&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;.x &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; data.x;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;    this&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;.y &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; data.y;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;    this&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;.z &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; data.z;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;  }&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6F42C1&quot;&gt;  serialize&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;()&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#6F42C1&quot;&gt; SerializedPositionComponent&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;    return&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;      x: &lt;/span&gt;&lt;span style=&quot;color:#6F42C1&quot;&gt;Number&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;this&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;.x.&lt;/span&gt;&lt;span style=&quot;color:#6F42C1&quot;&gt;toFixed&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;2&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;)), &lt;/span&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;// quantize to 2 decimal places&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;      y: &lt;/span&gt;&lt;span style=&quot;color:#6F42C1&quot;&gt;Number&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;this&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;.y.&lt;/span&gt;&lt;span style=&quot;color:#6F42C1&quot;&gt;toFixed&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;2&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;)),&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;      z: &lt;/span&gt;&lt;span style=&quot;color:#6F42C1&quot;&gt;Number&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;this&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;.z.&lt;/span&gt;&lt;span style=&quot;color:#6F42C1&quot;&gt;toFixed&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;2&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;)),&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;    };&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;  }&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;export&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt; interface&lt;/span&gt;&lt;span style=&quot;color:#6F42C1&quot;&gt; SerializedPositionComponent&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt; extends&lt;/span&gt;&lt;span style=&quot;color:#6F42C1&quot;&gt; SerializedComponent&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E36209&quot;&gt;  x&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt; number&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E36209&quot;&gt;  y&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt; number&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E36209&quot;&gt;  z&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt; number&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Since this lives in the &lt;code&gt;shared&lt;/code&gt; folder, both the frontend and backend use the exact same serialization code, no codegen, no drift.&lt;/p&gt;
&lt;p&gt;The &lt;code&gt;SerializedComponent&lt;/code&gt; interface and its type enum look like this:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-light&quot; style=&quot;background-color:#fff;color:#24292e; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;typescript&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;// SerializedComponent.ts&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;// shared/component/SerializedComponent.ts&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;export&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt; interface&lt;/span&gt;&lt;span style=&quot;color:#6F42C1&quot;&gt; SerializedComponent&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E36209&quot;&gt;  t&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;?:&lt;/span&gt;&lt;span style=&quot;color:#6F42C1&quot;&gt; SerializedComponentType&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;; &lt;/span&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;// discriminator sent over the wire&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;export&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt; enum&lt;/span&gt;&lt;span style=&quot;color:#6F42C1&quot;&gt; SerializedComponentType&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;  NONE&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt; 0&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;  POSITION&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt; 1&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;  ROTATION&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt; 2&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;  SIZE&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt; 3&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;  COLOR&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt; 4&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;  ENTITY_DESTROYED_EVENT&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt; 5&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;  SINGLE_SIZE&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt; 6&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;  STATE&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt; 7&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;// used for animations&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;  CHAT_LIST&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt; 8&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;  MESSAGE&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt; 9&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;  SERVER_MESH&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt; 10&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;  PROXIMITY_PROMPT&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt; 11&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;  TEXT&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt; 12&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;  VEHICLE&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt; 13&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;  PLAYER&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt; 14&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;  VEHICLE_OCCUPANCY&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt; 15&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;  COMPONENT_REMOVED_EVENT&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt; 16&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;  WHEEL&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt; 17&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;  INVISIBLE&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt; 18&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Putting it all together, here’s what creating a cube entity looks like on the server:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-light&quot; style=&quot;background-color:#fff;color:#24292e; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;typescript&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;// Cube.ts&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;// back/src/ecs/entity/Cube.ts&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;const&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt; cubeEntity&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt; EntityManager.&lt;/span&gt;&lt;span style=&quot;color:#6F42C1&quot;&gt;createEntity&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;(SerializedEntityType.&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;CUBE&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;const&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt; positionComponent&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt; new&lt;/span&gt;&lt;span style=&quot;color:#6F42C1&quot;&gt; PositionComponent&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;(&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;  cubeEntity.id,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;  position.x,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;  position.y,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;  position.z,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;cubeEntity.&lt;/span&gt;&lt;span style=&quot;color:#6F42C1&quot;&gt;addComponent&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;(positionComponent);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;cubeEntity.&lt;/span&gt;&lt;span style=&quot;color:#6F42C1&quot;&gt;addComponent&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;(rotationComponent);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;const&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt; serverMeshComponent&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt; new&lt;/span&gt;&lt;span style=&quot;color:#6F42C1&quot;&gt; ServerMeshComponent&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;(&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;  cubeEntity.id,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#032F62&quot;&gt;  &quot;https://notbloxo.fra1.cdn.digitaloceanspaces.com/Notblox-Assets/base/Crate.glb&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;cubeEntity.&lt;/span&gt;&lt;span style=&quot;color:#6F42C1&quot;&gt;addComponent&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;(serverMeshComponent);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;const&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt; sizeComponent&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt; new&lt;/span&gt;&lt;span style=&quot;color:#6F42C1&quot;&gt; SizeComponent&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;(cubeEntity.id, &lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;1&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;1&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt;1&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;cubeEntity.&lt;/span&gt;&lt;span style=&quot;color:#6F42C1&quot;&gt;addComponent&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;(sizeComponent);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;const&lt;/span&gt;&lt;span style=&quot;color:#005CC5&quot;&gt; colorComponent&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt; new&lt;/span&gt;&lt;span style=&quot;color:#6F42C1&quot;&gt; ColorComponent&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;(cubeEntity.id, color &lt;/span&gt;&lt;span style=&quot;color:#D73A49&quot;&gt;??&lt;/span&gt;&lt;span style=&quot;color:#032F62&quot;&gt; &quot;default&quot;&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#24292E&quot;&gt;cubeEntity.&lt;/span&gt;&lt;span style=&quot;color:#6F42C1&quot;&gt;addComponent&lt;/span&gt;&lt;span style=&quot;color:#24292E&quot;&gt;(colorComponent);&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
</content:encoded><category>gamedev</category><category>webgames</category><category>backend</category><category>networking</category></item><item><title>why?</title><link>https://iercan.com/posts/why-i-made-this-blog/</link><guid isPermaLink="true">https://iercan.com/posts/why-i-made-this-blog/</guid><description>Sharing my projects &amp; experiments</description><pubDate>Tue, 06 Jan 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;img src=&quot;https://iercan.com/_astro/cover.Br9ki_Pw.png&quot; alt=&quot;My GitHub profile, 106 repositories&quot;&gt;&lt;/p&gt;&lt;p&gt;I’ve always wanted a place to show what I do, most of my projects end up in private GitHub repos I never touch again..&lt;/p&gt;
&lt;p&gt;Nothing here has to be perfect!&lt;/p&gt;
&lt;p&gt;Developing is messy, full of trial and error, and that’s okay. I just want to show some projects I work on, the experiments I try..&lt;/p&gt;
&lt;p&gt;Hopefully this can inspire or help someone, or at least give a peek into stuff I’m working on in my free time :)&lt;/p&gt;
&lt;p&gt;There is this blog I often often read: &lt;a href=&quot;https://fabiensanglard.net/&quot;&gt;https://fabiensanglard.net/&lt;/a&gt;, even tho I don’t know much about low level programming, it’s always a fun read. why not make my own?&lt;/p&gt;
</content:encoded><category>blog</category><category>personal</category></item></channel></rss>