<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>Forem: Amir Reza Dalir</title>
    <description>The latest articles on Forem by Amir Reza Dalir (@dalirnet).</description>
    <link>https://forem.com/dalirnet</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F903715%2Ff0c2decf-15ee-491f-b293-43d9f134f9a9.jpeg</url>
      <title>Forem: Amir Reza Dalir</title>
      <link>https://forem.com/dalirnet</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/dalirnet"/>
    <language>en</language>
    <item>
      <title>No Docker Here: Welcome to Singularity 🔬</title>
      <dc:creator>Amir Reza Dalir</dc:creator>
      <pubDate>Wed, 25 Feb 2026 18:55:22 +0000</pubDate>
      <link>https://forem.com/dalirnet/no-docker-here-welcome-to-singularity-2b2g</link>
      <guid>https://forem.com/dalirnet/no-docker-here-welcome-to-singularity-2b2g</guid>
      <description>&lt;p&gt;I just joined a new project — one that runs on a big HPC cluster. I opened the project &lt;code&gt;README.md&lt;/code&gt; and saw something like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;singularity &lt;span class="nb"&gt;exec &lt;/span&gt;docker://python:3.11 python train_model.py &lt;span class="nt"&gt;--data&lt;/span&gt; /shared/datasets/train
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I had no idea what &lt;code&gt;singularity&lt;/code&gt; was. 😅 So I did what felt natural — typed the Docker command instead:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker run python:3.11 python train_model.py &lt;span class="nt"&gt;--data&lt;/span&gt; /shared/datasets/train
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And the terminal replied with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;bash: docker: &lt;span class="nb"&gt;command &lt;/span&gt;not found
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I messaged my project manager. His reply was short:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"We don't use Docker here. We use Singularity."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;I stared at the message, thinking: "I have been using Docker for years. I know &lt;code&gt;docker build&lt;/code&gt;, &lt;code&gt;docker run&lt;/code&gt;, &lt;code&gt;docker push&lt;/code&gt; like the back of my hand. And now none of that works here?"&lt;/p&gt;

&lt;p&gt;That's how it all started.&lt;/p&gt;




&lt;h2&gt;
  
  
  🤔 "Why Not Docker?"
&lt;/h2&gt;

&lt;p&gt;I &lt;strong&gt;loved&lt;/strong&gt; Docker. Years of packaging apps in containers, deploying to production, running ML training pipelines. It was part of how I worked every day.&lt;/p&gt;

&lt;p&gt;So I asked my project manager to install Docker on the cluster. His reply came quickly:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"We can't. There are legal reasons we can't use Docker here. Company policy. But also — Singularity is a better fit for what we do."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;He didn't go into all the legal details, but explained the technical side in a Google Meet session. &lt;strong&gt;Docker needs a background daemon running all the time&lt;/strong&gt; — a service that sits there waiting for your commands. On a shared HPC cluster where hundreds of researchers submit jobs, that adds complexity and overhead. &lt;strong&gt;Singularity doesn't need any daemon&lt;/strong&gt; — you just run it directly. No background process. And you are the same user inside the container as you are outside. No switching to root, no permission confusion.&lt;/p&gt;

&lt;p&gt;That last part sounded too good to be true. But it was true.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;What?&lt;/th&gt;
&lt;th&gt;Docker 🐳&lt;/th&gt;
&lt;th&gt;Singularity 🔬&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Main idea&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Isolation (microservices)&lt;/td&gt;
&lt;td&gt;Integration (HPC)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Background daemon?&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Yes (always running)&lt;/td&gt;
&lt;td&gt;No (daemonless)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Who are you inside?&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;root by default&lt;/td&gt;
&lt;td&gt;Same user as host&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Image format&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Multi-layer, managed by daemon&lt;/td&gt;
&lt;td&gt;Single &lt;code&gt;.sif&lt;/code&gt; file&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;File access&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Must mount volumes&lt;/td&gt;
&lt;td&gt;Auto-mounts &lt;code&gt;$HOME&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Network&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Isolated by default&lt;/td&gt;
&lt;td&gt;Host network&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;blockquote&gt;
&lt;p&gt;💡 Docker wants to &lt;strong&gt;isolate&lt;/strong&gt; your app from the system. Singularity wants to &lt;strong&gt;integrate&lt;/strong&gt; your app with the system.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That made sense. I started learning Singularity and writing down every Docker command I knew next to its Singularity equivalent. This article is that cheat sheet.&lt;/p&gt;




&lt;h2&gt;
  
  
  📦 Image Management
&lt;/h2&gt;

&lt;p&gt;First thing I needed was a Python image. In Docker, I would type &lt;code&gt;docker pull python:3.11&lt;/code&gt;. In Singularity, the command is almost the same — with one small twist:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;singularity pull docker://python:3.11
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;See that &lt;code&gt;docker://&lt;/code&gt; prefix? &lt;strong&gt;All my Docker Hub images still work&lt;/strong&gt;. Every image I had ever used — GPU-enabled, data science, notebook servers — still available.&lt;/p&gt;

&lt;p&gt;But instead of layers hidden in Docker's storage, I got a &lt;strong&gt;single file&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;ls&lt;/span&gt; &lt;span class="nt"&gt;-lh&lt;/span&gt; python_3.11.sif
&lt;span class="c"&gt;# -rwxr-xr-x 1 dalirnet dalirnet 385M Feb 25 09:15 python_3.11.sif&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That &lt;code&gt;.sif&lt;/code&gt; file &lt;strong&gt;is&lt;/strong&gt; the image. A real file in my directory. I can &lt;code&gt;cp&lt;/code&gt; it, &lt;code&gt;scp&lt;/code&gt; it to another node, or &lt;code&gt;rsync&lt;/code&gt; it anywhere. Try that with Docker — you need a registry, accounts, push, pull... With Singularity, you just copy a file.&lt;/p&gt;

&lt;p&gt;Want to delete it? &lt;code&gt;rm python_3.11.sif&lt;/code&gt;. No &lt;code&gt;docker system prune&lt;/code&gt;, no dangling images.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Task&lt;/th&gt;
&lt;th&gt;Docker 🐳&lt;/th&gt;
&lt;th&gt;Singularity 🔬&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Pull image&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;docker pull ubuntu:22.04&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;singularity pull docker://ubuntu:22.04&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Build image&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;docker build -t myimage .&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;singularity build myimage.sif myimage.def&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;List images&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;docker images&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;ls *.sif&lt;/code&gt; (they're just files!)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Inspect image&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;docker inspect myimage&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;singularity inspect myimage.sif&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  ▶️ Running Containers
&lt;/h2&gt;

&lt;p&gt;In Docker, I would do &lt;code&gt;docker run python:3.11 python script.py&lt;/code&gt;. In Singularity, the keyword is &lt;code&gt;exec&lt;/code&gt; instead of &lt;code&gt;run&lt;/code&gt; (&lt;code&gt;run&lt;/code&gt; exists too — more on that in a second):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;singularity &lt;span class="nb"&gt;exec &lt;/span&gt;python_3.11.sif python &lt;span class="nt"&gt;--version&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Task&lt;/th&gt;
&lt;th&gt;Docker 🐳&lt;/th&gt;
&lt;th&gt;Singularity 🔬&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Run a command&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;docker run myimage cmd&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;singularity exec myimage.sif cmd&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Interactive shell&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;docker run -it myimage bash&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;singularity shell myimage.sif&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Default command&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;docker run myimage&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;singularity run myimage.sif&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Background&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;docker run -d myimage&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;singularity instance start myimage.sif name&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  📂 File System &amp;amp; Volumes
&lt;/h2&gt;

&lt;p&gt;This is where Singularity first surprised me. I typed &lt;code&gt;singularity shell python_3.11.sif&lt;/code&gt;, then &lt;code&gt;ls&lt;/code&gt; — and saw &lt;strong&gt;all my files&lt;/strong&gt;. My notebooks. My training scripts. My config files. Everything from my home folder, right there.&lt;/p&gt;

&lt;p&gt;In Docker, you see an empty filesystem unless you mount your folder with &lt;code&gt;-v&lt;/code&gt;. In Singularity, your home directory, current working directory, &lt;code&gt;/tmp&lt;/code&gt;, and system paths like &lt;code&gt;/proc&lt;/code&gt; and &lt;code&gt;/sys&lt;/code&gt; are &lt;strong&gt;automatically available&lt;/strong&gt; ✅.&lt;/p&gt;

&lt;p&gt;So my old Docker habit:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker run &lt;span class="nt"&gt;-v&lt;/span&gt; &lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;pwd&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;:/workspace &lt;span class="nt"&gt;-w&lt;/span&gt; /workspace python:3.11 python analysis.py
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Becomes just:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;singularity &lt;span class="nb"&gt;exec &lt;/span&gt;docker://python:3.11 python analysis.py
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No &lt;code&gt;-v&lt;/code&gt;. No &lt;code&gt;-w&lt;/code&gt;. Your current directory is already there.&lt;/p&gt;

&lt;p&gt;When you need folders &lt;strong&gt;outside&lt;/strong&gt; your home directory, use &lt;code&gt;--bind&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;singularity &lt;span class="nb"&gt;exec&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--bind&lt;/span&gt; /shared/datasets:/data &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--bind&lt;/span&gt; /scratch/&lt;span class="nv"&gt;$USER&lt;/span&gt;:/scratch &lt;span class="se"&gt;\&lt;/span&gt;
    myimage.sif python train.py &lt;span class="nt"&gt;--data&lt;/span&gt; /data/train &lt;span class="nt"&gt;--output&lt;/span&gt; /scratch/checkpoints
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can also make binds read-only: &lt;code&gt;--bind /shared/datasets:/data:ro&lt;/code&gt;.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Task&lt;/th&gt;
&lt;th&gt;Docker 🐳&lt;/th&gt;
&lt;th&gt;Singularity 🔬&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Bind mount&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;-v /host:/container&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;--bind /host:/container&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Current dir&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;-v $(pwd):/app&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Already there&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Read-only&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;-v /host:/container:ro&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;--bind /host:/container:ro&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Multiple&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;-v /a:/a -v /b:/b&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;--bind /a:/a,/b:/b&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Working dir&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;-w /app&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;--pwd /app&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  ⚙️ Environment Variables
&lt;/h2&gt;

&lt;p&gt;This one caught me off guard. In Docker, the container starts with a &lt;strong&gt;clean&lt;/strong&gt; environment. If you need an API key inside, you pass it explicitly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker run &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="nv"&gt;API_KEY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;abc123 myimage python train.py
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Singularity does the opposite — it &lt;strong&gt;inherits your entire host environment&lt;/strong&gt; by default ✅. Your &lt;code&gt;$PATH&lt;/code&gt;, your custom variables — all available inside the container.&lt;/p&gt;

&lt;p&gt;At first I thought this was great. Then I hit a bug where my container's Python was fighting with my host's Python paths because environment variables were leaking in. That's when I learned about &lt;code&gt;--cleanenv&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Clean environment — recommended for reproducible experiments&lt;/span&gt;
singularity &lt;span class="nb"&gt;exec&lt;/span&gt; &lt;span class="nt"&gt;--cleanenv&lt;/span&gt; myimage.sif python train.py

&lt;span class="c"&gt;# Clean env + only the variables you need&lt;/span&gt;
singularity &lt;span class="nb"&gt;exec&lt;/span&gt; &lt;span class="nt"&gt;--cleanenv&lt;/span&gt; &lt;span class="nt"&gt;--env&lt;/span&gt; &lt;span class="nv"&gt;API_KEY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;abc123 myimage.sif python train.py

&lt;span class="c"&gt;# Or use an env file&lt;/span&gt;
singularity &lt;span class="nb"&gt;exec&lt;/span&gt; &lt;span class="nt"&gt;--cleanenv&lt;/span&gt; &lt;span class="nt"&gt;--env-file&lt;/span&gt; .env myimage.sif python train.py
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;My advice: &lt;strong&gt;always use &lt;code&gt;--cleanenv&lt;/code&gt; for training runs&lt;/strong&gt;. The inherited environment is handy for quick interactive work, but for anything reproducible, you want a clean slate.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Task&lt;/th&gt;
&lt;th&gt;Docker 🐳&lt;/th&gt;
&lt;th&gt;Singularity 🔬&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Set variable&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;-e VAR=value&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;--env VAR=value&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Inherit host env&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Default&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Env file&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;--env-file .env&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;--env-file .env&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Clean env&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Default&lt;/td&gt;
&lt;td&gt;&lt;code&gt;--cleanenv&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  🌐 Networking
&lt;/h2&gt;

&lt;p&gt;In Docker, every container gets its own isolated network. Want to run a notebook server? You need port mapping:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker run &lt;span class="nt"&gt;-p&lt;/span&gt; 8888:8888 mynotebook-image
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In Singularity, there is &lt;strong&gt;no network isolation&lt;/strong&gt;. The container uses the host network directly. Start a service on port 8888 inside the container, and it is on port 8888 on your machine. No mapping needed ✅:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;singularity &lt;span class="nb"&gt;exec &lt;/span&gt;myimage.sif python &lt;span class="nt"&gt;-m&lt;/span&gt; notebook &lt;span class="nt"&gt;--port&lt;/span&gt; 8888
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Running notebook servers, dashboards, monitoring tools — no more figuring out port mappings. Just start the service and go to &lt;code&gt;localhost:port&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The downside? Two users on the same node starting something on port 8888 will conflict. But our cluster setup handles that by assigning different ports.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Docker 🐳&lt;/th&gt;
&lt;th&gt;Singularity 🔬&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Default&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Isolated bridge&lt;/td&gt;
&lt;td&gt;Host network&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Port mapping&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;-p 8888:8888&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Not needed&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Custom network&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;docker network create&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Not available&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  🔄 Running Services (Instances)
&lt;/h2&gt;

&lt;p&gt;Sometimes you need something running in the background — a notebook server, a database, a monitoring dashboard. In Docker, you use &lt;code&gt;-d&lt;/code&gt; to detach. Singularity has &lt;strong&gt;instances&lt;/strong&gt;:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Task&lt;/th&gt;
&lt;th&gt;Docker 🐳&lt;/th&gt;
&lt;th&gt;Singularity 🔬&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Start&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;docker run -d --name nb myimage&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;singularity instance start myimage.sif nb&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;List&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;docker ps&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;singularity instance list&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Stop&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;docker stop nb&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;singularity instance stop nb&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Exec&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;docker exec nb ls&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;singularity exec instance://nb ls&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Logs&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;docker logs nb&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Check instance-specific logs&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;One important difference: in Docker, the daemon keeps your containers alive even if you log out. In Singularity, instances are tied to your session. If you disconnect from the cluster, they stop. For long-running services, you need a job scheduler — which is usually how HPC clusters work anyway.&lt;/p&gt;




&lt;h2&gt;
  
  
  📄 Definition Files
&lt;/h2&gt;

&lt;p&gt;Every Docker user knows the Dockerfile. Singularity has its own version called a &lt;strong&gt;definition file&lt;/strong&gt; (&lt;code&gt;.def&lt;/code&gt;). Different structure, same idea — a recipe for building your image.&lt;/p&gt;

&lt;p&gt;A Dockerfile:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="s"&gt; python:3.11-slim&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;pip &lt;span class="nb"&gt;install &lt;/span&gt;numpy pandas scikit-learn matplotlib
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; analysis.py /app/&lt;/span&gt;
&lt;span class="k"&gt;WORKDIR&lt;/span&gt;&lt;span class="s"&gt; /app&lt;/span&gt;
&lt;span class="k"&gt;CMD&lt;/span&gt;&lt;span class="s"&gt; ["python", "analysis.py"]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The same thing as a Singularity definition file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Bootstrap: docker
From: python:3.11-slim

%post
    pip install numpy pandas scikit-learn matplotlib

%files
    analysis.py /app/

%environment
    export LC_ALL=C

%runscript
    cd /app
    exec python analysis.py
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Dockerfile&lt;/th&gt;
&lt;th&gt;Singularity&lt;/th&gt;
&lt;th&gt;Purpose&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;FROM&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;Bootstrap: docker&lt;/code&gt; + &lt;code&gt;From:&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Base image&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;RUN&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;%post&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Build commands&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;COPY&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;%files&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Copy files in&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ENV&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;%environment&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Environment vars&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;CMD&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;%runscript&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Default command&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ENTRYPOINT&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;%startscript&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Instance command&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;LABEL&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;%labels&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Metadata&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;WORKDIR&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Set in &lt;code&gt;%runscript&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Working dir&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Most of the time, &lt;strong&gt;you don't need a &lt;code&gt;.def&lt;/code&gt; file at all&lt;/strong&gt;. If you already have a Docker image, convert it directly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;singularity build myenv.sif docker://myregistry/gpu-image:latest
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I only started writing &lt;code&gt;.def&lt;/code&gt; files when I needed a custom environment that didn't exist on Docker Hub. For everything else, &lt;code&gt;docker://&lt;/code&gt; was enough.&lt;/p&gt;

&lt;h3&gt;
  
  
  🧪 Sandbox Mode
&lt;/h3&gt;

&lt;p&gt;When I need to experiment with new packages before committing to a build, I create a writable sandbox — a draft environment I can mess around in, then freeze into a clean image:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;singularity build &lt;span class="nt"&gt;--sandbox&lt;/span&gt; myenv/ myenv.def   &lt;span class="c"&gt;# create writable folder&lt;/span&gt;
singularity shell &lt;span class="nt"&gt;--writable&lt;/span&gt; myenv/             &lt;span class="c"&gt;# shell in and install stuff&lt;/span&gt;
&lt;span class="c"&gt;# pip install some-new-package&lt;/span&gt;
&lt;span class="c"&gt;# python -c "import some_new_package"           # test it&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;singularity build myenv.sif myenv/         &lt;span class="c"&gt;# freeze when happy&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In Docker, you would do &lt;code&gt;docker run -it myimage bash&lt;/code&gt; then &lt;code&gt;docker commit&lt;/code&gt;, but the sandbox approach feels more intentional.&lt;/p&gt;




&lt;h2&gt;
  
  
  🎮 GPU Access
&lt;/h2&gt;

&lt;p&gt;This is where Singularity really shines.&lt;/p&gt;

&lt;p&gt;In Docker, you need &lt;code&gt;--gpus all&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker run &lt;span class="nt"&gt;--gpus&lt;/span&gt; all gpu-image:latest python train.py
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In Singularity, you add &lt;code&gt;--nv&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;singularity &lt;span class="nb"&gt;exec&lt;/span&gt; &lt;span class="nt"&gt;--nv&lt;/span&gt; gpu-image.sif python train.py
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's it. No nvidia-docker, no container runtime config, no Docker daemon GPU passthrough setup. Just &lt;code&gt;--nv&lt;/code&gt; and your NVIDIA drivers are available. For AMD GPUs, it's &lt;code&gt;--rocm&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Combined with a job scheduler, a typical training job looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;singularity &lt;span class="nb"&gt;exec&lt;/span&gt; &lt;span class="nt"&gt;--nv&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--bind&lt;/span&gt; /shared/datasets:/data &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--bind&lt;/span&gt; /scratch/&lt;span class="nv"&gt;$USER&lt;/span&gt;:/scratch &lt;span class="se"&gt;\&lt;/span&gt;
    gpu-image.sif &lt;span class="se"&gt;\&lt;/span&gt;
    python train.py &lt;span class="se"&gt;\&lt;/span&gt;
        &lt;span class="nt"&gt;--data&lt;/span&gt; /data/train &lt;span class="se"&gt;\&lt;/span&gt;
        &lt;span class="nt"&gt;--output&lt;/span&gt; /scratch/checkpoints &lt;span class="se"&gt;\&lt;/span&gt;
        &lt;span class="nt"&gt;--epochs&lt;/span&gt; 100 &lt;span class="se"&gt;\&lt;/span&gt;
        &lt;span class="nt"&gt;--batch-size&lt;/span&gt; 512 &lt;span class="se"&gt;\&lt;/span&gt;
        &lt;span class="nt"&gt;--gpus&lt;/span&gt; 4
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Submit the job and check the results later.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;GPU&lt;/th&gt;
&lt;th&gt;Docker 🐳&lt;/th&gt;
&lt;th&gt;Singularity 🔬&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;NVIDIA&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;--gpus all&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;--nv&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Specific GPU&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;--gpus '"device=0"'&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;CUDA_VISIBLE_DEVICES=0&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;AMD&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;--device /dev/kfd --device /dev/dri&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;--rocm&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  📤 Registry &amp;amp; Sharing
&lt;/h2&gt;

&lt;p&gt;In Docker, sharing an image means pushing to a registry. Both sides need accounts and permissions.&lt;/p&gt;

&lt;p&gt;In Singularity, you &lt;strong&gt;can&lt;/strong&gt; push to the Singularity Library:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;singularity remote login
singularity push myimage.sif library://myname/default/myimage:v1.0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But what I actually do: just copy the file.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cp &lt;/span&gt;myimage.sif /shared/containers/team-env.sif
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Anyone on the cluster can run &lt;code&gt;singularity exec /shared/containers/team-env.sif python train.py&lt;/code&gt; and get the &lt;strong&gt;exact same environment&lt;/strong&gt;. Same packages, same versions, same GPU libraries. No registry, no login. Just a file on a shared filesystem.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Task&lt;/th&gt;
&lt;th&gt;Docker 🐳&lt;/th&gt;
&lt;th&gt;Singularity 🔬&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Login&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;docker login&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;singularity remote login&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Push&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;docker push user/image:tag&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;singularity push image.sif library://user/image:tag&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Share&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Push to registry&lt;/td&gt;
&lt;td&gt;Copy the &lt;code&gt;.sif&lt;/code&gt; file&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  🎼 Multi-Container Orchestration
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Singularity has no built-in &lt;code&gt;docker-compose&lt;/code&gt; alternative&lt;/strong&gt; ⚠️. If you come from a world of &lt;code&gt;docker-compose up&lt;/code&gt; with web servers, databases, and caches all wired together, this will feel like a step back.&lt;/p&gt;

&lt;p&gt;But &lt;strong&gt;on HPC, you usually don't need it&lt;/strong&gt;. Most workloads are single-container jobs. Your training script runs inside one environment, reads data from shared storage, and writes checkpoints to scratch space.&lt;/p&gt;

&lt;p&gt;When you &lt;strong&gt;do&lt;/strong&gt; need multiple services, you have a few options:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;📜 Option 1: A simple shell script.&lt;/strong&gt; Start each service as a Singularity instance, connect them through localhost (since they share the host network):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;#!/bin/bash&lt;/span&gt;
singularity instance start postgres.sif db
&lt;span class="nb"&gt;sleep &lt;/span&gt;5
singularity instance start &lt;span class="nt"&gt;--env&lt;/span&gt; &lt;span class="nv"&gt;DATABASE_URL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;postgresql://localhost/mydb tracker.sif tracker
singularity instance list
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;🔧 Option 2: &lt;a href="https://github.com/singularityhub/singularity-compose" rel="noopener noreferrer"&gt;singularity-compose&lt;/a&gt;&lt;/strong&gt; — a community tool that reads YAML files similar to &lt;code&gt;docker-compose.yml&lt;/code&gt;. It works for simple setups, but it is not actively maintained.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Feature&lt;/th&gt;
&lt;th&gt;Docker Compose&lt;/th&gt;
&lt;th&gt;Singularity Compose&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Network isolation&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;✅ Full&lt;/td&gt;
&lt;td&gt;❌ Host only&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Service discovery&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;✅ DNS&lt;/td&gt;
&lt;td&gt;⚠️ Limited&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Health checks&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;✅ Built-in&lt;/td&gt;
&lt;td&gt;❌ Manual&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;depends_on&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;✅ Full&lt;/td&gt;
&lt;td&gt;⚠️ Limited&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Secrets&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;✅ Built-in&lt;/td&gt;
&lt;td&gt;❌ Env vars&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Production ready&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;✅ Yes&lt;/td&gt;
&lt;td&gt;⚠️ Not actively maintained&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  🔍 Troubleshooting
&lt;/h2&gt;

&lt;p&gt;Here are the problems I hit and how I solved them:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;🐍 Host Python leaking in&lt;/strong&gt; — my &lt;code&gt;$PYTHONPATH&lt;/code&gt; and other environment variables were being inherited by the container, causing import errors. Fix: always use &lt;code&gt;--cleanenv&lt;/code&gt; for training runs.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;✏️ "Read-only file system" errors&lt;/strong&gt; — Singularity images are read-only by default. If your script tries to write to &lt;code&gt;/opt&lt;/code&gt; or &lt;code&gt;/usr&lt;/code&gt;, it will fail. Fix: write to &lt;code&gt;$HOME&lt;/code&gt;, use &lt;code&gt;--bind&lt;/code&gt;, or use sandbox mode.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;🔌 Port conflicts&lt;/strong&gt; — two users starting a service on the same port will conflict since Singularity shares the host network. Fix: always pick a random port, or let your job scheduler assign one.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;💾 Disk space&lt;/strong&gt; — &lt;code&gt;.sif&lt;/code&gt; files can be large (multi-GB for GPU images) and there is no layer sharing. Fix: keep shared images in &lt;code&gt;/shared/containers/&lt;/code&gt; instead of each person having their own copy.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Problem&lt;/th&gt;
&lt;th&gt;Docker fix 🐳&lt;/th&gt;
&lt;th&gt;Singularity fix 🔬&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Permission denied&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;--user $(id -u):$(id -g)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Shouldn't happen (same user)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Can't write to a path&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Mount a volume&lt;/td&gt;
&lt;td&gt;Use &lt;code&gt;$HOME&lt;/code&gt;, &lt;code&gt;--bind&lt;/code&gt;, or sandbox&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Port in use&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Change port mapping&lt;/td&gt;
&lt;td&gt;Pick a different port&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Out of space&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;docker system prune&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;rm *.sif&lt;/code&gt; or use shared images&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Wrong Python version&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Check base image&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;--cleanenv&lt;/code&gt; to stop host env leaking in&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Package not found&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Install in Dockerfile&lt;/td&gt;
&lt;td&gt;Install in &lt;code&gt;%post&lt;/code&gt; or use sandbox&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  💡 What I Learned
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Where Singularity wins
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;No daemon — just run it&lt;/li&gt;
&lt;li&gt;Single-file images — &lt;code&gt;cp&lt;/code&gt; for instant reproducibility&lt;/li&gt;
&lt;li&gt;Same user inside and outside — no permission headaches&lt;/li&gt;
&lt;li&gt;Simple GPU access — just &lt;code&gt;--nv&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Works on shared HPC clusters without special privileges&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Watch out for
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Building images needs root (&lt;code&gt;--fakeroot&lt;/code&gt; or &lt;code&gt;--remote&lt;/code&gt; as workaround)&lt;/li&gt;
&lt;li&gt;No network isolation&lt;/li&gt;
&lt;li&gt;No built-in compose/orchestration&lt;/li&gt;
&lt;li&gt;No layer caching — full rebuild every time&lt;/li&gt;
&lt;li&gt;Host environment can leak in — &lt;strong&gt;always use &lt;code&gt;--cleanenv&lt;/code&gt;&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  🎬 The End
&lt;/h2&gt;

&lt;p&gt;That &lt;code&gt;command not found&lt;/code&gt; on my first day scared me. I thought none of my Docker experience would transfer.&lt;/p&gt;

&lt;p&gt;But it did 😊. Every Docker image still works — just add &lt;code&gt;docker://&lt;/code&gt; in front. Every concept — images, containers, mounts, environment variables — still applies. Same mental model, different tool.&lt;/p&gt;

&lt;p&gt;Singularity taught me something unexpected: &lt;strong&gt;isolation is not always the goal&lt;/strong&gt;. Docker keeps containers separate from the host. But on a shared cluster, you actually &lt;strong&gt;want&lt;/strong&gt; the container to feel like part of the system. You want your files there. You want the GPU drivers to just work. You want to submit a job and not worry about daemon sockets and port mappings.&lt;/p&gt;

&lt;p&gt;My workflow now: Docker on my MacBook for local testing. Singularity on the cluster for real training. Same images, same Dockerfiles, different last mile.&lt;/p&gt;

&lt;p&gt;They are not competitors. They answer different questions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;🐳 &lt;strong&gt;Docker:&lt;/strong&gt; "How do I isolate this app?"&lt;/li&gt;
&lt;li&gt;🔬 &lt;strong&gt;Singularity:&lt;/strong&gt; "How do I bring this app into the researcher's environment?"&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Sometimes the second question is the right one.&lt;/p&gt;




&lt;p&gt;If you found this useful, follow me here on &lt;a href="https://dev.to/dalirnet"&gt;dev.to&lt;/a&gt; and check out my &lt;a href="https://github.com/dalirnet" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>docker</category>
      <category>singularity</category>
      <category>datascience</category>
      <category>containers</category>
    </item>
    <item>
      <title>You Just Want Containers — Why Is Installing Docker &amp; Podman Still This Hard?</title>
      <dc:creator>Amir Reza Dalir</dc:creator>
      <pubDate>Mon, 23 Feb 2026 11:14:47 +0000</pubDate>
      <link>https://forem.com/diphyx/you-just-want-containers-why-is-installing-docker-podman-still-this-hard-25a6</link>
      <guid>https://forem.com/diphyx/you-just-want-containers-why-is-installing-docker-podman-still-this-hard-25a6</guid>
      <description>&lt;p&gt;It's 11 PM. Your deadline is tomorrow. The model is ready, the compose file is written, and all you need is &lt;code&gt;docker compose up&lt;/code&gt;. But you're staring at a fresh Ubuntu VM and Docker isn't installed.&lt;/p&gt;

&lt;p&gt;No problem, right? Just install it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;apt-get &lt;span class="nb"&gt;install &lt;/span&gt;docker.io
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Except the version in the repo is two years old. The docs say to add Docker's official repository. So you start adding GPG keys, configuring apt sources, updating package lists... fifteen minutes later, Docker is running. You pull your image, start the stack, and finally get to work.&lt;/p&gt;

&lt;p&gt;That was the easy version. 😅&lt;/p&gt;

&lt;p&gt;What if you could just type &lt;strong&gt;dock&lt;/strong&gt;er + &lt;strong&gt;pod&lt;/strong&gt;man and have it work — anywhere?&lt;/p&gt;




&lt;h2&gt;
  
  
  When the easy version doesn't exist
&lt;/h2&gt;

&lt;p&gt;🔬 &lt;strong&gt;The researcher.&lt;/strong&gt; You're on a university HPC cluster with a containerized pipeline — a deep learning training job, a genomics workflow, a physics simulation. You need Docker or Podman. But you don't have &lt;code&gt;sudo&lt;/code&gt;. You can't install packages. The sysadmin takes days to respond to tickets. Your deadline doesn't care.&lt;/p&gt;

&lt;p&gt;🧠 &lt;strong&gt;The ML engineer.&lt;/strong&gt; You're spinning up GPU instances on a cloud provider. The VM comes with a minimal OS — no &lt;code&gt;apt&lt;/code&gt;, no &lt;code&gt;yum&lt;/code&gt;, no package manager at all. Just a kernel and a shell. You need Docker, but the official install script assumes you have a full distro underneath.&lt;/p&gt;

&lt;p&gt;🔒 &lt;strong&gt;The restricted environment.&lt;/strong&gt; You're working in a secure facility. The machines are air-gapped — no internet. You can transfer files in via USB, but every tool you need has to be bundled and self-contained.&lt;/p&gt;

&lt;p&gt;🔀 &lt;strong&gt;The mixed-runtime team.&lt;/strong&gt; Your team standardized on Podman for security reasons, but half your tooling still expects Docker. You need both, and switching between them shouldn't require reconfiguring everything.&lt;/p&gt;

&lt;p&gt;These aren't edge cases. For researchers, data scientists, and engineers working outside the typical cloud-native setup — this is everyday reality.&lt;/p&gt;




&lt;h2&gt;
  
  
  The official path is paved, but narrow
&lt;/h2&gt;

&lt;p&gt;Docker does provide a convenience script:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-fsSL&lt;/span&gt; https://get.docker.com | sh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It's well-maintained and it works. But it needs &lt;code&gt;sudo&lt;/code&gt;. It needs a package manager. It needs internet access. It only supports &lt;a href="https://docs.docker.com/engine/install/" rel="noopener noreferrer"&gt;specific distributions&lt;/a&gt;. And it only installs Docker — no Podman, no rootless mode out of the box.&lt;/p&gt;

&lt;p&gt;For Podman, the story is worse. There's no &lt;code&gt;get.podman.com&lt;/code&gt;. Your options are distro packages (often outdated, sometimes missing entirely) or building from source. That means cloning seven separate repositories — Podman itself, plus crun, conmon, netavark, aardvark-dns, slirp4netns, and fuse-overlayfs — and compiling Go, Rust, and C code. On a machine where you can't even &lt;code&gt;sudo apt install gcc&lt;/code&gt;, that's a dead end. 🛑&lt;/p&gt;

&lt;p&gt;And Compose? That's another installation step on top of everything else.&lt;/p&gt;

&lt;p&gt;The tools exist. The documentation exists. But the path from "I need containers" to "I have containers" is full of assumptions about your environment that don't always hold.&lt;/p&gt;




&lt;h2&gt;
  
  
  Removing the assumptions
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/diphyx/dockpod" rel="noopener noreferrer"&gt;dockpod&lt;/a&gt; (&lt;strong&gt;dock&lt;/strong&gt;er + &lt;strong&gt;pod&lt;/strong&gt;man, quick setup) was built around a simple idea: installing a container runtime should work the same way everywhere.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-fsSL&lt;/span&gt; diphyx.github.io/dockpod/setup.sh | bash
dockpod &lt;span class="nb"&gt;install&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;p&gt;That's the entire setup. An interactive menu asks which runtime you want — Docker, Podman, or both. Compose is included automatically with either choice. No &lt;code&gt;sudo&lt;/code&gt; required. No package manager. No GPG keys. No repository configuration.&lt;/p&gt;

&lt;p&gt;It works on any Linux system with kernel 4.18+ and systemd — which covers essentially every modern distro and cloud image. ✅&lt;/p&gt;




&lt;h2&gt;
  
  
  Under the hood
&lt;/h2&gt;

&lt;p&gt;When you run &lt;code&gt;dockpod install&lt;/code&gt;, it checks your system, detects your architecture, and pulls prebuilt static binaries — the same ones compiled from the official upstream sources. It places them in the right directories, writes configuration files, sets up systemd services, and verifies the installation by actually running a container.&lt;/p&gt;

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

&lt;p&gt;🛡️ If you have root, binaries go to &lt;code&gt;/usr/local/bin/&lt;/code&gt; and services run system-wide. If you don't, everything goes into &lt;code&gt;~/.local/bin/&lt;/code&gt; and runs as user-scoped systemd services. Docker uses its rootless mode, Podman works rootless natively. Either way, you end up with a fully working runtime without ever typing &lt;code&gt;sudo&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;🔌 Compose is included out of the box — installed as both a standalone &lt;code&gt;docker-compose&lt;/code&gt; binary and a Docker CLI plugin (&lt;code&gt;docker compose&lt;/code&gt;). When Podman is installed, dockpod creates a &lt;code&gt;podman-compose&lt;/code&gt; symlink automatically, so your existing compose files work with either runtime:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker compose up &lt;span class="nt"&gt;-d&lt;/span&gt;       &lt;span class="c"&gt;# with Docker&lt;/span&gt;
podman-compose up &lt;span class="nt"&gt;-d&lt;/span&gt;       &lt;span class="c"&gt;# with Podman — same file, no changes&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;🔀 Need both Docker and Podman? Install both, then switch between them instantly. Your compose files, your scripts, your muscle memory — everything stays the same. Only the engine underneath changes.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;dockpod switch podman
dockpod switch docker
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  No internet? No problem
&lt;/h2&gt;

&lt;p&gt;This is the part that matters most for restricted environments.&lt;/p&gt;

&lt;p&gt;dockpod publishes architecture-specific tarballs with every release. Each one is self-contained — Docker, Podman, Compose, and all their dependencies bundled together. Nothing is downloaded during installation.&lt;/p&gt;

&lt;p&gt;Grab the tarball on a connected machine, transfer it however you can — USB, scp, a shared drive — and install on the target without a single network call:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;tar&lt;/span&gt; &lt;span class="nt"&gt;-xzf&lt;/span&gt; dockpod-v2.0.0-amd64.tar.gz &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;cd &lt;/span&gt;dockpod-v2.0.0-amd64
./dockpod.sh &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;--offline&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For labs, secure facilities, and isolated clusters, this is often the only practical way to get containers running without a lengthy approval process.&lt;/p&gt;




&lt;h2&gt;
  
  
  Give it a try
&lt;/h2&gt;

&lt;p&gt;If you've ever spent more time installing a container runtime than actually using it — that's exactly what dockpod is for.&lt;/p&gt;

&lt;p&gt;If it saves you time, share it with your team, your lab, or that colleague who's still struggling to get containers running.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/diphyx/dockpod" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt; · &lt;a href="https://github.com/diphyx/dockpod/blob/main/LICENSE" rel="noopener noreferrer"&gt;MIT licensed&lt;/a&gt; · Built by &lt;a href="https://github.com/diphyx" rel="noopener noreferrer"&gt;DiPhyx&lt;/a&gt;&lt;/p&gt;

</description>
      <category>docker</category>
      <category>podman</category>
      <category>containers</category>
      <category>linux</category>
    </item>
    <item>
      <title>Your Cheap Home Router Has a Hidden CLI — Here's How I Found It</title>
      <dc:creator>Amir Reza Dalir</dc:creator>
      <pubDate>Fri, 13 Feb 2026 08:43:24 +0000</pubDate>
      <link>https://forem.com/dalirnet/your-cheap-home-router-has-a-hidden-cli-heres-how-i-found-it-22oc</link>
      <guid>https://forem.com/dalirnet/your-cheap-home-router-has-a-hidden-cli-heres-how-i-found-it-22oc</guid>
      <description>&lt;h2&gt;
  
  
  🏚️ Chapter 1: The Budget
&lt;/h2&gt;

&lt;p&gt;Let me set the scene.&lt;/p&gt;

&lt;p&gt;Most of us don't get to choose our router. Your ISP comes to your house, puts a white plastic box on the wall, gives you a paper with the WiFi password, and leaves. That's it. That's your whole network now.&lt;/p&gt;

&lt;p&gt;My box was a &lt;strong&gt;Huawei HG8240&lt;/strong&gt;. A GPON terminal. This kind of device exists in millions of homes across Asia, the Middle East, South America — anywhere ISPs buy hardware in bulk and give it away for almost nothing. It's cheap. And it &lt;em&gt;looks&lt;/em&gt; cheap too.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Web UI: A Beautiful Prison
&lt;/h3&gt;

&lt;p&gt;Every router comes with a web UI. You open a browser, type an IP address, and you get an admin panel. Fine.&lt;/p&gt;

&lt;p&gt;But here's what nobody tells you: &lt;strong&gt;the web UI doesn't show you everything your router can do&lt;/strong&gt;. You get WiFi settings, a MAC filter table, maybe port forwarding if you're lucky. The rest — advanced routing, VLAN management, QoS controls, real firewall rules, diagnostics — is either hidden behind an "advanced" tab that barely works, or just not there at all.&lt;/p&gt;

&lt;p&gt;And some of these web UIs... okay, I don't want to insult anyone's work, but — &lt;strong&gt;frames&lt;/strong&gt; inside &lt;strong&gt;frames&lt;/strong&gt;. Pages that fully reload on every click. Buttons that do nothing when you press them. Session timeouts that kick you out while you're still typing a MAC address.&lt;/p&gt;

&lt;p&gt;You learn to be fast and hope nothing crashes. It's not network administration — it's speedrunning.&lt;/p&gt;

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

&lt;h3&gt;
  
  
  The Real Problem: Humans Required
&lt;/h3&gt;

&lt;p&gt;But the bad design is not even the worst part. The &lt;strong&gt;real&lt;/strong&gt; problem is more basic than that:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A web UI needs a human.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Every action needs your hands and your eyes. You can't write a script for it. You can't automate it. You can't connect it to anything else.&lt;/p&gt;

&lt;p&gt;Want to add 10 devices to the whitelist? That's 10 rounds of click, type, save, wait, repeat. Want a script that blocks all devices at midnight and turns them back on in the morning? Not possible. Want to connect your router to your smart home or a monitoring tool? The web UI says: &lt;em&gt;"No. Sit down. Click the button. Wait for the page to reload. That's your life now."&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;For a technical user, this is a dead end. A wall. A cage made of &lt;code&gt;&amp;lt;iframe&amp;gt;&lt;/code&gt; tags.&lt;/p&gt;

&lt;h3&gt;
  
  
  The "Easy" Solution
&lt;/h3&gt;

&lt;p&gt;Now, there's always the obvious answer: go to a store, drop your money, and buy a MikroTik RouterBoard. Problem solved. CLI out of the box. Full control. Done. 💸&lt;/p&gt;

&lt;p&gt;And if money is no problem, why stop there? Get a Cisco managed switch. Get a rack. Get a server room. Put on a suit. Become a network engineer. Problem &lt;em&gt;really&lt;/em&gt; solved.&lt;/p&gt;

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

&lt;p&gt;But back in reality — where the ISP already gave us a router for free and we'd rather spend that money on coffee ☕ — let's work with what we have.&lt;/p&gt;

&lt;h3&gt;
  
  
  What If There Was Another Way?
&lt;/h3&gt;

&lt;p&gt;A CLI changes everything. Every command the router supports — available directly. You can write scripts. You can automate tasks. You can connect it to other tools. You could set up your whole network in seconds instead of clicking through fifteen slow pages.&lt;/p&gt;

&lt;p&gt;That's not a nice-to-have — that's the difference between &lt;strong&gt;controlling&lt;/strong&gt; your router and &lt;strong&gt;being stuck with it&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;And I was definitely stuck. Every time a new device needed WiFi access — open browser, type &lt;code&gt;192.168.1.1&lt;/code&gt;, log in, go through three menus, type a MAC address, click save, hope it works. Every. Single. Time.&lt;/p&gt;

&lt;p&gt;Meanwhile, MikroTik and Cisco users had one-line CLI commands. Clean. Scriptable. Well documented.&lt;/p&gt;

&lt;p&gt;And me? Clicking through a web panel like it was 2008. Because I had the budget router. The one nobody makes CLI tools for. The one nobody writes docs for. The one that's supposed to sit in the corner and never be questioned.&lt;/p&gt;

&lt;p&gt;But I'm a developer. I question things.&lt;/p&gt;




&lt;h2&gt;
  
  
  🔍 Chapter 2: The Suspicion
&lt;/h2&gt;

&lt;p&gt;It started with a port scan.&lt;/p&gt;

&lt;p&gt;I don't remember exactly why I ran it — maybe I was bored, maybe curious, maybe it was one of those late nights where developers do things they probably shouldn't with &lt;code&gt;nmap&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;nmap 192.168.1.1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And there it was in the output. Something unexpected.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Port 22. Closed.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Not &lt;em&gt;filtered&lt;/em&gt; (that means a firewall is hiding it). Not &lt;em&gt;absent&lt;/em&gt; (that means the service doesn't exist). &lt;strong&gt;Closed&lt;/strong&gt; — that means something is &lt;em&gt;right there&lt;/em&gt;, but it's not accepting connections.&lt;/p&gt;

&lt;p&gt;Like a door that's locked, but you can see there's a room behind it.&lt;/p&gt;

&lt;p&gt;SSH. On my cheap home router.&lt;/p&gt;

&lt;p&gt;My heart beat a little faster. I tried to connect:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;ssh root@192.168.1.1
ssh: connect to host 192.168.1.1 port 22: Connection refused
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Connection refused. Of course. 😑&lt;/p&gt;

&lt;p&gt;But now I &lt;em&gt;knew&lt;/em&gt; there was a door. I just needed to find the key.&lt;/p&gt;




&lt;h2&gt;
  
  
  📚 Chapter 3: The Documentation Nightmare
&lt;/h2&gt;

&lt;p&gt;This is where the story gets painful.&lt;/p&gt;

&lt;p&gt;I started looking for official Huawei documentation for the HG8240 series. What I found was... almost nothing useful.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Language&lt;/th&gt;
&lt;th&gt;Usefulness&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Chinese&lt;/td&gt;
&lt;td&gt;Detailed — if you can read it&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Portuguese&lt;/td&gt;
&lt;td&gt;Brazil-specific firmware only&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;English&lt;/td&gt;
&lt;td&gt;Machine-translated, barely usable&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Russian&lt;/td&gt;
&lt;td&gt;Random blog posts, hit or miss&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Indonesian&lt;/td&gt;
&lt;td&gt;"It works!" (no explanation)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;There was no "Developer Guide." No "CLI Reference." No simple README with examples. Just small pieces here and there — a forum post from someone in Indonesia who said they enabled SSH but didn't explain how, a Russian blog with screenshots of XML files that looked useful but led nowhere, and PDFs. So many PDFs.&lt;/p&gt;

&lt;p&gt;Seriously — PDFs. In 2026. Not a web page, not a wiki, not even a text file. PDFs. And not small ones — each one over 10MB, full of screenshots of admin panels I've never seen, for firmware versions I don't have, about features I wasn't looking for. I downloaded about a dozen of them. Opened each one hoping to find a CLI reference. Got 200 pages about how to set up PPPoE through the web panel instead. Thanks, Huawei.&lt;/p&gt;

&lt;p&gt;I spent &lt;strong&gt;hours&lt;/strong&gt;. Real hours. Not the "I searched for five minutes" kind. The kind where you have too many browser tabs open, half of them in Google Translate, comparing a Chinese PDF with a Portuguese forum thread because they both describe the same XML setting but with different names.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Hard Truth
&lt;/h3&gt;

&lt;p&gt;Here's what I want to say clearly: &lt;strong&gt;this is normal for consumer routers.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Huawei, ZTE, TP-Link — they all do this. They don't write documentation for people like us. Their real customer is the ISP, not the person at home. The ISP gets proper guides and setup tools. We get a web panel and nothing else.&lt;/p&gt;

&lt;p&gt;But just because they don't tell you about something doesn't mean it's not there. It just means nobody thought you'd go looking.&lt;/p&gt;

&lt;p&gt;The engineers who build these devices still leave traces behind. Config files have patterns. Firmware has &lt;code&gt;help&lt;/code&gt; commands. Ports show up in scans. The information is out there — hard to find, badly written, in the wrong language — but it exists.&lt;/p&gt;

&lt;p&gt;You just have to want it badly enough.&lt;/p&gt;




&lt;h2&gt;
  
  
  🔓 Chapter 4: The Configuration File
&lt;/h2&gt;

&lt;p&gt;The answer came from the last place I expected: &lt;strong&gt;the web panel itself&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Yes, the irony was not lost on me.&lt;/p&gt;

&lt;p&gt;I logged into the admin panel — not with the regular user, but with &lt;code&gt;telecomadmin&lt;/code&gt; / &lt;code&gt;admintelecom&lt;/code&gt;, the ISP-level account that most people never use. I went to:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;Advanced → Maintenance Diagnostic → Configuration File Management&lt;/code&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;And there it was. A small button to &lt;strong&gt;download the router's full configuration as a file&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;I had ignored it many times before. But I was desperate enough to try anything. I downloaded it. Made a backup copy first (always do this). Opened it in my IDE and... XML. Thousands of lines of XML.&lt;/p&gt;

&lt;p&gt;Now — I got really lucky here. Some Huawei routers &lt;strong&gt;encrypt&lt;/strong&gt; the config file when you export it. You download it, open it, and instead of readable XML you see random characters. Useless.&lt;/p&gt;

&lt;p&gt;If that had happened to me, honestly? I probably would have closed my laptop and moved on with my life. Mystery over. The web panel wins.&lt;/p&gt;

&lt;p&gt;But my config came out as plain, readable XML. That small bit of luck is the only reason this article exists.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A note for anyone following along&lt;/strong&gt; — if your config file &lt;em&gt;is&lt;/em&gt; encrypted, don't give up. People have already figured this out. There are Gists on GitHub with scripts that can decode and re-encode config files for different Huawei models. The process is: download the encrypted config → decode it with the script → make your changes → encode it back → upload it. Search for &lt;code&gt;"huawei config decrypt HG8245"&lt;/code&gt; or &lt;code&gt;"huawei gpon config encode decode"&lt;/code&gt; on GitHub Gists — people have been sharing these tools for years.&lt;/p&gt;

&lt;p&gt;Anyway, back to my config. I started scrolling. And somewhere around line 400, in that wall of angle brackets, I found this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;X_HW_CLISSHControl&lt;/span&gt; &lt;span class="na"&gt;Enable=&lt;/span&gt;&lt;span class="s"&gt;"0"&lt;/span&gt; &lt;span class="na"&gt;port=&lt;/span&gt;&lt;span class="s"&gt;"22"&lt;/span&gt; &lt;span class="na"&gt;Mode=&lt;/span&gt;&lt;span class="s"&gt;"0"&lt;/span&gt; &lt;span class="na"&gt;AluSSHAbility=&lt;/span&gt;&lt;span class="s"&gt;"0"&lt;/span&gt;&lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I stared at it. Read it again. Read it a third time.&lt;/p&gt;

&lt;p&gt;But let me tell you about the journey to &lt;em&gt;find&lt;/em&gt; that line. Scrolling through this XML felt like Huawei was trying to break me personally. Thousands of lines of nested tags, no comments, no clear sections, no formatting. It looked like it was created by a machine that had never heard of indentation. I'm pretty sure this config format is older than most programming languages I use.&lt;/p&gt;

&lt;p&gt;Anyway — back to the discovery:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;SSH was not missing. It was not unsupported. It was just... turned off.&lt;/strong&gt; In some firmware versions the line is there with &lt;code&gt;Enable="0"&lt;/code&gt;. In others it doesn't exist at all — you have to add it yourself. Either way, the feature is built into the firmware. It just needs to be turned on.&lt;/p&gt;

&lt;p&gt;I kept reading. Found another flag:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;AclServices&lt;/span&gt; &lt;span class="err"&gt;...&lt;/span&gt; &lt;span class="na"&gt;SSHLanEnable=&lt;/span&gt;&lt;span class="s"&gt;"0"&lt;/span&gt; &lt;span class="err"&gt;...&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two zeros. Two flags. That's all that stood between a "dumb" web-only router and one with a full command-line interface.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Surgery
&lt;/h3&gt;

&lt;p&gt;The fix was almost too simple:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 1&lt;/strong&gt; — Add or change the &lt;code&gt;X_HW_CLISSHControl&lt;/code&gt; line to &lt;code&gt;Enable="1"&lt;/code&gt;. Make sure to place it before &lt;code&gt;X_HW_CLITelnetAccess&lt;/code&gt; in the XML:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;X_HW_CLISSHControl&lt;/span&gt; &lt;span class="na"&gt;Enable=&lt;/span&gt;&lt;span class="s"&gt;"1"&lt;/span&gt; &lt;span class="na"&gt;port=&lt;/span&gt;&lt;span class="s"&gt;"22"&lt;/span&gt; &lt;span class="na"&gt;Mode=&lt;/span&gt;&lt;span class="s"&gt;"0"&lt;/span&gt; &lt;span class="na"&gt;AluSSHAbility=&lt;/span&gt;&lt;span class="s"&gt;"0"&lt;/span&gt;&lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step 2&lt;/strong&gt; — Find &lt;code&gt;SSHLanEnable&lt;/code&gt; and change the zero to one:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;AclServices&lt;/span&gt; &lt;span class="err"&gt;...&lt;/span&gt; &lt;span class="na"&gt;SSHLanEnable=&lt;/span&gt;&lt;span class="s"&gt;"1"&lt;/span&gt; &lt;span class="err"&gt;...&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step 3&lt;/strong&gt; — Upload the changed config through the same admin page. The router rebooted on its own.&lt;/p&gt;

&lt;p&gt;Then I waited. Watched the lights blink. Watched them go steady. Opened my terminal. And typed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;ssh &lt;span class="nt"&gt;-o&lt;/span&gt; &lt;span class="nv"&gt;HostKeyAlgorithms&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;+ssh-rsa &lt;span class="nt"&gt;-o&lt;/span&gt; &lt;span class="nv"&gt;PubkeyAcceptedKeyTypes&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;+ssh-rsa root@192.168.1.1
root@192.168.1.1&lt;span class="s1"&gt;'s password: ********
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;The &lt;code&gt;-o HostKeyAlgorithms=+ssh-rsa&lt;/code&gt; flags are needed because these routers use an older SSH algorithm that new SSH clients block by default. Also, only the &lt;code&gt;root&lt;/code&gt; user can connect — the password is printed on the back of the device.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;And then...&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;WAP&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;p&gt;A prompt. A real command prompt. On my budget router. 🎉&lt;/p&gt;

&lt;p&gt;I typed &lt;code&gt;help&lt;/code&gt;. A long list of commands appeared — WiFi, routing, VLAN management, diagnostics. This router had a whole operating system hiding behind that web panel the entire time.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;WAP&amp;gt; display wifi filter                                &lt;span class="c"&gt;# List filtered devices&lt;/span&gt;
WAP&amp;gt; add wifi filter index 1 mac AA:BB:CC:DD:EE:FF     &lt;span class="c"&gt;# Whitelist a device on SSID-1&lt;/span&gt;
WAP&amp;gt; del wifi filter index 1 mac AA:BB:CC:DD:EE:FF     &lt;span class="c"&gt;# Remove from whitelist&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The locked door was open. And the room behind it was huge.&lt;/p&gt;




&lt;h2&gt;
  
  
  🚀 Chapter 5: Now What?
&lt;/h2&gt;

&lt;p&gt;The CLI was open. And suddenly, everything that was impossible through the web UI became easy.&lt;/p&gt;

&lt;p&gt;I could write a bash script to add ten devices in two seconds. I could set up a &lt;code&gt;cron&lt;/code&gt; job to block everything at midnight. I could send the output of &lt;code&gt;display wifi filter&lt;/code&gt; to &lt;code&gt;grep&lt;/code&gt; and build a monitoring tool. The CLI turned my "dumb" router into something I could actually &lt;strong&gt;write code against&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;And that's what I did — I wrapped these SSH commands into &lt;a href="https://github.com/dalirnet/wmac" rel="noopener noreferrer"&gt;a small macOS app called WMac&lt;/a&gt; using Expect scripts to handle the old SSH connection. But that's just one example. The point isn't the app — the point is that once you have CLI access, you can build &lt;em&gt;anything&lt;/em&gt; on top of it.&lt;/p&gt;

&lt;p&gt;The locked door led to a whole building.&lt;/p&gt;




&lt;h2&gt;
  
  
  🌍 Chapter 6: The Bigger Mystery
&lt;/h2&gt;

&lt;p&gt;Here's where I stop talking about my story and start talking about yours.&lt;/p&gt;

&lt;p&gt;I did all of this on one router — the Huawei HG8240 — because that's what I have at home. I don't have a ZTE. I don't have a TP-Link. I can't test them, so I won't pretend to know what's inside them.&lt;/p&gt;

&lt;p&gt;But here's what I believe: &lt;strong&gt;most cheap home routers are hiding something like this.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;These devices are not built from zero. They share the same chips, the same firmware base, the same design. The same factories in Shenzhen that make Huawei's GPON terminals also make ZTE's, and many other brands you've never heard of. If Huawei's firmware has SSH built in but turned off, there's a good chance the router on your shelf does too.&lt;/p&gt;

&lt;p&gt;The problem is always the same — nobody tells you. The docs are bad, in the wrong language, written for someone else, about a different firmware version, on forums in countries you've never been to, in threads from 2019.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;But the information is out there.&lt;/strong&gt; That's the biggest thing I learned from this whole experience. Bad documentation doesn't mean no documentation. The answers are just harder to find. But they can be found.&lt;/p&gt;




&lt;h2&gt;
  
  
  🎬 Your Turn
&lt;/h2&gt;

&lt;p&gt;That's my story. One cheap router. One hidden CLI. A lot of bad docs and late nights.&lt;/p&gt;

&lt;p&gt;But the real point of this article is not about my router — it's about yours. Here's a simple guide to start:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Step&lt;/th&gt;
&lt;th&gt;What to Do&lt;/th&gt;
&lt;th&gt;What to Look For&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;&lt;code&gt;nmap 192.168.1.1&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Closed ports (not filtered — &lt;em&gt;closed&lt;/em&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;Download config from admin panel&lt;/td&gt;
&lt;td&gt;XML/JSON with disabled settings&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;Search for &lt;code&gt;Enable="0"&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;SSH, Telnet, SNMP — anything turned off&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;Read the bad docs&lt;/td&gt;
&lt;td&gt;Google Translate is your friend&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;Get a shell, type &lt;code&gt;help&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;See what commands are available&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The worst thing that can happen is you learn something about your router. The best thing? You find a whole hidden interface that was always there, waiting for someone curious enough to look.&lt;/p&gt;




&lt;p&gt;I solved this mystery for one router. There are thousands of models out there that nobody has looked into yet. If you've explored your cheap home router and found hidden features — SSH on a ZTE, telnet on a TP-Link, a CLI on some ISP box nobody knows about — tell me in the comments.&lt;/p&gt;

&lt;p&gt;The more you share, the fewer people have to suffer through web panels.&lt;/p&gt;

&lt;p&gt;Sometimes the cheapest hardware has the best-hidden secrets. You just have to read the bad docs.&lt;/p&gt;




&lt;p&gt;The tool I built from this is open source: &lt;strong&gt;&lt;a href="https://github.com/dalirnet/wmac" rel="noopener noreferrer"&gt;github.com/dalirnet/wmac&lt;/a&gt;&lt;/strong&gt; — give it a ⭐ if this was useful.&lt;/p&gt;

&lt;p&gt;Follow me on GitHub &lt;strong&gt;&lt;a href="https://github.com/dalirnet" rel="noopener noreferrer"&gt;@dalirnet&lt;/a&gt;&lt;/strong&gt; for more weekend hacks like this.&lt;/p&gt;

</description>
      <category>networking</category>
      <category>linux</category>
      <category>ssh</category>
      <category>homelab</category>
    </item>
    <item>
      <title>Your Resume Deserves Better Than a Black Hole</title>
      <dc:creator>Amir Reza Dalir</dc:creator>
      <pubDate>Thu, 12 Feb 2026 16:15:01 +0000</pubDate>
      <link>https://forem.com/cvinci/your-resume-deserves-better-than-a-black-hole-50f3</link>
      <guid>https://forem.com/cvinci/your-resume-deserves-better-than-a-black-hole-50f3</guid>
      <description>&lt;p&gt;Every day, thousands of resumes disappear into inboxes, portals, and applicant tracking systems — never to be heard from again. No read receipt. No engagement data. No signal at all.&lt;/p&gt;

&lt;p&gt;We believe your resume deserves better than that. And we built &lt;a href="https://cvinci.me" rel="noopener noreferrer"&gt;CVinci&lt;/a&gt; to prove it.&lt;/p&gt;

&lt;p&gt;But first, let us tell you about a developer we know.&lt;/p&gt;




&lt;h2&gt;
  
  
  🕳️ Into the Black Hole
&lt;/h2&gt;

&lt;p&gt;He's a web developer with a computer science degree, real-world projects, and three years of experience at a company in Austin. He was looking for his next remote role — flexible on the company, as long as it was US-based.&lt;/p&gt;

&lt;p&gt;He spent a weekend rewriting his resume, exported a clean PDF, and started applying. By the end of the month, he had sent it to 25 companies.&lt;/p&gt;

&lt;p&gt;Then — silence.&lt;/p&gt;

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

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;What he expected&lt;/th&gt;
&lt;th&gt;What actually happened&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Acknowledgment that someone received his file&lt;/td&gt;
&lt;td&gt;Nothing&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Feedback on whether his CV was a fit&lt;/td&gt;
&lt;td&gt;Nothing&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Knowing if anyone even opened the PDF&lt;/td&gt;
&lt;td&gt;Nothing&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;blockquote&gt;
&lt;p&gt;25 applications. Zero visibility. Every one felt like shouting into a void.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;We've heard this story from hundreds of people. It's the norm — and it shouldn't be.&lt;/p&gt;




&lt;h2&gt;
  
  
  📁 The Mess Behind the Scenes
&lt;/h2&gt;

&lt;p&gt;He was tailoring his resume for different roles. Within two weeks, his laptop looked like this:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;Resume_v3_final.pdf&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Resume_frontend_updated.pdf&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Resume_startup_FINAL_v2.pdf&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Resume_startup_FINAL_v2_revised.pdf&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And his supporting materials were scattered everywhere:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Document&lt;/th&gt;
&lt;th&gt;Location&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;CV versions&lt;/td&gt;
&lt;td&gt;Local folder&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cover letters&lt;/td&gt;
&lt;td&gt;Email drafts&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Portfolio&lt;/td&gt;
&lt;td&gt;Separate website&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Certifications&lt;/td&gt;
&lt;td&gt;Downloads folder&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Every time a recruiter asked for the "full package," he spent twenty minutes hunting for files and hoping he grabbed the right versions. We know this pain — it's one of the first problems we set out to solve.&lt;/p&gt;




&lt;h2&gt;
  
  
  📊 The Data Gap No One Talks About
&lt;/h2&gt;

&lt;p&gt;One evening, his friend in marketing mentioned that their team tracks open rates, click-through rates, and engagement time on every email they send.&lt;/p&gt;

&lt;p&gt;Our developer thought about his 25 job applications.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;He had less insight into his career-defining document than his friend had into a promotional newsletter.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;We thought about this too:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Marketing email&lt;/th&gt;
&lt;th&gt;Traditional resume&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Was it opened?&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;No idea&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;How long did they read?&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;No idea&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Did they click anything?&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;No idea&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Where is the reader located?&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;No idea&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Which version performs best?&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;No idea&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Your resume is the most important document in your job search. Why does it have less analytics than a marketing email?&lt;/p&gt;

&lt;p&gt;That question is exactly what pushed us to build CVinci.&lt;/p&gt;




&lt;h2&gt;
  
  
  💡 Out of the Black Hole
&lt;/h2&gt;

&lt;p&gt;He found &lt;a href="https://cvinci.me" rel="noopener noreferrer"&gt;CVinci&lt;/a&gt; and decided to give it a try. The process took about two minutes:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Uploaded&lt;/strong&gt; his existing PDF&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Our AI extracted&lt;/strong&gt; his work history, education, skills, languages, and certifications — automatically&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Picked a template&lt;/strong&gt; and customized colors and fonts&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Attached&lt;/strong&gt; his cover letter and portfolio as downloadable files&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Got his link&lt;/strong&gt; — a clean, personalized URL&lt;/li&gt;
&lt;/ol&gt;

&lt;blockquote&gt;
&lt;p&gt;One link. Everything a recruiter needs. No more black hole.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  👀 Finally — Visibility
&lt;/h2&gt;

&lt;p&gt;Two days later, he opened his CVinci dashboard.&lt;/p&gt;

&lt;p&gt;For the first time, he could actually see what happened after he hit send:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Viewer&lt;/th&gt;
&lt;th&gt;Location&lt;/th&gt;
&lt;th&gt;Time spent&lt;/th&gt;
&lt;th&gt;Engagement&lt;/th&gt;
&lt;th&gt;Action&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Recruiter A&lt;/td&gt;
&lt;td&gt;Chicago, IL&lt;/td&gt;
&lt;td&gt;1 min 42 sec&lt;/td&gt;
&lt;td&gt;Deep Dive 🔥&lt;/td&gt;
&lt;td&gt;Downloaded portfolio&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Hiring Manager B&lt;/td&gt;
&lt;td&gt;Denver, CO&lt;/td&gt;
&lt;td&gt;3 visits in 48 hrs&lt;/td&gt;
&lt;td&gt;Deep Dive 🔥&lt;/td&gt;
&lt;td&gt;Viewed all sections&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Company C&lt;/td&gt;
&lt;td&gt;Seattle, WA&lt;/td&gt;
&lt;td&gt;4 seconds&lt;/td&gt;
&lt;td&gt;Quick Glance 👀&lt;/td&gt;
&lt;td&gt;Left immediately&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  What the data told him:
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;🔥 &lt;strong&gt;Chicago&lt;/strong&gt; — strong signal. Recruiter spent real time and downloaded files. Worth a follow-up.&lt;/li&gt;
&lt;li&gt;🔁 &lt;strong&gt;Denver&lt;/strong&gt; — came back three times. Serious interest he would have never known about with a static PDF.&lt;/li&gt;
&lt;li&gt;👋 &lt;strong&gt;Seattle&lt;/strong&gt; — four seconds and gone. Not the right fit. Move on.&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;His resume was no longer in a black hole. It was a live, trackable page with real data behind it.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  🔄 From Guesswork to Strategy
&lt;/h2&gt;

&lt;p&gt;With real numbers, he started experimenting. He created a second CV version for startup roles — different template, revised summary, separate link.&lt;/p&gt;

&lt;p&gt;After one week, the data was clear:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Version&lt;/th&gt;
&lt;th&gt;Avg. view time&lt;/th&gt;
&lt;th&gt;Scroll to bottom&lt;/th&gt;
&lt;th&gt;Downloads&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;General&lt;/td&gt;
&lt;td&gt;38 sec&lt;/td&gt;
&lt;td&gt;41%&lt;/td&gt;
&lt;td&gt;12%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Startup-focused&lt;/td&gt;
&lt;td&gt;1 min 14 sec&lt;/td&gt;
&lt;td&gt;67%&lt;/td&gt;
&lt;td&gt;29%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;He doubled down on the startup version.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;We designed CVinci so anyone can A/B test their job search — something that was never possible when your resume lived in a black hole.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  📬 The Right Follow-Up at the Right Time
&lt;/h2&gt;

&lt;p&gt;The feature that surprised him most — our live notifications. The moment someone opened his page, he knew. When someone downloaded his portfolio, he saw it instantly.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;One afternoon:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;🔔 A recruiter from his top-choice company opened his CV&lt;/li&gt;
&lt;li&gt;👀 She spent two full minutes reading&lt;/li&gt;
&lt;li&gt;📥 She downloaded both his cover letter and portfolio&lt;/li&gt;
&lt;li&gt;✉️ He sent a follow-up email within the hour&lt;/li&gt;
&lt;li&gt;📞 He got an interview the next day&lt;/li&gt;
&lt;/ol&gt;

&lt;blockquote&gt;
&lt;p&gt;When your resume is in a black hole, you can't time your follow-ups. When it's on CVinci, you can.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  🎯 Black Hole vs. CVinci
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;The black hole&lt;/th&gt;
&lt;th&gt;CVinci&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Send a PDF and hope for the best&lt;/td&gt;
&lt;td&gt;Share a trackable link and see what happens&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Zero visibility after sending&lt;/td&gt;
&lt;td&gt;Real-time view notifications&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;No idea which version works&lt;/td&gt;
&lt;td&gt;A/B testing with engagement data&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Scattered files across folders&lt;/td&gt;
&lt;td&gt;Everything bundled in one link&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Follow-ups based on guesswork&lt;/td&gt;
&lt;td&gt;Follow-ups based on actual engagement&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Eight weeks later&lt;/strong&gt;, he accepted an offer from the Chicago recruiter — the one who downloaded his portfolio on day two.&lt;/p&gt;

&lt;p&gt;We built CVinci for developers like him. And we think you might be one of them.&lt;/p&gt;




&lt;h2&gt;
  
  
  ⚡ What We Built
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Core features:
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;AI Content Extraction&lt;/strong&gt; — upload a PDF, Word doc, or image. Our AI structures your content automatically&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Personalized URL&lt;/strong&gt; — every CV gets a clean link like &lt;code&gt;cvinci.me/your-name&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Multiple Templates&lt;/strong&gt; — switch styles without re-entering data&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;File Attachments&lt;/strong&gt; — bundle up to 3 supporting documents per CV&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Access Control&lt;/strong&gt; — toggle between public and private with one click&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Analytics you get for every view:
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Data point&lt;/th&gt;
&lt;th&gt;Example&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Engagement level&lt;/td&gt;
&lt;td&gt;Quick Glance / Considered / Deep Dive&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Time spent&lt;/td&gt;
&lt;td&gt;1 min 42 sec&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Location&lt;/td&gt;
&lt;td&gt;Chicago, IL — plotted on map&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Platform&lt;/td&gt;
&lt;td&gt;Chrome on macOS&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Referrer&lt;/td&gt;
&lt;td&gt;LinkedIn / Email / Direct&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Downloads&lt;/td&gt;
&lt;td&gt;Portfolio.pdf downloaded&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Timestamp&lt;/td&gt;
&lt;td&gt;Feb 12, 2026 at 3:14 PM&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  What makes us different:
&lt;/h3&gt;

&lt;blockquote&gt;
&lt;p&gt;Most resume tools help you &lt;strong&gt;build&lt;/strong&gt; a CV.&lt;br&gt;
We help you &lt;strong&gt;build, share, track, and optimize&lt;/strong&gt; it.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  🚀 Get Started
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Step&lt;/th&gt;
&lt;th&gt;What happens&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;Sign in with Google, GitHub, or LinkedIn&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;Upload your existing resume&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;Review the AI-extracted content&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;Pick a template and customize&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;Share your personalized link&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;Track views from your dashboard&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Free. No credit card. No setup wizard. Takes about two minutes.&lt;/p&gt;




&lt;p&gt;Your resume deserves better than a black hole.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://cvinci.me/auth/" rel="noopener noreferrer"&gt;Try CVinci →&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>career</category>
      <category>webdev</category>
      <category>productivity</category>
      <category>free</category>
    </item>
    <item>
      <title>When Your Country Blocks the Internet, You Build Your Own Path</title>
      <dc:creator>Amir Reza Dalir</dc:creator>
      <pubDate>Thu, 12 Feb 2026 10:20:05 +0000</pubDate>
      <link>https://forem.com/dalirnet/when-your-country-blocks-the-internet-you-build-your-own-path-4dpp</link>
      <guid>https://forem.com/dalirnet/when-your-country-blocks-the-internet-you-build-your-own-path-4dpp</guid>
      <description>&lt;p&gt;I'm a developer unfortunately living in a country where the government blocks access to most of the internet — Twitter, YouTube, GitHub &lt;em&gt;(sometimes)&lt;/em&gt;, messaging apps, news sites... all filtered. Millions of people deal with this every single day.&lt;/p&gt;

&lt;p&gt;The obvious solution? A VPN server somewhere abroad. Spin up an instance on AWS or Hetzner, install a VPN, connect from home. Simple, right?&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Except it doesn't work.&lt;/strong&gt; ❌&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Local ISPs actively detect and block VPN protocols. Even if you get a connection working, the foreign IP gets blacklisted within days. You set it up, it works for a week, then it's dead. Start over.&lt;/p&gt;




&lt;h2&gt;
  
  
  🔍 The Real Problem
&lt;/h2&gt;

&lt;p&gt;You can't connect directly to a foreign server running a proxy. The connection gets detected and killed — not always, but almost mostly. But here's the thing — &lt;strong&gt;not all networks are filtered equally&lt;/strong&gt;. Each ISP behaves differently, and filtering varies from city to city. One ISP might block everything, while another provider in a different city lets certain traffic through.&lt;/p&gt;

&lt;p&gt;This means your middle server doesn't have to be in a datacenter. It can be:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;🏠 A home server on a &lt;strong&gt;different ISP&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;🏙️ A friend's machine in &lt;strong&gt;another city&lt;/strong&gt; with a static IP&lt;/li&gt;
&lt;li&gt;💰 A cheap VPS at a &lt;strong&gt;local hosting provider&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;As long as it can reach both your device and the foreign server, it works as an &lt;strong&gt;EDGE&lt;/strong&gt;.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The filtering mostly targets end-user residential connections to foreign IPs. Internal traffic — between ISPs, cities, datacenters — is &lt;strong&gt;far less restricted&lt;/strong&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;So the solution is a &lt;strong&gt;chain&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;📱 Your phone/laptop ➜ 🔗 Middle server (EDGE) ➜ 🌍 Exit server (GATEWAY) ➜ 🌐 Free internet
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Your device connects to the middle server &lt;em&gt;(fast, low latency, not blocked)&lt;/em&gt;. That server forwards everything to your exit server abroad. The exit server fetches the content and sends it back through the chain.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;This is exactly what I've been working on.&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  ⚡ Xray Chain Proxy
&lt;/h2&gt;

&lt;p&gt;This tool is built on top of &lt;a href="https://github.com/XTLS/Xray-core" rel="noopener noreferrer"&gt;Xray-core&lt;/a&gt; — one of the most powerful and battle-tested proxy platforms out there. Xray supports advanced protocols, encryption, and routing that make it extremely hard to detect and block. But configuring it manually is painful — JSON config files, multiple protocols, user management, all by hand, and repeating the whole process every time a server gets blocked.&lt;/p&gt;

&lt;p&gt;So I wrote a &lt;strong&gt;single bash script&lt;/strong&gt; that wraps all of Xray's power into simple commands.&lt;/p&gt;

&lt;p&gt;Two servers, two commands:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# On your foreign server (e.g., AWS in Frankfurt)&lt;/span&gt;
./xcp.sh setup gateway

&lt;span class="c"&gt;# On your local server (inside your country)&lt;/span&gt;
./xcp.sh setup edge
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;That's the entire setup.&lt;/strong&gt; ✅&lt;/p&gt;

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

&lt;p&gt;The &lt;strong&gt;gateway&lt;/strong&gt; setup configures your exit node — the server with free internet. It gives you the IP, ports, and a password.&lt;/p&gt;

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

&lt;p&gt;Then you enter those details on the &lt;strong&gt;edge&lt;/strong&gt; server (the local one). The encrypted chain between the two is established automatically.&lt;/p&gt;




&lt;h2&gt;
  
  
  🏗️ How the Architecture Works
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;┌────────┐      ┌────────┐      ┌─────────┐      ┌──────────┐
│ Client │ ──── │  EDGE  │ ──── │ GATEWAY │ ──── │ Internet │
│ (You)  │      │(Local) │      │  (AWS)  │      │          │
└────────┘      └────────┘      └─────────┘      └──────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Server&lt;/th&gt;
&lt;th&gt;Location&lt;/th&gt;
&lt;th&gt;Role&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;EDGE&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Local datacenter / home server&lt;/td&gt;
&lt;td&gt;🚪 Entry point — your devices connect here&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;GATEWAY&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Foreign server (AWS, Hetzner, etc.)&lt;/td&gt;
&lt;td&gt;🌍 Exit point — fetches content from the internet&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Why this works:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;🟢 &lt;strong&gt;EDGE is local&lt;/strong&gt; — your ISP sees a connection to a local IP, nothing suspicious&lt;/li&gt;
&lt;li&gt;🔒 &lt;strong&gt;GATEWAY is hidden&lt;/strong&gt; — censors never see it directly, only the EDGE talks to it&lt;/li&gt;
&lt;li&gt;🔐 &lt;strong&gt;Traffic is AES-256-GCM encrypted&lt;/strong&gt; between EDGE and GATEWAY&lt;/li&gt;
&lt;li&gt;🔄 &lt;strong&gt;If EDGE gets blocked&lt;/strong&gt; — spin up a new server, run one command, done in 2 minutes&lt;/li&gt;
&lt;li&gt;🛡️ &lt;strong&gt;GATEWAY stays safe&lt;/strong&gt; — it never changes, no one knows about it except your EDGE&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  📡 3 Protocols at Once
&lt;/h2&gt;

&lt;p&gt;Each server runs three protocols simultaneously:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Protocol&lt;/th&gt;
&lt;th&gt;Port&lt;/th&gt;
&lt;th&gt;Best For&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Shadowsocks&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;443&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;📱 Mobile apps (v2rayNG, Shadowrocket), looks like HTTPS&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;HTTP&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;80&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;🌐 Browser proxy, curl&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;SOCKS5&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;1080&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;💻 System-wide proxy on desktop&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;All share the same username and password. Connect with whatever works best for your device.&lt;/p&gt;




&lt;h2&gt;
  
  
  👥 Adding Users
&lt;/h2&gt;

&lt;p&gt;I share my proxy with family and friends. Adding a new user takes seconds:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;./xcp.sh user add
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;p&gt;It generates the credentials, a &lt;strong&gt;QR code&lt;/strong&gt; &lt;em&gt;(scan with your phone)&lt;/em&gt;, and a Shadowsocks URI you can share directly.&lt;/p&gt;




&lt;h2&gt;
  
  
  📊 Monitoring
&lt;/h2&gt;

&lt;p&gt;When you share with others, you want to know what's happening.&lt;/p&gt;

&lt;h3&gt;
  
  
  Check if everything is running
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;./xcp.sh status
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;h3&gt;
  
  
  See who's using how much bandwidth
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;./xcp.sh stats
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;h3&gt;
  
  
  Test the full chain
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;./xcp.sh &lt;span class="nb"&gt;test&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;p&gt;This verifies the chain is working and shows the exit IP &lt;em&gt;(should be your GATEWAY's IP)&lt;/em&gt; plus speed measurements.&lt;/p&gt;




&lt;h2&gt;
  
  
  🧭 Smart Routing
&lt;/h2&gt;

&lt;p&gt;Not everything needs to go through the foreign server. Local websites work fine directly — routing them through AWS just adds latency for no reason.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;./xcp.sh rule add
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;p&gt;Real examples I use:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;🏠 &lt;strong&gt;Local sites direct&lt;/strong&gt; &lt;em&gt;(no proxy needed)&lt;/em&gt;: &lt;code&gt;geosite:ir&lt;/code&gt; as &lt;code&gt;direct&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;🚫 &lt;strong&gt;Block ads&lt;/strong&gt;: &lt;code&gt;geosite:category-ads-all&lt;/code&gt; as &lt;code&gt;blocked&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;🌐 &lt;strong&gt;Social media through proxy&lt;/strong&gt;: &lt;code&gt;twitter.com, instagram.com, youtube.com&lt;/code&gt; as &lt;code&gt;proxy&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This way, local sites stay fast and only filtered content goes through the chain.&lt;/p&gt;




&lt;h2&gt;
  
  
  📖 Documentation
&lt;/h2&gt;

&lt;p&gt;Full documentation is available in &lt;strong&gt;English&lt;/strong&gt; and &lt;strong&gt;Persian&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;🇬🇧 &lt;a href="https://dalirnet.github.io/xray-chain-proxy/" rel="noopener noreferrer"&gt;English Docs&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;🇮🇷 &lt;a href="https://dalirnet.github.io/xray-chain-proxy/#/fa/" rel="noopener noreferrer"&gt;Persian Docs&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Covers all commands, configuration options, routing rules, and more.&lt;/p&gt;




&lt;h2&gt;
  
  
  🔄 When the EDGE Gets Blocked
&lt;/h2&gt;

&lt;p&gt;It happens. The local server's IP gets flagged and your connection drops. Here's my workflow:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Spin up a new VPS at a local datacenter &lt;em&gt;(takes 1 minute)&lt;/em&gt;
&lt;/li&gt;
&lt;li&gt;Download the script: &lt;code&gt;curl -sL ... -o xcp.sh &amp;amp;&amp;amp; chmod +x xcp.sh&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Run: &lt;code&gt;./xcp.sh setup edge&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Enter the same GATEWAY details&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Done.&lt;/strong&gt; New EDGE, same chain, &lt;strong&gt;2 minutes total&lt;/strong&gt; ⏱️&lt;/li&gt;
&lt;/ol&gt;

&lt;blockquote&gt;
&lt;p&gt;The GATEWAY never changes. Only the EDGE rotates. Your users just update the server IP and they're back online.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  📦 Requirements
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Debian/Ubuntu with root access&lt;/li&gt;
&lt;li&gt;512 MB RAM, 1 CPU &lt;em&gt;(cheapest VPS works)&lt;/em&gt;
&lt;/li&gt;
&lt;li&gt;Dependencies (&lt;code&gt;curl&lt;/code&gt;, &lt;code&gt;jq&lt;/code&gt;, &lt;code&gt;unzip&lt;/code&gt;) are auto-checked&lt;/li&gt;
&lt;li&gt;Works on &lt;strong&gt;x86_64&lt;/strong&gt;, &lt;strong&gt;ARM64&lt;/strong&gt;, and &lt;strong&gt;ARM32&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  🚀 Get Started
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-sL&lt;/span&gt; https://raw.githubusercontent.com/dalirnet/xray-chain-proxy/main/script.sh &lt;span class="nt"&gt;-o&lt;/span&gt; xcp.sh
&lt;span class="nb"&gt;chmod&lt;/span&gt; +x xcp.sh
./xcp.sh setup gateway  &lt;span class="c"&gt;# on foreign server&lt;/span&gt;
./xcp.sh setup edge     &lt;span class="c"&gt;# on local server&lt;/span&gt;
./xcp.sh user add       &lt;span class="c"&gt;# create your account&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;GitHub:&lt;/strong&gt; &lt;a href="https://github.com/dalirnet/xray-chain-proxy" rel="noopener noreferrer"&gt;github.com/dalirnet/xray-chain-proxy&lt;/a&gt;&lt;br&gt;
&lt;strong&gt;Docs:&lt;/strong&gt; &lt;a href="https://dalirnet.github.io/xray-chain-proxy/" rel="noopener noreferrer"&gt;dalirnet.github.io/xray-chain-proxy&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;This tool exists because I needed it. If you're in a similar situation — Iran, China, Russia, or anywhere else with internet restrictions — I hope it helps. ⭐ A star on GitHub helps others find it.&lt;/p&gt;

</description>
      <category>opensource</category>
      <category>proxy</category>
      <category>privacy</category>
      <category>censorship</category>
    </item>
    <item>
      <title>I Built a Native macOS Authenticator App Because I Was Tired of Reaching for My Phone</title>
      <dc:creator>Amir Reza Dalir</dc:creator>
      <pubDate>Wed, 11 Feb 2026 07:37:15 +0000</pubDate>
      <link>https://forem.com/dalirnet/i-built-a-native-macos-authenticator-app-because-i-was-tired-of-reaching-for-my-phone-5899</link>
      <guid>https://forem.com/dalirnet/i-built-a-native-macos-authenticator-app-because-i-was-tired-of-reaching-for-my-phone-5899</guid>
      <description>&lt;p&gt;You're working on your Mac, logging into a service, and then — "Enter your verification code." You stop. You reach for your phone. You unlock it. You open the authenticator app. You squint at tiny digits. You type them in before the timer runs out. If you're lucky, you make it. If not, you wait for the next code and do it again.&lt;/p&gt;

&lt;p&gt;I got tired of this, so I built &lt;strong&gt;Mactokio&lt;/strong&gt; (&lt;strong&gt;Mac&lt;/strong&gt; + &lt;strong&gt;Tok&lt;/strong&gt;en + &lt;strong&gt;I/O&lt;/strong&gt;) — a free, open-source authenticator that lives right on your Mac.&lt;/p&gt;




&lt;h2&gt;
  
  
  What is Mactokio?
&lt;/h2&gt;

&lt;p&gt;Mactokio is a lightweight authenticator app built natively for macOS. It generates the same verification codes as Google Authenticator or Microsoft Authenticator — but directly on your desktop. No phone needed.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Free. Open source. Offline. Your secrets never leave your Mac.&lt;/strong&gt;&lt;br&gt;
No cloud. No account. No subscription.&lt;/p&gt;
&lt;/blockquote&gt;

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




&lt;h2&gt;
  
  
  Why Not Just Use What's Already Out There?
&lt;/h2&gt;

&lt;p&gt;Most authenticator solutions fall into two camps:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Phone-only apps&lt;/th&gt;
&lt;th&gt;Cloud-synced apps&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;Mactokio&lt;/strong&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Examples&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Google Authenticator, Microsoft Authenticator&lt;/td&gt;
&lt;td&gt;Authy, 1Password&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Works on Mac?&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Yes&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Secrets stored&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;On your phone&lt;/td&gt;
&lt;td&gt;On their servers&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;On your Mac only&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Requires account?&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;No&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Cost&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Free&lt;/td&gt;
&lt;td&gt;Free / Paid&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Free&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Open source?&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Yes&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Mactokio is the third option: &lt;strong&gt;your codes, on your Mac, encrypted on your disk, and nowhere else.&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  The Killer Feature: Safe Account Import via Webcam
&lt;/h2&gt;

&lt;p&gt;Let's talk about the elephant in the room. How do you actually move your authenticator accounts from your phone to your Mac?&lt;/p&gt;

&lt;p&gt;Here's what the typical workflow looks like:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Open Google Authenticator on your phone&lt;/li&gt;
&lt;li&gt;Go to &lt;strong&gt;Transfer accounts → Export&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;A QR code appears — this QR contains &lt;strong&gt;every single one of your secret keys&lt;/strong&gt; in readable form&lt;/li&gt;
&lt;li&gt;You take a screenshot&lt;/li&gt;
&lt;li&gt;Now you need to get that screenshot to your Mac&lt;/li&gt;
&lt;li&gt;So you send it via... Email? Telegram? WhatsApp? AirDrop?&lt;/li&gt;
&lt;/ol&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Think about what just happened.&lt;/strong&gt; That screenshot — containing the master keys to all your accounts — is now:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Sitting in your &lt;strong&gt;email sent folder&lt;/strong&gt; (stored on Google/Microsoft servers)&lt;/li&gt;
&lt;li&gt;Saved in your &lt;strong&gt;Telegram/WhatsApp chat history&lt;/strong&gt; (on their cloud servers)&lt;/li&gt;
&lt;li&gt;In your phone's &lt;strong&gt;photo gallery&lt;/strong&gt; (possibly auto-synced to iCloud or Google Photos)&lt;/li&gt;
&lt;li&gt;Cached in whatever transfer app you used&lt;/li&gt;
&lt;li&gt;Potentially visible to anyone who gains access to any of those services&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;

&lt;p&gt;You just took the most sensitive data you own and scattered copies of it across the internet. &lt;strong&gt;The very act of transferring your secrets has created a bigger security risk than not having two-factor authentication at all.&lt;/strong&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Mactokio's Approach: Just Point Your Camera
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Mactokio eliminates this entire problem.&lt;/strong&gt; Instead of transferring a file, you simply hold your phone up to your Mac's webcam.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Step&lt;/th&gt;
&lt;th&gt;What you do&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;1&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Open Google Authenticator → &lt;strong&gt;Transfer accounts → Export&lt;/strong&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;2&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Open Mactokio → click &lt;strong&gt;+&lt;/strong&gt; → &lt;strong&gt;From Camera&lt;/strong&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;3&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Hold your phone's screen up to your Mac's webcam&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;4&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Done. All your accounts are imported.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;No screenshot. No file transfer. No email. No messaging app. No cloud.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The QR code goes directly from your phone's screen, through the air, into your Mac's camera, gets encrypted, and is stored safely on your disk. The secret never exists as a file, never touches a network, and never leaves the space between your two devices.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Your secrets travel through physical space, not through the internet.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  The Camera is Smart About It
&lt;/h3&gt;

&lt;p&gt;The scanner doesn't just grab the first QR it sees. It carefully watches the QR code across &lt;strong&gt;multiple frames&lt;/strong&gt; before accepting it — this prevents mistakes from partial scans or blurry images. You'll see clear visual feedback the whole time:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;What you see&lt;/th&gt;
&lt;th&gt;What it means&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Highlight border&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Scanner found a QR code, verifying...&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Green border&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Valid authenticator code — imported!&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Red border&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;QR found but not a valid authenticator code&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;A momentary hand wobble won't reset the scan — there's a built-in grace period. Just hold your phone reasonably steady and the app does the rest.&lt;/p&gt;

&lt;h3&gt;
  
  
  All Your Accounts in One Scan
&lt;/h3&gt;

&lt;p&gt;Google Authenticator's export puts all your accounts into a single QR code. Mactokio fully supports this — &lt;strong&gt;one scan imports every account at once&lt;/strong&gt;. Each secret is individually encrypted. One scan, all your accounts, zero exposure.&lt;/p&gt;

&lt;p&gt;
  Other ways to import (From File / From Clipboard)
  &lt;p&gt;The camera is the safest and recommended way, but Mactokio also supports:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;From File&lt;/strong&gt; — select a QR code image or a text file&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;From Clipboard&lt;/strong&gt; — paste a QR screenshot or a link&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;With every method, the same principle applies: the secret is encrypted the instant it's read, and is never stored unprotected on your disk.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;But if you can use the camera, use the camera.&lt;/strong&gt; It's the only method where your secret never exists as a digital file outside your two devices.&lt;/p&gt;



&lt;/p&gt;




&lt;h2&gt;
  
  
  How Mactokio Protects Your Secrets
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Your Mac Guards the Door — Touch ID &amp;amp; Device Password
&lt;/h3&gt;

&lt;p&gt;Before you can see anything, Mactokio asks for &lt;strong&gt;Touch ID or your Mac's password&lt;/strong&gt; — the same authentication you use to log into your Mac. This isn't some new password that Mactokio asks you to create. It's your Mac's own built-in security, managed by Apple.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Touch ID&lt;/strong&gt; (fingerprint) if your Mac supports it&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Your Mac password&lt;/strong&gt; as a fallback&lt;/li&gt;
&lt;li&gt;Required &lt;strong&gt;every single time&lt;/strong&gt; you open the app — no "stay logged in", no "remember me"&lt;/li&gt;
&lt;li&gt;If someone sits down at your unlocked Mac, they still can't see your codes without your fingerprint or password&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;You already trust this to protect your entire Mac. Mactokio simply puts the same lock on your verification codes.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  2. Your Secrets Are Locked to Your Hardware
&lt;/h3&gt;

&lt;p&gt;Your secrets are protected by a second layer: &lt;strong&gt;AES-256 encryption&lt;/strong&gt; — the same standard used by governments and banks to protect classified data.&lt;/p&gt;

&lt;p&gt;The encryption key comes from your &lt;strong&gt;Mac's unique hardware identity&lt;/strong&gt;. It's not a password you choose or type. It's something only your specific Mac can produce.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Your encrypted secrets are &lt;strong&gt;useless on any other computer&lt;/strong&gt; — even if someone copies the files&lt;/li&gt;
&lt;li&gt;There's &lt;strong&gt;no master password&lt;/strong&gt; to remember, forget, or have stolen&lt;/li&gt;
&lt;li&gt;Even if your Mac is stolen and someone extracts the hard drive, the secrets are unreadable without the original hardware&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Two layers working together:&lt;/strong&gt; your fingerprint (or password) controls who can open the app, and your Mac's hardware controls who can read the data.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  3. Completely Offline
&lt;/h3&gt;

&lt;p&gt;Mactokio makes &lt;strong&gt;zero internet connections&lt;/strong&gt;. None.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;What some apps do&lt;/th&gt;
&lt;th&gt;What Mactokio does&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Phone home on launch&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Nothing&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Check for updates online&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Nothing&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Send usage analytics&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Nothing&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sync to cloud&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Nothing&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Your secrets exist in exactly one place: encrypted on your Mac.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Auto-Lock
&lt;/h3&gt;

&lt;p&gt;If you reveal a code and walk away, it hides itself automatically after about 90 seconds. Your codes aren't left sitting on screen for anyone to see.&lt;/p&gt;




&lt;h2&gt;
  
  
  What You Get
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Feature&lt;/th&gt;
&lt;th&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Verification codes on your Mac&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;No more reaching for your phone&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Works with any service&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Google, GitHub, AWS, Discord — anything that uses authenticator codes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Search and filter&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Quickly find the account you need&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;One-click copy&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Click a code → it's on your clipboard → paste it&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Visual countdown&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;See exactly how much time before the code changes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Clean, minimal interface&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Small window that stays out of your way&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Free and open source&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;No subscriptions, no ads, no hidden costs&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  Getting Started
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Step 1: Install
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;Download &lt;code&gt;Mactokio.zip&lt;/code&gt; from the &lt;a href="https://github.com/dalirnet/mactokio/releases/latest" rel="noopener noreferrer"&gt;latest release on GitHub&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Unzip and move &lt;strong&gt;Mactokio.app&lt;/strong&gt; to your Applications folder&lt;/li&gt;
&lt;li&gt;Right-click the app → click &lt;strong&gt;Open&lt;/strong&gt; (required the first time only, because the app isn't from the App Store)&lt;/li&gt;
&lt;li&gt;Authenticate with Touch ID or your Mac password&lt;/li&gt;
&lt;/ol&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;That's it.&lt;/strong&gt; No account creation, no setup wizard, no configuration.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Step 2: Import Your Accounts
&lt;/h3&gt;

&lt;p&gt;The recommended way — &lt;strong&gt;From Camera:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;On your phone, open your authenticator app and go to the export or transfer option&lt;/li&gt;
&lt;li&gt;In Mactokio, click the &lt;strong&gt;+&lt;/strong&gt; button → &lt;strong&gt;From Camera&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Hold your phone's QR code up to your Mac's webcam&lt;/li&gt;
&lt;li&gt;Mactokio detects the code and imports your accounts automatically&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Your accounts appear in the list, encrypted and ready to use.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Tip:&lt;/strong&gt; You can also add accounts one at a time — whenever a website shows you a QR code to set up two-factor authentication, just scan it with Mactokio's camera instead of (or in addition to) your phone.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Step 3: Use Your Codes
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;Click an account to reveal its current code&lt;/li&gt;
&lt;li&gt;Click the code to copy it to your clipboard&lt;/li&gt;
&lt;li&gt;Paste it wherever you need it&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Codes auto-hide after about 90 seconds for security.&lt;/p&gt;




&lt;h2&gt;
  
  
  It's Free and Open Source
&lt;/h2&gt;

&lt;p&gt;Mactokio is completely free under the MIT license. The entire source code is publicly available on GitHub — anyone can inspect exactly how the app handles your secrets, build it from source, or contribute improvements.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/dalirnet/mactokio" class="crayons-btn crayons-btn--primary" rel="noopener noreferrer"&gt;Star Mactokio on GitHub&lt;/a&gt;
&lt;/p&gt;

&lt;p&gt;If Mactokio saves you from reaching for your phone, a star on GitHub helps others discover the project.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Mactokio requires macOS 13 or later. No accounts. No cloud. No tracking. Just your codes, on your Mac.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>opensource</category>
      <category>macos</category>
      <category>security</category>
      <category>productivity</category>
    </item>
    <item>
      <title>Eliminate 80% of Nuxt store boilerplate with a single createStore call</title>
      <dc:creator>Amir Reza Dalir</dc:creator>
      <pubDate>Tue, 10 Feb 2026 20:00:29 +0000</pubDate>
      <link>https://forem.com/diphyx/eliminate-80-of-nuxt-store-boilerplate-with-a-single-createstore-call-o42</link>
      <guid>https://forem.com/diphyx/eliminate-80-of-nuxt-store-boilerplate-with-a-single-createstore-call-o42</guid>
      <description>&lt;p&gt;Every Nuxt app has the same problem.&lt;/p&gt;

&lt;p&gt;For every API resource — users, posts, products, orders — the same code gets written:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Define a TypeScript interface&lt;/li&gt;
&lt;li&gt;Create reactive state (&lt;code&gt;ref&lt;/code&gt;, &lt;code&gt;reactive&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Create a loading ref&lt;/li&gt;
&lt;li&gt;Create an error ref&lt;/li&gt;
&lt;li&gt;Write a fetch function with try/catch/finally&lt;/li&gt;
&lt;li&gt;Repeat for create, update, delete&lt;/li&gt;
&lt;li&gt;Repeat for the next resource&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://github.com/diphyx/harlemify" rel="noopener noreferrer"&gt;Harlemify&lt;/a&gt; is an open-source Nuxt module by &lt;a href="https://github.com/diphyx" rel="noopener noreferrer"&gt;Diphyx&lt;/a&gt; that eliminates this repetition. It's built on top of &lt;a href="https://harlemjs.com/" rel="noopener noreferrer"&gt;Harlem&lt;/a&gt;, a powerful and extensible state management library for Vue 3 — so the reactive store layer is solid and battle-tested.&lt;/p&gt;

&lt;h2&gt;
  
  
  Define your data shape once, get everything else for free
&lt;/h2&gt;

&lt;p&gt;Harlemify generates typed stores from Zod schemas. Here's what a full CRUD store looks like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;createStore&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;shape&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;ModelOneMode&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;ModelManyMode&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@diphyx/harlemify/runtime&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;userShape&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;shape&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;factory&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;factory&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;number&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;meta&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
            &lt;span class="na"&gt;identifier&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;}),&lt;/span&gt;
        &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;factory&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
        &lt;span class="na"&gt;email&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;factory&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;email&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;userStore&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;createStore&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;users&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nf"&gt;model&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;one&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;many&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="na"&gt;current&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;one&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;userShape&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="na"&gt;list&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;many&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;userShape&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="p"&gt;};&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="nf"&gt;view&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="na"&gt;user&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;current&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="na"&gt;users&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;list&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="p"&gt;};&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="nf"&gt;action&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;api&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="na"&gt;list&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;api&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="p"&gt;{&lt;/span&gt;
                    &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/users&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="p"&gt;},&lt;/span&gt;
                &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;model&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;list&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;mode&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ModelManyMode&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;SET&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
            &lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="na"&gt;get&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;api&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="p"&gt;{&lt;/span&gt;
                    &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/users/:id&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="p"&gt;},&lt;/span&gt;
                &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;model&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;current&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;mode&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ModelOneMode&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;SET&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
            &lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="na"&gt;create&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;api&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="p"&gt;{&lt;/span&gt;
                    &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/users&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="p"&gt;},&lt;/span&gt;
                &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;model&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;list&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;mode&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ModelManyMode&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ADD&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
            &lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="na"&gt;delete&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;api&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;delete&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="p"&gt;{&lt;/span&gt;
                    &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/users/:id&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="p"&gt;},&lt;/span&gt;
                &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;model&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;list&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;mode&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ModelManyMode&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;REMOVE&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
            &lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="p"&gt;};&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No separate type definitions. No manual loading/error refs. No try/catch boilerplate.&lt;/p&gt;

&lt;h2&gt;
  
  
  What comes out of this single definition
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Typed models&lt;/strong&gt; — &lt;code&gt;current&lt;/code&gt; holds a single user, &lt;code&gt;list&lt;/code&gt; holds a collection. Both are fully typed from the Zod shape.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Reactive views&lt;/strong&gt; — &lt;code&gt;user&lt;/code&gt; and &lt;code&gt;users&lt;/code&gt; are computed refs that update whenever the underlying model changes. Powered by Harlem's reactive store engine under the hood.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;API actions with auto-commit&lt;/strong&gt; — Each action knows which model to update and how (SET replaces, ADD appends, REMOVE deletes by identifier).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Automatic status tracking&lt;/strong&gt; — Every action exposes &lt;code&gt;loading&lt;/code&gt;, &lt;code&gt;error&lt;/code&gt;, and &lt;code&gt;status&lt;/code&gt; without writing a single ref.&lt;/p&gt;

&lt;h2&gt;
  
  
  Using it in components
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight vue"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;script&lt;/span&gt; &lt;span class="na"&gt;setup&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;loading&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;error&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useStoreAction&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;userStore&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;list&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;users&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useStoreView&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;userStore&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;users&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="k"&gt;script&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;

&lt;span class="nt"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;template&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;p&lt;/span&gt; &lt;span class="na"&gt;v-if=&lt;/span&gt;&lt;span class="s"&gt;"error"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;{{&lt;/span&gt; &lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt; &lt;span class="si"&gt;}}&lt;/span&gt;&lt;span class="nt"&gt;&amp;lt;/p&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;ul&lt;/span&gt; &lt;span class="na"&gt;v-else-if=&lt;/span&gt;&lt;span class="s"&gt;"!loading"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;li&lt;/span&gt; &lt;span class="na"&gt;v-for=&lt;/span&gt;&lt;span class="s"&gt;"user in users"&lt;/span&gt; &lt;span class="na"&gt;:key=&lt;/span&gt;&lt;span class="s"&gt;"user.id"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;{{&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt; &lt;span class="si"&gt;}}&lt;/span&gt;&lt;span class="nt"&gt;&amp;lt;/li&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/ul&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="k"&gt;template&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three composables cover everything:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;useStoreAction&lt;/code&gt; — execute actions, track loading/error/status&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;useStoreView&lt;/code&gt; — access reactive computed state&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;useStoreModel&lt;/code&gt; — mutate state directly (set, patch, add, remove, reset)&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Beyond basic state management
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Concurrency control
&lt;/h3&gt;

&lt;p&gt;Every action can be configured with a concurrency strategy:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;BLOCK&lt;/strong&gt; — throw if the action is already running&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SKIP&lt;/strong&gt; — return the existing promise&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CANCEL&lt;/strong&gt; — abort the previous call, start a new one&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;ALLOW&lt;/strong&gt; — run both in parallel&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;No more debouncing hacks or manual AbortController wiring.&lt;/p&gt;

&lt;h3&gt;
  
  
  Record collections
&lt;/h3&gt;

&lt;p&gt;For grouped data (e.g., users by team), use record mode:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nf"&gt;model&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;many&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;byTeam&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;many&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;userShape&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ModelManyKind&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;RECORD&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt;
    &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;},&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This gives you &lt;code&gt;{ teamA: [...users], teamB: [...users] }&lt;/code&gt; with typed CRUD operations per key.&lt;/p&gt;

&lt;h3&gt;
  
  
  Handler actions
&lt;/h3&gt;

&lt;p&gt;Not every action is an API call. Custom logic gets first-class support:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nf"&gt;action&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;handler&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;sortByName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;handler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;model&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;sorted&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[...&lt;/span&gt;&lt;span class="nx"&gt;model&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;list&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;sort&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;localeCompare&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
            &lt;span class="nx"&gt;model&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;list&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;sorted&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="p"&gt;}),&lt;/span&gt;
    &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;},&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Handlers have full access to models and views, with the same status tracking as API actions.&lt;/p&gt;

&lt;h3&gt;
  
  
  SSR with automatic hydration
&lt;/h3&gt;

&lt;p&gt;Harlemify uses Harlem's SSR plugin (&lt;code&gt;@harlem/plugin-ssr&lt;/code&gt;) under the hood. Server-rendered state is automatically hydrated on the client — no manual transfer or serialization needed.&lt;/p&gt;

&lt;h2&gt;
  
  
  How it compares
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Manual / Pinia&lt;/th&gt;
&lt;th&gt;Harlemify&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Schema&lt;/td&gt;
&lt;td&gt;Separate TypeScript interfaces&lt;/td&gt;
&lt;td&gt;Zod shape — types + validation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;API calls&lt;/td&gt;
&lt;td&gt;Manual fetch + state updates&lt;/td&gt;
&lt;td&gt;Declarative with auto-commit&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Loading/error&lt;/td&gt;
&lt;td&gt;Manual refs per action&lt;/td&gt;
&lt;td&gt;Automatic per action&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Concurrency&lt;/td&gt;
&lt;td&gt;Not built-in&lt;/td&gt;
&lt;td&gt;Block, Skip, Cancel, Allow&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Collections&lt;/td&gt;
&lt;td&gt;Manual array management&lt;/td&gt;
&lt;td&gt;Built-in modes (SET, ADD, PATCH, REMOVE)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SSR&lt;/td&gt;
&lt;td&gt;Plugin required&lt;/td&gt;
&lt;td&gt;Built-in via Harlem SSR&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Lines per resource&lt;/td&gt;
&lt;td&gt;~50-60&lt;/td&gt;
&lt;td&gt;~15-20&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Harlemify is not a Pinia replacement. Pinia is great for general-purpose client state. Harlemify is built on top of Harlem and optimized for &lt;strong&gt;API-driven data&lt;/strong&gt; — the CRUD resources that make up 80% of most apps.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install&lt;/span&gt; @diphyx/harlemify
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// nuxt.config.ts&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="nf"&gt;defineNuxtConfig&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;modules&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@diphyx/harlemify&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;&lt;a href="https://diphyx.github.io/harlemify/" rel="noopener noreferrer"&gt;Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/diphyx/harlemify" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.npmjs.com/package/@diphyx/harlemify" rel="noopener noreferrer"&gt;npm&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Feedback and contributions are welcome — star the repo if it's useful, or open an issue for anything that's missing.&lt;/p&gt;

</description>
      <category>vue</category>
      <category>nuxt</category>
      <category>typescript</category>
      <category>opensource</category>
    </item>
  </channel>
</rss>
