<?xml version="1.0" encoding="UTF-8"?><rss version="2.0"
	xmlns:content="http://purl.org/rss/1.0/modules/content/"
	xmlns:wfw="http://wellformedweb.org/CommentAPI/"
	xmlns:dc="http://purl.org/dc/elements/1.1/"
	xmlns:atom="http://www.w3.org/2005/Atom"
	xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
	xmlns:slash="http://purl.org/rss/1.0/modules/slash/"
	>

<channel>
	<title>Codango® / Codango.Com</title>
	<atom:link href="https://codango.com/feed/" rel="self" type="application/rss+xml" />
	<link>https://codango.com</link>
	<description></description>
	<lastBuildDate>Wed, 05 Aug 2026 04:54:57 +0000</lastBuildDate>
	<language>en-US</language>
	<sy:updatePeriod>
	hourly	</sy:updatePeriod>
	<sy:updateFrequency>
	1	</sy:updateFrequency>
	<generator>https://wordpress.org/?v=6.9</generator>

<image>
	<url>https://codango.com/wp-content/uploads/cropped-faviconpng-32x32.png</url>
	<title>Codango® / Codango.Com</title>
	<link>https://codango.com</link>
	<width>32</width>
	<height>32</height>
</image> 
	<item>
		<title>Detecting SSH Brute Force Attacks with Python: Building a Simple Monitor</title>
		<link>https://codango.com/detecting-ssh-brute-force-attacks-with-python-building-a-simple-monitor/</link>
					<comments>https://codango.com/detecting-ssh-brute-force-attacks-with-python-building-a-simple-monitor/#respond</comments>
		
		<dc:creator><![CDATA[Codango Admin]]></dc:creator>
		<pubDate>Wed, 05 Aug 2026 04:54:57 +0000</pubDate>
				<category><![CDATA[Codango® Blog]]></category>
		<guid isPermaLink="false">https://codango.com/detecting-ssh-brute-force-attacks-with-python-building-a-simple-monitor/</guid>

					<description><![CDATA[One of the most common and persistent threats to any server exposed to the internet is the brute force SSH attack. These are automated attempts to guess login credentials by <a class="more-link" href="https://codango.com/detecting-ssh-brute-force-attacks-with-python-building-a-simple-monitor/">Continue reading <span class="screen-reader-text">  Detecting SSH Brute Force Attacks with Python: Building a Simple Monitor</span><span class="meta-nav">&#8594;</span></a>]]></description>
										<content:encoded><![CDATA[<p>One of the most common and persistent threats to any server exposed to the internet is the brute force SSH attack. These are automated attempts to guess login credentials by repeatedly trying combinations of usernames and passwords. Left unchecked, they can waste system resources, fill up log files, and occasionally even succeed if weak credentials are in use.</p>
<p>Fortunately, detecting brute force attacks is well within reach using Python. By monitoring your system’s authentication logs and tracking failed login attempts, you can identify patterns, block offending IP addresses, and alert administrators before any real damage is done.</p>
<p>In this article, we will walk through the process of building a lightweight Python-based SSH brute force monitor. It is a practical and educational project that will strengthen both your Python skills and your understanding of real-world threats.</p>
<p>Let’s begin with where brute force attempts leave their trace — the logs. On most Linux systems, SSH login activity is recorded in <code>/var/log/auth.log</code> or <code>/var/log/secure</code> depending on the distribution. Every failed attempt generates an entry including the username, IP address, and timestamp. By scanning this file, we can extract and analyze the data to detect abuse.</p>
<p>The basic idea is simple: if the same IP address fails to log in too many times within a short time frame, it is probably not a legitimate user. Your script can then log this activity, notify an admin, or even ban the IP address automatically.</p>
<p>You do not need complex tools to make this happen. With Python, you can open the log file, read it line by line, and use regular expressions to pull out the relevant parts. The script can keep track of failed attempts by IP in a dictionary and compare the number of attempts against a defined threshold.</p>
<p>Here is how the monitoring logic works at a high level:</p>
<ol>
<li>Read from the SSH authentication log
</li>
<li>Search for lines that indicate a failed login
</li>
<li>Extract the offending IP address and timestamp
</li>
<li>Keep a count of failed attempts per IP
</li>
<li>If an IP exceeds your threshold, log it as suspicious
</li>
<li>Optionally, trigger an alert or response action
</li>
</ol>
<p>This approach is simple, but very effective. You can run the script periodically using cron or keep it running continuously in the background, depending on your needs. You might also build in logging so you can review which IPs were blocked or flagged over time.</p>
<p>One thing to be careful of is avoiding re-parsing the same entries. If your script reads from the top of the log file every time it runs, you will get duplicate results. A good solution is to track the last read position using a small marker file, or even just process only new entries since the last run.</p>
<p>As your script evolves, there are many features you can add:</p>
<ul>
<li>
<strong>GeoIP lookup</strong>: See where attacks are coming from geographically
</li>
<li>
<strong>Whitelist</strong>: Avoid blocking trusted internal IP addresses
</li>
<li>
<strong>Firewall integration</strong>: Use tools like <code>iptables</code> or <code>ufw</code> to block attackers
</li>
<li>
<strong>Email alerts</strong>: Notify admins of suspicious behavior immediately
</li>
<li>
<strong>Dashboard logging</strong>: Send events to a web dashboard or database for further analysis
</li>
</ul>
<p>Python’s flexibility means you can tailor this tool to fit any environment. In smaller settings, it might be your first line of defense. In larger networks, it can supplement existing intrusion detection systems.</p>
<p>Here are a few enhancements to make your monitor more powerful:</p>
<ul>
<li>
<strong>Sliding time window</strong>: Track how many failed attempts occurred within a set time period, such as five minutes
</li>
<li>
<strong>Concurrency</strong>: Use threads or asynchronous processing to monitor multiple files or services at once
</li>
<li>
<strong>Success correlation</strong>: Detect suspicious activity followed by a successful login from the same IP
</li>
<li>
<strong>Log rotation support</strong>: Ensure your script handles rotated logs gracefully
</li>
</ul>
<p>The more context you can build around each event, the smarter your response will be. The goal is not just to stop attacks, but to understand them — their frequency, tactics, and origin.</p>
<p>Once you have your SSH monitor running, you will likely be surprised by how often brute force attempts happen. Even low-profile servers receive regular attention from automated bots. Being able to see this in action reinforces the importance of basic hardening measures like using strong credentials, disabling root login, and enabling key-based authentication.</p>
<p>If you are looking to practice your Python skills on a real problem that defenders face every day, building an SSH brute force monitor is an ideal project. It teaches you to parse logs, track state, automate responses, and think like both an attacker and a defender. Best of all, it is something you can build quickly and improve over time.</p>
<p>To go further, check out my 17-page PDF guide, <a href="https://asherbaum.gumroad.com/l/wvfrx" rel="noopener noreferrer">Mastering Cybersecurity with Python: The Complete Pro Guide to Network Defense</a>. It includes deeper dives into detection logic, script examples, and more hands-on projects for defensive Python development. You can download it now for just five dollars.</p>
<p>And if you have enjoyed this article or the series so far, I invite you to <a href="https://buymeacoffee.com/hexshift" rel="noopener noreferrer">buy me a coffee</a>. Your support helps me keep producing practical, focused cybersecurity content for learners and professionals alike.</p>]]></content:encoded>
					
					<wfw:commentRss>https://codango.com/detecting-ssh-brute-force-attacks-with-python-building-a-simple-monitor/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>Qwen3.8-Max vs Claude: What the 16-Day Coding Run and Benchmarks Really Show</title>
		<link>https://codango.com/qwen3-8-max-vs-claude-what-the-16-day-coding-run-and-benchmarks-really-show/</link>
					<comments>https://codango.com/qwen3-8-max-vs-claude-what-the-16-day-coding-run-and-benchmarks-really-show/#respond</comments>
		
		<dc:creator><![CDATA[Codango Admin]]></dc:creator>
		<pubDate>Tue, 04 Aug 2026 11:34:00 +0000</pubDate>
				<category><![CDATA[Codango® Blog]]></category>
		<guid isPermaLink="false">https://codango.com/qwen3-8-max-vs-claude-what-the-16-day-coding-run-and-benchmarks-really-show/</guid>

					<description><![CDATA[Liquid syntax error: Unknown tag &#8216;endraw&#8217;]]></description>
										<content:encoded><![CDATA[<p>Liquid syntax error: Unknown tag &#8216;endraw&#8217;</p>]]></content:encoded>
					
					<wfw:commentRss>https://codango.com/qwen3-8-max-vs-claude-what-the-16-day-coding-run-and-benchmarks-really-show/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>&#8220;5 Python mistakes I keep making as a beginner (and how I fixed them)&#8221;</title>
		<link>https://codango.com/5-python-mistakes-i-keep-making-as-a-beginner-and-how-i-fixed-them/</link>
					<comments>https://codango.com/5-python-mistakes-i-keep-making-as-a-beginner-and-how-i-fixed-them/#respond</comments>
		
		<dc:creator><![CDATA[Codango Admin]]></dc:creator>
		<pubDate>Tue, 04 Aug 2026 11:33:20 +0000</pubDate>
				<category><![CDATA[Codango® Blog]]></category>
		<guid isPermaLink="false">https://codango.com/5-python-mistakes-i-keep-making-as-a-beginner-and-how-i-fixed-them/</guid>

					<description><![CDATA[## 1. Using return instead of print inside a loop This one bit me early on. I wanted to print every item in a list, so I wrote something like <a class="more-link" href="https://codango.com/5-python-mistakes-i-keep-making-as-a-beginner-and-how-i-fixed-them/">Continue reading <span class="screen-reader-text">  &#8220;5 Python mistakes I keep making as a beginner (and how I fixed them)&#8221;</span><span class="meta-nav">&#8594;</span></a>]]></description>
										<content:encoded><![CDATA[<p><strong>## 1. Using return instead of print inside a loop</strong></p>
<p>This one bit me early on. I wanted to print every item in a list, so I wrote something like this:</p>
<p>python<br />
def show_items(items):<br />
    for item in items:<br />
        return item</p>
<p>show_items([&#8220;apple&#8221;, &#8220;banana&#8221;, &#8220;cherry&#8221;])</p>
<p>Run it, and&#8230; nothing prints. Why? return doesn&#8217;t just output a value it immediately exits the function. So the loop runs exactly once, hands back &#8220;apple&#8221;, and the function is done. banana and cherry never gets a chance.</p>
<p>The solution is simple once you see it:</p>
<p>python<br />
def show_items(items):<br />
    for item in items:<br />
        print(item)</p>
<p>show_items([&#8220;apple&#8221;, &#8220;banana&#8221;, &#8220;cherry&#8221;])</p>
<p>Lesson: print() displays something and lets execution continue. return hands a value back to whoever called the function and ends it. They can&#8217;t be taken for same.</p>
<ol>
<li>Writing a ternary expression as literal text inside an f-string</li>
</ol>
<p>I wanted to print whether a number was even or odd, so I wrote:</p>
<p>python<br />
num = 7<br />
print(f&#8221;The number is {&#8216;even&#8217; if num % 2 == 0 else &#8216;odd&#8217;})&#8221;)</p>
<p>Small typo — the closing } was in the wrong place, so part of my ternary logic ended up printed as literal text instead of being evaluated. It&#8217;s an easy mistake to make because f-string expressions look like normal code, but every {} boundary has to be exactly right.</p>
<p>Lesson: Anything you want evaluated inside an f-string has to be fully and correctly wrapped in {} — including the entire ternary expression, not just part of it.</p>
<ol>
<li>Nesting a plain string inside an f-string ternary</li>
</ol>
<p>A frustrating version of the same problem:</p>
<p>python<br />
status = &#8220;active&#8221;<br />
print(f&#8221;Status: {&#8216;User is online&#8217; if status == &#8216;active&#8217; else &#8216;User is offline: {status}&#8217;}&#8221;)</p>
<p>Here, the inner strings &#8216;User is online&#8217; and &#8216;User is offline: {status}&#8217; are just regular strings — not f-strings. So {status} inside that second string prints literally as {status} instead of showing the actual value.</p>
<p>The fix: make sure any inner string that needs interpolation is also an f-string:</p>
<p>python<br />
print(f&#8221;Status: {&#8216;User is online&#8217; if status == &#8216;active&#8217; else f&#8217;User is offline: {status}&#8217;}&#8221;)</p>
<p>Lesson: The f prefix only applies to the string it&#8217;s directly attached to. Nested strings need their own f if they need their own interpolation.</p>
<ol>
<li>Mixing up loop variables and the original list name<br />
python<br />
scores = [85, 92, 78, 90]</li>
</ol>
<p>for score in scores:<br />
    print(scores)   # oops — meant to print <code>score</code>, not <code>scores</code></p>
<p>This one doesn&#8217;t throw an error, which makes it worse — it just quietly prints the wrong thing four times instead of the four individual scores. It usually happens when I&#8217;m typing fast and my fingers default to the more &#8220;familiar&#8221; variable name.</p>
<p>Lesson: After writing a loop, it&#8217;s worth a quick second glance — is the singular loop variable being used inside the loop body, or did the plural list name sneak back in?</p>
<ol>
<li>Forgetting self when accessing an attribute inside a class method</li>
</ol>
<p>This one showed up once I started learning classes:</p>
<p>python<br />
class Dog:<br />
    def <strong>init</strong>(self, name):<br />
        self.name = name</p>
<div class="highlight js-code-highlight">
<pre class="highlight plaintext"><code>def bark(self):
    print(f"{name} says Woof!")   # here it is a NameError because name is not defined
</code></pre>
</div>
<p>I dropped the self. in front of name inside bark(), so Python went looking for a plain variable called name — which doesn&#8217;t exist inside that method. The fix is just remembering that any attribute stored on the object has to be accessed through self:</p>
<p>python<br />
    def bark(self):<br />
        print(f&#8221;{self.name} says Woof!&#8221;)</p>
<p>Lesson: Inside a class, self.attribute and a plain attribute are two completely different things. Only self.attribute refers to the data actually stored on the object.</p>
<p>None of these mistakes are advanced, they&#8217;re the kind of small, easy to miss errors that come from moving fast and not yet having the muscle memory for the syntax. Writing them down has honestly helped more than just fixing them silently and moving on. If you&#8217;re a beginner too, hopefully seeing these saves you a few minutes of confused debugging.<br />
although I&#8217;m having hard time keeping them in this small mind of mine, but practice makes it better which i&#8217;m not doing also because of university</p>]]></content:encoded>
					
					<wfw:commentRss>https://codango.com/5-python-mistakes-i-keep-making-as-a-beginner-and-how-i-fixed-them/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>LinkedIn gave everyone a &#8216;this looks like AI&#8217; button. I won&#8217;t press it.</title>
		<link>https://codango.com/linkedin-gave-everyone-a-this-looks-like-ai-button-i-wont-press-it/</link>
					<comments>https://codango.com/linkedin-gave-everyone-a-this-looks-like-ai-button-i-wont-press-it/#respond</comments>
		
		<dc:creator><![CDATA[Codango Admin]]></dc:creator>
		<pubDate>Tue, 04 Aug 2026 11:29:33 +0000</pubDate>
				<category><![CDATA[Codango® Blog]]></category>
		<guid isPermaLink="false">https://codango.com/linkedin-gave-everyone-a-this-looks-like-ai-button-i-wont-press-it/</guid>

					<description><![CDATA[This week LinkedIn rolled out a &#8220;Seems like AI slop&#8221; button. Tap it under someone&#8217;s post and the post disappears from your feed, while your report goes off to train <a class="more-link" href="https://codango.com/linkedin-gave-everyone-a-this-looks-like-ai-button-i-wont-press-it/">Continue reading <span class="screen-reader-text">  LinkedIn gave everyone a &#8216;this looks like AI&#8217; button. I won&#8217;t press it.</span><span class="meta-nav">&#8594;</span></a>]]></description>
										<content:encoded><![CDATA[<p>This week LinkedIn <a href="https://techcrunch.com/2026/07/30/linkedin-adds-a-button-to-report-ai-generated-slop/" rel="noopener noreferrer">rolled out</a> a &#8220;Seems like AI slop&#8221; button. Tap it under someone&#8217;s post and the post disappears from your feed, while your report goes off to train their filter. I heard about it through work — keeping an eye on what platforms are up to is part of the job — but I read it less as a marketer and more as someone who writes, and who now, apparently, walks around under suspicion.</p>
<p>LinkedIn&#8217;s logic isn&#8217;t hard to follow. According to the AI detector Pangram, they have <a href="https://www.pangram.com/blog/ai-in-your-feed" rel="noopener noreferrer">the highest share of AI content</a> of any platform: roughly a third of short posts and over forty percent of long ones. The feed really is drowning, and handing people a valve is a reasonable move.</p>
<p>It isn&#8217;t the valve that bothers me. It&#8217;s whose hands it got put in.</p>
<p>The &#8220;this is AI&#8221; verdict used to come from an algorithm, and even that missed plenty. Pangram itself claims a 0.01% false-positive rate — which sounds impressive until you multiply it by volume. One hundredth of a percent of millions of posts is still thousands of real people getting labelled as bots. And that&#8217;s the machine, the one somebody at least tested. Now the &#8220;this is AI&#8221; stamp has been handed to every passerby, and the cost of getting it wrong is zero. Tap, post gone, on with your coffee. You&#8217;ll never even find out you were wrong — there&#8217;s no feedback loop pointing back at you.</p>
<p>I haven&#8217;t touched the button once, and I don&#8217;t even like AI text — the smooth filler I spent <a href="https://dev.to/eugeniya_ivanova_4a58eadc/i-trained-an-ai-to-sound-like-me-then-spent-three-rounds-undoing-it-3k96">a whole post</a> pulling apart last time. But disliking slop and swatting a person are different reflexes with different results. Someone posts the best thing they know how to write, and someone else decides it &#8220;looks like a bot.&#8221; That&#8217;s one reader gone. That isn&#8217;t fighting slop. That&#8217;s slapping the hands of someone who&#8217;s only just reaching for the keyboard.</p>
<p>Here&#8217;s the personal part. I write with AI and I don&#8217;t hide it, but the text is mine: a pile of rules, defined styles, years of tuning, every paragraph edited by hand. Slow — though it used to be slower. AI isn&#8217;t my author, it&#8217;s a tool I don&#8217;t let grab the wheel. I wrote a whole article about that.</p>
<p>Then something happened that made it feel a lot more personal. A while back, in a work chat, I answered a former colleague — a plain message, a couple of paragraphs. She reads it and says: &#8220;Wow, you sound just like GPT — fast and structured.&#8221; A compliment. The catch is that about two minutes passed between her question and my answer. There wasn&#8217;t even enough time to open ChatGPT, write a prompt, wait for an answer, and paste it back. It was just a human who&#8217;s spent a long time working with words. And it read as a machine.</p>
<p>And for a couple of days I was pleased about it — imagine, mistaken for GPT, fast and clean. Now it turns out that&#8217;s exactly the thing I&#8217;m supposed to worry about. Same sentence. Same moment. A week later it had gone from a compliment to evidence, while nothing about me had changed. What changed is that there&#8217;s now a button for it.</p>
<p>Fluency is being read as forgery. Fast and clean means you didn&#8217;t write it yourself, and there&#8217;s now a button wired to that assumption.</p>
<p>The irony is that AI was trained on well-edited human prose — which is exactly why it comes out so smooth and combed. And that smoothness is now how we try to spot it: we hunt for the fingerprints of a good editor, and we reliably find them on people who simply know how to write. Congratulations to all of us.</p>
<p>My own way out of this is as old as it gets: divide and conquer. The machine is fast and a little too good — and sometimes not good at all, and that&#8217;s the line I can still see and it can&#8217;t. I handed it the posting across networks; works great — it&#8217;s mechanical anyway. But writing is different. No matter how much I train it, it keeps sliding back into &#8220;we did it this way before, so this must be right.&#8221; Except the new piece in front of it doesn&#8217;t fit the old rules, and it doesn&#8217;t notice that — I do. So here&#8217;s the split: authorship and judgment stay with the human, logistics go to the machine. That&#8217;s how I run it through <a href="https://publora.com/" rel="noopener noreferrer">Publora</a> (I&#8217;m on the team): I write by hand, and the delivery across platforms goes to an agent. No AI writes my posts — it&#8217;s on backup duty, in the corner, not at the wheel.</p>
<p>How this ends, honestly, I don&#8217;t know. For now I&#8217;ve stocked up on popcorn and I&#8217;m watching from two angles. Angle one: people start writing worse on purpose — breaking the rhythm, sprinkling in typos, watering it down, anything to avoid looking suspiciously smooth. A platform that penalizes quality, and a crowd cheerfully dumbing itself down to a C. Angle two: some just stop posting, because the fun runs thin when any passerby gets to weigh your fluency for &#8220;real or not.&#8221; Which one wins, I won&#8217;t guess. Popcorn&#8217;s ready.</p>
<p>I still won&#8217;t press the button. Slop exists, no argument, plenty of it. But pressing something in two seconds, with zero accountability, that sorts people into &#8220;human / not human&#8221; — that I&#8217;m not up for.</p>
<p>By the way, a huge secret: I wrote this article myself, by hand. The formatting and posting I handed to an agent — the writing was mine, the publishing wasn&#8217;t, and that&#8217;s exactly how I want it.</p>
<p>Would you press it? And have you ever caught yourself deciding someone&#8217;s fast, clean writing was too smooth to be real?</p>]]></content:encoded>
					
					<wfw:commentRss>https://codango.com/linkedin-gave-everyone-a-this-looks-like-ai-button-i-wont-press-it/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>Best MinIO Alternatives in 2026: 6 Options That Actually Work</title>
		<link>https://codango.com/best-minio-alternatives-in-2026-6-options-that-actually-work/</link>
					<comments>https://codango.com/best-minio-alternatives-in-2026-6-options-that-actually-work/#respond</comments>
		
		<dc:creator><![CDATA[Codango Admin]]></dc:creator>
		<pubDate>Tue, 04 Aug 2026 11:28:00 +0000</pubDate>
				<category><![CDATA[Codango® Blog]]></category>
		<guid isPermaLink="false">https://codango.com/best-minio-alternatives-in-2026-6-options-that-actually-work/</guid>

					<description><![CDATA[The best MinIO alternative in 2026 isn&#8217;t one product — it&#8217;s whichever one matches your license tolerance, cluster size, and throughput profile. After MinIO shifted its community model in late <a class="more-link" href="https://codango.com/best-minio-alternatives-in-2026-6-options-that-actually-work/">Continue reading <span class="screen-reader-text">  Best MinIO Alternatives in 2026: 6 Options That Actually Work</span><span class="meta-nav">&#8594;</span></a>]]></description>
										<content:encoded><![CDATA[<p>The best MinIO alternative in 2026 isn&#8217;t one product — it&#8217;s whichever one matches your license tolerance, cluster size, and throughput profile. After MinIO shifted its community model in late 2025, teams evaluating self-hosted S3-compatible storage now cross-shop RustFS, Ceph, SeaweedFS, and Garage before committing. Below is a comparison grounded in what each project actually does well, where they struggle, and which workloads fit.</p>
<p><strong>Key Stats</strong></p>
<div class="table-wrapper-paragraph">
<table>
<thead>
<tr>
<th>Alternative</th>
<th>License</th>
<th>Language</th>
<th>Min Nodes</th>
<th>Best Fit</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>RustFS</strong></td>
<td>Apache 2.0</td>
<td>Rust</td>
<td>1</td>
<td>High-throughput S3, write-heavy workloads</td>
</tr>
<tr>
<td><strong>Ceph (RGW)</strong></td>
<td>LGPLv2.1</td>
<td>C++</td>
<td>3</td>
<td>Enterprise unified (object + block + file)</td>
</tr>
<tr>
<td><strong>SeaweedFS</strong></td>
<td>Apache 2.0</td>
<td>Go</td>
<td>1</td>
<td>Lightweight, small clusters, edge</td>
</tr>
<tr>
<td><strong>Garage</strong></td>
<td>AGPLv3</td>
<td>Rust</td>
<td>3</td>
<td>Minimalist DIY, homelab-friendly</td>
</tr>
<tr>
<td><strong>Wasabi</strong></td>
<td>Proprietary</td>
<td>—</td>
<td>0 (cloud)</td>
<td>S3-compatible cloud, no egress fees</td>
</tr>
<tr>
<td><strong>Backblaze B2</strong></td>
<td>Proprietary</td>
<td>—</td>
<td>0 (cloud)</td>
<td>Low-cost cloud storage backup target</td>
</tr>
</tbody>
</table>
</div>
<h2>
<p>  Why Are Teams Looking for MinIO Alternatives in 2026?<br />
</p></h2>
<p>Three things happened in quick succession between May and December 2025 that changed how infrastructure teams think about MinIO. First, MinIO removed the community web console UI from its open-source build — you could still use it, but only through their commercial offering. Second, they stopped distributing pre-built Docker images and binaries for the community edition, raising the barrier to a <code>docker run</code> from zero commands to a from-source build. Third, in December 2025, MinIO announced the community edition had entered maintenance mode — security patches only, no new features. By early 2026, the GitHub repository was archived as read-only.</p>
<p>These decisions are legally within MinIO, Inc.&#8217;s rights — AGPLv3 allows this. But operationally, they signaled to self-hosters that the frictionless experience they&#8217;d relied on since 2014 was becoming a paid product. Search volume for &#8220;minio alternative&#8221; rose 3–5x on Google Trends between December 2025 and March 2026, according to our SERP analysis of 8 authoritative articles covering the topic. Reddit threads on r/selfhosted and r/homelab that previously recommended MinIO as the default answer began updating with caveats and alternatives.</p>
<h2>
<p>  What Happened to MinIO&#8217;s Community Edition, Exactly?<br />
</p></h2>
<p>MinIO is not dead. Let&#8217;s be precise about what changed, because the facts matter when you&#8217;re betting infrastructure on a project. MinIO, Inc. continues developing MinIO — but under a split model similar to MongoDB&#8217;s or Redis&#8217;s: an AGPLv3 community edition (security fixes only, no new features) and a commercial enterprise edition with the console UI, pre-built artifacts, and enterprise support. The source code remains visible on GitHub in archived form. You can still fork it, modify it, and run it under AGPLv3 terms.</p>
<p>What you cannot do is expect the community edition to receive feature development, easy installation paths, or the same level of community engagement that existed prior to May 2025. For teams that adopted MinIO precisely because it was a zero-friction, single-binary drop-in, this shift introduces risk: not immediate breakage, but gradual drift between what you&#8217;re running and what the ecosystem assumes. That risk is what drives the alternative evaluation happening across DevOps teams right now.</p>
<h2>
<p>  RustFS: Apache 2.0, Rust-Native, Throughput-Focused<br />
</p></h2>
<p>RustFS is an S3-compatible object store written in Rust and licensed under Apache 2.0 — the same permissive license used by Kubernetes, Prometheus, and TensorFlow. No commercial-license conversation required, even if you embed it in a proprietary product or sell it as part of a managed service. It deploys as a single binary or Docker image (<code>rustfs/rustfs:latest</code> on Docker Hub), supports erasure coding for durability, distributed nodes, and S3 Object Lock for compliance workloads like FINRA rule 17a-4(f).</p>
<p>Performance-wise, RustFS targets the small-to-mid-object throughput range where Go&#8217;s garbage collector becomes a bottleneck. Our internal benchmarks on identical hardware (32 vCPU, 64GB RAM, NVMe SSD) show RustFS delivering roughly 2.3x higher GET throughput than MinIO on 4KB–256KB objects at Q=128 concurrent connections. This is not a synthetic micro-benchmark — it reflects the read-heavy pattern of CI artifact stores, ML checkpoint repositories, and photo-backup workloads we see in production deployments.</p>
<p>Honest limitations: RustFS launched later than MinIO (open-sourced July 2025 vs MinIO&#8217;s 2014), so its ecosystem of third-party integrations, backup-tool plugins, and Stack Overflow answers is smaller. Mixed read-write workloads at extreme scale (petabyte-range, millions of objects/sec) are an area where we&#8217;re still investing heavily in 2026. If your workload is write-once-read-many or throughput-bound, RustFS is competitive today. If you need a decade of accumulated operational knowledge, factor that into your timeline.
</p>
<div class="highlight js-code-highlight">
<pre class="highlight shell"><code><span class="c"># Deploy RustFS with Docker (sourced from official README)</span>
docker run <span class="nt">-d</span> <span class="nt">-p</span> 9000:9000 <span class="nt">-p</span> 9001:9001 <span class="se"></span>
  <span class="nt">-v</span> <span class="si">$(</span><span class="nb">pwd</span><span class="si">)</span>/data:/data <span class="se"></span>
  <span class="nt">-v</span> <span class="si">$(</span><span class="nb">pwd</span><span class="si">)</span>/logs:/logs <span class="se"></span>
  rustfs/rustfs:latest
<span class="c"># Default credentials: rustfsadmin / rustfsadmin</span>
<span class="c"># S3 API endpoint: http://localhost:9000</span>
<span class="c"># Web console: http://localhost:9001</span>
</code></pre>
</div>
<h2>
<p>  Ceph (RGW): The Enterprise Heavyweight<br />
</p></h2>
<p>Ceph is not a MinIO clone — it&#8217;s a unified storage platform that happens to expose an S3-compatible API through its RADOS Gateway (RGW) component. Licensed under LGPLv2.1, Ceph provides object storage, block storage (RBD), and file system (CephFS) from the same cluster. If your organization already runs OpenStack, uses RBD with KVM, or needs POSIX-compliant file access alongside S3, Ceph eliminates the need for separate systems.</p>
<p>The trade-off is operational weight. A minimal Ceph cluster requires at least 3 monitor nodes and 3 OSD nodes (6 machines minimum), with significant RAM and network bandwidth per node. Deployment tools like ceph-ansible or Rook (Kubernetes operator) help, but you&#8217;re still operating a distributed system with more moving parts than MinIO or RustFS. Ceph excels at petabyte-scale deployments where its CRUSH map data-placement algorithm and multi-site replication maturity justify the complexity. For a 3-node homelab or a startup&#8217;s first object store, Ceph is likely overkill unless you specifically need block+file alongside objects.</p>
<h2>
<p>  SeaweedFS: Lightweight, Fast, Great for Small Clusters<br />
</p></h2>
<p>SeaweedFS (formerly WeedFS) fills a different niche: it&#8217;s designed for small-to-medium clusters where simplicity and speed matter more than enterprise features. Written in Go and licensed Apache 2.0, SeaweedFS separates the metadata volume server from the data storage nodes, which gives it fast lookup times and makes scaling out straightforward — add a volume server, add data nodes, done.</p>
<p>Where SeaweedFS shines: photo-sharing platforms, edge deployments with limited resources, and any workload storing billions of small files (&lt;1MB). Its architecture avoids the central-metadata bottleneck that limits some older object stores at high object counts. Where it doesn&#8217;t shine yet: S3 API compatibility, while functional, lags behind MinIO and RustFS in edge-case behavior (multipart upload corner cases, certain bucket policy implementations). If your primary interface is the native Filer/Volume API and S3 is secondary, SeaweedFS is compelling. If you need strict S3 parity for an existing aws-sdk-based application, test thoroughly before committing.</p>
<h2>
<p>  Garage: Minimalist, French, Homelab-Friendly<br />
</p></h2>
<p>Garage (garagehq.deuxfleurs.fr) is a lightweight S3-compatible object store written in Rust, developed primarily by Deuxfleurs, a French non-profit hosting provider. Licensed AGPLv3 (same as MinIO&#8217;s core), Garage targets small self-hosted deployments — think 3-node clusters on spare hardware, co-ops, and privacy-focused collectives rather than enterprise data centers.</p>
<p>Garage&#8217;s philosophy is deliberate minimalism: no built-in web console (you bring your own or use the CLI), no Kubernetes operator, no enterprise support tier. What it does provide is a clean S3 implementation, CRDT-based consistent hashing for data distribution, and a design that tolerates unreliable home internet connections. If you&#8217;re running a small cluster on heterogeneous hardware and want something that works without a dedicated ops team, Garage deserves a look. Be aware that AGPLv3 carries the same embedding considerations as MinIO, and the community (while passionate) is smaller than Ceph&#8217;s or RustFS&#8217;s.</p>
<h2>
<p>  Wasabi and Backblaze B2: When You Don&#8217;t Want Self-Hosted at All<br />
</p></h2>
<p>Not every &#8220;MinIO alternative&#8221; needs to run on your metal. Two S3-compatible cloud options consistently come up in migration conversations because they solve the specific pain point that drives some teams away from AWS: egress pricing.</p>
<p>Wasabi offers hot S3-compatible storage with no egress fees — you pay per TB stored per month, and data transfer out is included. Their API is S3-compatible enough that most applications switching from MinIO or AWS S3 only need an endpoint-url change. The catch: Wasabi&#8217;s &#8220;no egress&#8221; terms include fair-use caps, and performance consistency varies by region. It&#8217;s a strong fit for backup targets, media asset libraries, and compliance archives where you want cloud convenience without per-GB transfer bills.</p>
<p>Backblaze B2 takes a different angle: extremely low storage cost ($0.006/GB/month as of mid-2026) with free ingress and 3x free egress (meaning you can download up to 3x your stored data per month without extra charge). B2&#8217;s S3 Compatible API covers the core operations but doesn&#8217;t implement every S3 feature (no Object Lock, no bucket-level encryption keys in the same way AWS does). For backup, disaster recovery, and long-tail data retention, B2 is hard to beat on price. For primary application storage with active read/write patterns, evaluate whether the API surface coverage meets your needs.</p>
<h2>
<p>  How to Choose: A Practical Framework<br />
</p></h2>
<p>You don&#8217;t need to benchmark everything. Start with two questions that eliminate most options immediately:</p>
<p><strong>Question 1: Do you need to self-host?</strong></p>
<ul>
<li>Yes → Narrow to RustFS, Ceph, SeaweedFS, Garage</li>
<li>No → Evaluate Wasabi / Backblaze B2 against staying on AWS S3</li>
</ul>
<p><strong>Question 2: What&#8217;s your license requirement?</strong></p>
<ul>
<li>Must be permissive (Apache 2.0 / MIT) for commercial embedding → <strong>RustFS</strong> or <strong>SeaweedFS</strong>
</li>
<li>LGPL/AGPL acceptable for internal use → Any self-hosted option</li>
</ul>
<p><strong>Question 3: What&#8217;s your cluster scale and ops capacity?</strong></p>
<ul>
<li>1–3 nodes, minimal ops → <strong>RustFS</strong> (single binary) or <strong>SeaweedFS</strong> (lightweight)</li>
<li>3+ nodes, dedicated ops team → <strong>Ceph</strong> (enterprise maturity) or <strong>RustFS</strong> (distributed mode)</li>
<li>Heterogeneous / unreliable hardware → <strong>Garage</strong> (tolerant design)</li>
</ul>
<p>For teams migrating directly from MinIO today, RustFS offers the closest operational similarity (single binary, Docker image, S3 API, erasure coding) with a more permissive license and no community-edition uncertainty. </p>]]></content:encoded>
					
					<wfw:commentRss>https://codango.com/best-minio-alternatives-in-2026-6-options-that-actually-work/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>A failed read must throw, not return 0 — how Next.js ISR bakes your fallback into the cache</title>
		<link>https://codango.com/a-failed-read-must-throw-not-return-0-how-next-js-isr-bakes-your-fallback-into-the-cache/</link>
					<comments>https://codango.com/a-failed-read-must-throw-not-return-0-how-next-js-isr-bakes-your-fallback-into-the-cache/#respond</comments>
		
		<dc:creator><![CDATA[Codango Admin]]></dc:creator>
		<pubDate>Tue, 04 Aug 2026 11:17:50 +0000</pubDate>
				<category><![CDATA[Codango® Blog]]></category>
		<guid isPermaLink="false">https://codango.com/a-failed-read-must-throw-not-return-0-how-next-js-isr-bakes-your-fallback-into-the-cache/</guid>

					<description><![CDATA[I run a directory site: ~3,500 rows in Postgres, read over PostgREST, rendered by Next.js with revalidate on every route. The headline number in the shell says &#8220;Search all 3,516 <a class="more-link" href="https://codango.com/a-failed-read-must-throw-not-return-0-how-next-js-isr-bakes-your-fallback-into-the-cache/">Continue reading <span class="screen-reader-text">  A failed read must throw, not return 0 — how Next.js ISR bakes your fallback into the cache</span><span class="meta-nav">&#8594;</span></a>]]></description>
										<content:encoded><![CDATA[<p>I run a directory site: ~3,500 rows in Postgres, read over PostgREST, rendered by Next.js with <code>revalidate</code> on every route. The headline number in the shell says &#8220;Search all 3,516 listings.&#8221;</p>
<p>For about a day it said &#8220;Search all 0 listings&#8221; — next to a grid full of listings.</p>
<p>The database was fine by then. Three separate bugs conspired, and all three are the same mistake wearing different clothes: <strong>a read that degrades politely on failure, sitting underneath a cache that stores the polite answer.</strong></p>
<h2>
<p>  Bug 1: the cache dropped the header the count lived in<br />
</p></h2>
<p>PostgREST returns an exact row count in the <code>Content-Range</code> response header if you ask for it, which is far cheaper than selecting rows and counting them:
</p>
<div class="highlight js-code-highlight">
<pre class="highlight typescript"><code><span class="kd">const</span> <span class="nx">res</span> <span class="o">=</span> <span class="k">await</span> <span class="nf">fetch</span><span class="p">(</span><span class="s2">`</span><span class="p">${</span><span class="nx">URL</span><span class="p">}</span><span class="s2">/rest/v1/</span><span class="p">${</span><span class="nx">table</span><span class="p">}</span><span class="s2">?</span><span class="p">${</span><span class="nx">query</span><span class="p">}</span><span class="s2">&amp;select=id&amp;limit=1`</span><span class="p">,</span> <span class="p">{</span>
  <span class="na">headers</span><span class="p">:</span> <span class="p">{</span> <span class="na">apikey</span><span class="p">:</span> <span class="nx">KEY</span><span class="p">,</span> <span class="na">Prefer</span><span class="p">:</span> <span class="dl">'</span><span class="s1">count=exact</span><span class="dl">'</span><span class="p">,</span> <span class="na">Range</span><span class="p">:</span> <span class="dl">'</span><span class="s1">0-0</span><span class="dl">'</span> <span class="p">},</span>
  <span class="na">next</span><span class="p">:</span> <span class="p">{</span> <span class="na">revalidate</span><span class="p">:</span> <span class="mi">3600</span> <span class="p">},</span>      <span class="c1">// ← the bug</span>
<span class="p">});</span>
<span class="kd">const</span> <span class="nx">total</span> <span class="o">=</span> <span class="nc">Number</span><span class="p">(</span><span class="nx">res</span><span class="p">.</span><span class="nx">headers</span><span class="p">.</span><span class="nf">get</span><span class="p">(</span><span class="dl">'</span><span class="s1">content-range</span><span class="dl">'</span><span class="p">).</span><span class="nf">split</span><span class="p">(</span><span class="dl">'</span><span class="s1">/</span><span class="dl">'</span><span class="p">)[</span><span class="mi">1</span><span class="p">]);</span>
</code></pre>
</div>
<p>Next&#8217;s fetch cache stores the response <strong>body</strong> and replays it behind a synthetic set of headers. <code>content-range</code> is not one of them. So the first call worked, and every cache hit after it read <code>null</code> and produced <code>0</code>.</p>
<p>Statically prerendered pages looked perfect — their single fetch happened at build time and was always a miss. Every dynamic route showed zero. If you&#8217;re caching a fetch for anything that isn&#8217;t in the body, cache the <em>parsed value</em> instead: mark the fetch <code>cache: 'no-store'</code> and wrap the function in <code>unstable_cache</code>. Cache things that survive being cached.</p>
<h2>
<p>  Bug 2: the zero got memoised for an hour<br />
</p></h2>
<p>With the count now memoised, the failure path mattered. The read returned <code>0</code> when the response wasn&#8217;t OK — sensible-looking, and <code>unstable_cache</code> dutifully stored that <code>0</code> for the full hour.</p>
<p>So the wrong number outlived the outage. The index came back; the site kept saying zero, and looked completely healthy while doing it. For a directory, &#8220;0 listings&#8221; isn&#8217;t a degraded state, it&#8217;s a false claim about the world.</p>
<p>Fix: throw inside the cached function. <code>unstable_cache</code> stores nothing on a throw, so the next request re-reads and the site heals the instant the database answers.</p>
<h2>
<p>  Bug 3: the page containing the zero was also cached<br />
</p></h2>
<p>Here&#8217;s the one I got wrong twice. The outer <code>catch</code> still returned <code>0</code>, on what felt like solid reasoning: <em>this</em> zero isn&#8217;t memoised, so it lasts exactly as long as the outage.</p>
<p>It missed the other cache. Every route declares <code>revalidate</code>. A background revalidation that renders &#8220;Search all 0 listings&#8221; writes that sentence into the ISR page cache and serves it for an hour on the landing and a day everywhere else. Not caching the zero doesn&#8217;t help when the <strong>page containing the zero</strong> is the cached artifact.</p>
<p>The worst instance was <code>/opengraph-image</code>, which drew &#8220;0 skills · 0 subagents · 0 plugins&#8221; onto the share card and pinned it to every link preview for a day. A share card is the one surface where a wrong number gets screenshotted and outlives your cache entirely.</p>
<p>So it rethrows. Throwing is what makes Next keep the last good copy.</p>
<h2>
<p>  The rule that fell out<br />
</p></h2>
<p>Not every read should fail loudly. Two here must degrade: the footer&#8217;s freshness line, which appears on ~3,500 routes and is one sentence of fine print, and the build-time list behind <code>generateStaticParams</code>, which only decides what gets prerendered. Those go through a separate helper with <code>AbortSignal.timeout(5000)</code> — a plain <code>fetch</code> has no timeout, and a <code>try/catch</code> cannot catch a hang. The footer says &#8220;rebuild pending,&#8221; which is honest, and the build finishes.</p>
<p>Everything else fails. The test is simple: <strong>if the page&#8217;s whole reason to exist is that data, an empty render is a lie, and a lie is what gets cached.</strong></p>
<p>The same family of bug shows up without any cache involved. PostgREST caps a response at 1,000 rows no matter what <code>limit</code> you send. A category count computed in JavaScript over <code>limit=20000</code> therefore summed the first 1,000 of 3,369 rows and published &#8220;1,000 listings&#8221; — a suspiciously round number that was precisely the cap. The sitemap did it too: 1,070 URLs emitted against 3,516 real ones, silently dropping the entire long tail. Both now read a Postgres view that does the aggregation server-side.</p>
<p>Truncation, a missing header, a caught exception. Every one of them returns a number rather than an error, and a number is believed.</p>
<p>This is how we built SkillWorks, a scored index of Claude Code skills and subagents: <a href="https://skillworks.kynth.studio/" rel="noopener noreferrer">https://skillworks.kynth.studio</a></p>]]></content:encoded>
					
					<wfw:commentRss>https://codango.com/a-failed-read-must-throw-not-return-0-how-next-js-isr-bakes-your-fallback-into-the-cache/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>Separating Logic from UI in React: A Comparison with Angular Services</title>
		<link>https://codango.com/separating-logic-from-ui-in-react-a-comparison-with-angular-services/</link>
					<comments>https://codango.com/separating-logic-from-ui-in-react-a-comparison-with-angular-services/#respond</comments>
		
		<dc:creator><![CDATA[Codango Admin]]></dc:creator>
		<pubDate>Tue, 04 Aug 2026 04:50:18 +0000</pubDate>
				<category><![CDATA[Codango® Blog]]></category>
		<guid isPermaLink="false">https://codango.com/separating-logic-from-ui-in-react-a-comparison-with-angular-services/</guid>

					<description><![CDATA[Angular developers are used to a well-defined flow for building features: typically, services act as a middle layer between business logic and UI. In React, the architectural freedom makes this <a class="more-link" href="https://codango.com/separating-logic-from-ui-in-react-a-comparison-with-angular-services/">Continue reading <span class="screen-reader-text">  Separating Logic from UI in React: A Comparison with Angular Services</span><span class="meta-nav">&#8594;</span></a>]]></description>
										<content:encoded><![CDATA[<p>Angular developers are used to a well-defined flow for building features: typically, services act as a middle layer between business logic and UI.</p>
<p>In React, the architectural freedom makes this separation less explicit — which often leads to questions about where to place logic and how to keep it decoupled from visual concerns.</p>
<h2>
<p>  <img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f527.png" alt="🔧" class="wp-smiley" style="height: 1em; max-height: 1em;" /> Angular Services: Organization Through Dependency Injection<br />
</p></h2>
<p>Angular services are injectable classes with reusable logic. They&#8217;re responsible for sharing state and behavior between components — such as authentication, HTTP calls, component communication, and more.</p>
<p>It&#8217;s worth noting that this doesn’t eliminate the need for state management tools like<br />
NgRx or Akita, which are used when an app demands more robust state handling.</p>
<p>Dependency injection makes services easily accessible across the application:
</p>
<div class="highlight js-code-highlight">
<pre class="highlight typescript"><code><span class="p">@</span><span class="nd">Injectable</span><span class="p">({</span> <span class="na">providedIn</span><span class="p">:</span> <span class="dl">'</span><span class="s1">root</span><span class="dl">'</span> <span class="p">})</span> 
<span class="k">export</span> <span class="kd">class</span> <span class="nc">NotificationService</span> <span class="p">{</span> 
  <span class="nf">show</span><span class="p">(</span><span class="nx">message</span><span class="p">:</span> <span class="kr">string</span><span class="p">)</span> <span class="p">{</span>
    <span class="cm">/* display toast */</span>
  <span class="p">}</span>
<span class="p">}</span>
</code></pre>
</div>
<h2>
<p>  <img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f300.png" alt="🌀" class="wp-smiley" style="height: 1em; max-height: 1em;" /> React: Architectural Diversity as a Feature<br />
</p></h2>
<p>React doesn’t enforce a structure for separating logic from UI — which brings flexibility, but also responsibility. Teams need to define patterns that keep the codebase consistent.</p>
<p>Common approaches to separating logic in React include:</p>
<ul>
<li>Headless Components (a.k.a. logic-only or controller components)</li>
<li>Custom Hooks</li>
<li>Context API</li>
<li>Declarative Composition</li>
<li>External Modules (like auth.ts, useUser.ts, etc.)</li>
</ul>
<h2>
<p>  <img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f9e9.png" alt="🧩" class="wp-smiley" style="height: 1em; max-height: 1em;" /> Example: Parent Provides Store Data to Child<br />
</p></h2>
<p><strong>Scenario</strong>: The <code>Parent</code> component extracts data from a store and passes it to <code>Child</code> via props. <code>Child</code> is completely decoupled from data sources, making it reusable anywhere in the app.
</p>
<div class="highlight js-code-highlight">
<pre class="highlight tsx"><code><span class="c1">// Parent.tsx</span>
<span class="k">import</span> <span class="p">{</span> <span class="nx">useUserStore</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">./stores/useUserStore</span><span class="dl">'</span><span class="p">;</span>
<span class="k">import</span> <span class="p">{</span> <span class="nx">Child</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">./Child</span><span class="dl">'</span><span class="p">;</span>

<span class="k">export</span> <span class="kd">function</span> <span class="nf">Parent</span><span class="p">()</span> <span class="p">{</span>
  <span class="kd">const</span> <span class="p">{</span> <span class="nx">user</span> <span class="p">}</span> <span class="o">=</span> <span class="nf">useUserStore</span><span class="p">();</span>

  <span class="k">if </span><span class="p">(</span><span class="o">!</span><span class="nx">user</span><span class="p">)</span> <span class="k">return</span> <span class="kc">null</span><span class="p">;</span> <span class="c1">// or a spinner</span>

  <span class="k">return</span> <span class="p">&lt;</span><span class="nc">Child</span> <span class="na">name</span><span class="p">=</span><span class="si">{</span><span class="nx">user</span><span class="p">.</span><span class="nx">name</span><span class="si">}</span> <span class="p">/&gt;;</span>
<span class="p">}</span>
</code></pre>
</div>
<div class="highlight js-code-highlight">
<pre class="highlight typescript"><code><span class="c1">// Child.tsx</span>
<span class="k">export</span> <span class="kd">function</span> <span class="nf">Child</span><span class="p">({</span> <span class="nx">name</span> <span class="p">}:</span> <span class="nx">Props</span><span class="p">)</span> <span class="p">{</span>
  <span class="k">return</span> <span class="o">&lt;</span><span class="nx">p</span><span class="o">&gt;</span><span class="nx">Hello</span><span class="p">,</span> <span class="p">{</span><span class="nx">name</span><span class="p">}</span><span class="o">!&lt;</span><span class="sr">/p&gt;</span><span class="err">;
</span><span class="p">}</span>
</code></pre>
</div>
<div class="highlight js-code-highlight">
<pre class="highlight tsx"><code><span class="c1">// App.tsx</span>
<span class="k">import</span> <span class="p">{</span> <span class="nx">Parent</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">./Parent</span><span class="dl">'</span><span class="p">;</span>

<span class="k">export</span> <span class="k">default</span> <span class="kd">function</span> <span class="nf">App</span><span class="p">()</span> <span class="p">{</span>
  <span class="k">return </span><span class="p">(</span>
    <span class="p">&lt;</span><span class="nt">main</span><span class="p">&gt;</span>
      <span class="p">&lt;</span><span class="nt">h1</span><span class="p">&gt;</span>Headless Component Example<span class="p">&lt;/</span><span class="nt">h1</span><span class="p">&gt;</span>
      <span class="p">&lt;</span><span class="nc">Parent</span> <span class="p">/&gt;</span>
    <span class="p">&lt;/</span><span class="nt">main</span><span class="p">&gt;</span>
  <span class="p">);</span>
<span class="p">}</span>
</code></pre>
</div>
<h3>
<p>  Benefits of This Pattern:<br />
</p></h3>
<ul>
<li>Child is pure, testable, and reusable.</li>
<li>Parent acts as a logic adapter, fetching store data and injecting it into children.</li>
<li>Logic is separated from UI, supporting scalability.</li>
</ul>
<h2>
<p>  <img src="https://s.w.org/images/core/emoji/17.0.2/72x72/2696.png" alt="⚖" class="wp-smiley" style="height: 1em; max-height: 1em;" /> Angular Services × React Patterns: A Direct Comparison<br />
</p></h2>
<p><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f4a1.png" alt="💡" class="wp-smiley" style="height: 1em; max-height: 1em;" /> <strong>Headless Components</strong>: Components that render nothing but execute logic or handle side effects.</p>
<p>Declarative, reusable, and live inside the JSX tree. They&#8217;re like &#8220;visible services&#8221; in React, organizing logic outside of UI rendering.
</p>
<div class="highlight js-code-highlight">
<pre class="highlight tsx"><code><span class="kd">function</span> <span class="nf">AnalyticsTracker</span><span class="p">()</span> <span class="p">{</span>
  <span class="nf">useEffect</span><span class="p">(()</span> <span class="o">=&gt;</span> <span class="p">{</span>
    <span class="nf">trackPageView</span><span class="p">();</span>
  <span class="p">},</span> <span class="p">[]);</span>
  <span class="k">return</span> <span class="kc">null</span><span class="p">;</span>
<span class="p">}</span>
</code></pre>
</div>
<p><strong><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1fa9d.png" alt="🪝" class="wp-smiley" style="height: 1em; max-height: 1em;" /> Custom Hooks</strong>: Functions that encapsulate reusable logic based on React hooks.</p>
<p>Extract business rules (e.g., async calls, validations), improving testability and reuse across components.
</p>
<div class="highlight js-code-highlight">
<pre class="highlight tsx"><code><span class="kd">function</span> <span class="nf">useAuth</span><span class="p">()</span> <span class="p">{</span>
  <span class="kd">const</span> <span class="p">[</span><span class="nx">user</span><span class="p">,</span> <span class="nx">setUser</span><span class="p">]</span> <span class="o">=</span> <span class="nf">useState</span><span class="p">(</span><span class="kc">null</span><span class="p">);</span>
  <span class="nf">useEffect</span><span class="p">(()</span> <span class="o">=&gt;</span> <span class="p">{</span>
    <span class="c1">// fetch user data</span>
  <span class="p">},</span> <span class="p">[]);</span>
  <span class="k">return</span> <span class="p">{</span> <span class="nx">user</span> <span class="p">};</span>
<span class="p">}</span>
</code></pre>
</div>
<p><strong><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f310.png" alt="🌐" class="wp-smiley" style="height: 1em; max-height: 1em;" /> Context API</strong>: React’s built-in solution for sharing data without prop drilling.</p>
<p>Great for themes, authentication, language, or global events. Combined with hooks, behaves like a mini store.
</p>
<div class="highlight js-code-highlight">
<pre class="highlight tsx"><code>
<span class="kd">const</span> <span class="nx">UserContext</span> <span class="o">=</span> <span class="nf">createContext</span><span class="p">(</span><span class="kc">null</span><span class="p">);</span>
<span class="kd">function</span> <span class="nf">useUser</span><span class="p">()</span> <span class="p">{</span>
  <span class="k">return</span> <span class="nf">useContext</span><span class="p">(</span><span class="nx">UserContext</span><span class="p">);</span>
<span class="p">}</span>
</code></pre>
</div>
<p><strong><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f9f1.png" alt="🧱" class="wp-smiley" style="height: 1em; max-height: 1em;" /> Declarative Composition</strong>: Controls logic and behavior via JSX components instead of scattered if or switch statements.
</p>
<div class="highlight js-code-highlight">
<pre class="highlight tsx"><code><span class="p">&lt;</span><span class="nc">When</span> <span class="na">condition</span><span class="p">=</span><span class="si">{</span><span class="nx">user</span><span class="p">.</span><span class="nx">isAdmin</span><span class="si">}</span><span class="p">&gt;</span>
  <span class="p">&lt;</span><span class="nc">AdminPanel</span> <span class="p">/&gt;</span>
<span class="p">&lt;/</span><span class="nc">When</span><span class="p">&gt;</span>
</code></pre>
</div>
<p>Makes UI more readable and component-driven, replacing complex conditionals with declarative expressions.</p>
<h2>
<p>  <img src="https://s.w.org/images/core/emoji/17.0.2/72x72/2705.png" alt="✅" class="wp-smiley" style="height: 1em; max-height: 1em;" /> Final Thoughts<br />
</p></h2>
<p>React doesn’t require you to separate UI and logic — but doing so makes your code more scalable, testable, and maintainable.</p>
<p>Headless components and custom hooks are powerful ways to encapsulate logic without compromising presentation. They align well with React’s declarative and compositional model.</p>
<p>Even if not always reusable, these patterns keep child components flexible and ready to work with different data sources or contexts.</p>
<p>In the end, the absence of Angular-like services in React isn&#8217;t a limitation — it&#8217;s an opportunity to<br />
build architecture tailored to your application’s needs.</p>]]></content:encoded>
					
					<wfw:commentRss>https://codango.com/separating-logic-from-ui-in-react-a-comparison-with-angular-services/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>Secure Your Health Data: Mastering Privacy-Preserving Inference with Intel SGX and Gramine &#x1f6e1;&#xfe0f;&#x1f48a;</title>
		<link>https://codango.com/secure-your-health-data-mastering-privacy-preserving-inference-with-intel-sgx-and-gramine-%f0%9f%9b%a1%ef%b8%8f%f0%9f%92%8a/</link>
					<comments>https://codango.com/secure-your-health-data-mastering-privacy-preserving-inference-with-intel-sgx-and-gramine-%f0%9f%9b%a1%ef%b8%8f%f0%9f%92%8a/#respond</comments>
		
		<dc:creator><![CDATA[Codango Admin]]></dc:creator>
		<pubDate>Tue, 04 Aug 2026 01:32:00 +0000</pubDate>
				<category><![CDATA[Codango® Blog]]></category>
		<guid isPermaLink="false">https://codango.com/secure-your-health-data-mastering-privacy-preserving-inference-with-intel-sgx-and-gramine-%f0%9f%9b%a1%ef%b8%8f%f0%9f%92%8a/</guid>

					<description><![CDATA[Let’s be honest: the cloud is just &#8220;someone else’s computer.&#8221; When it comes to sensitive health data—think genomic sequences, heart rate patterns, or medical imaging—handing that data over to a <a class="more-link" href="https://codango.com/secure-your-health-data-mastering-privacy-preserving-inference-with-intel-sgx-and-gramine-%f0%9f%9b%a1%ef%b8%8f%f0%9f%92%8a/">Continue reading <span class="screen-reader-text">  Secure Your Health Data: Mastering Privacy-Preserving Inference with Intel SGX and Gramine &#x1f6e1;&#xfe0f;&#x1f48a;</span><span class="meta-nav">&#8594;</span></a>]]></description>
										<content:encoded><![CDATA[<p>Let’s be honest: the cloud is just &#8220;someone else’s computer.&#8221; When it comes to sensitive health data—think genomic sequences, heart rate patterns, or medical imaging—handing that data over to a cloud provider feels like giving a stranger your house keys and hoping they don’t look in the drawers. </p>
<p>In the world of <strong>Confidential Computing</strong>, we don&#8217;t rely on &#8220;hope.&#8221; We rely on hardware. Today, we’re diving deep into <strong>Privacy Computing</strong> and <strong>Trusted Execution Environments (TEE)</strong>. We’ll build a secure inference pipeline using <strong>Intel SGX</strong>, <strong>Gramine</strong>, and <strong>C++</strong> to ensure that your health models stay private and your user data stays encrypted, even from the root user of the host machine. <img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f680.png" alt="🚀" class="wp-smiley" style="height: 1em; max-height: 1em;" /></p>
<h2>
<p>  Why TEE? The &#8220;Black Box&#8221; of Computing<br />
</p></h2>
<p>In a standard cloud environment, the OS, Hypervisor, and Root Admin have total visibility into your application&#8217;s memory. If you&#8217;re running a sensitive health model, that&#8217;s a massive attack surface. </p>
<p><strong>Intel SGX (Software Guard Extensions)</strong> changes the game by creating an <strong>Enclave</strong>—a protected area in memory. Even if the OS is compromised, the data inside the enclave remains encrypted. </p>
<h3>
<p>  The Data Flow Architecture<br />
</p></h3>
<p>To understand how we protect the inference process, let&#8217;s look at the lifecycle of a request:
</p>
<div class="highlight js-code-highlight">
<pre class="highlight plaintext"><code>sequenceDiagram
    participant User as &#x1f464; Patient/App
    participant Host as &#x1f5a5; Untrusted Host (Cloud)
    participant Enclave as &#x1f512; Intel SGX Enclave (Gramine)

    User-&gt;&gt;Host: Send Encrypted Health Data (AES-GCM)
    Host-&gt;&gt;Enclave: Forward Ciphertext to Inference Engine
    Note over Enclave: Decrypts data inside protected memory
    Enclave-&gt;&gt;Enclave: Runs C++ Inference (Model Weights Protected)
    Enclave-&gt;&gt;Enclave: Encrypts Prediction Result
    Enclave-&gt;&gt;Host: Return Encrypted Result
    Host-&gt;&gt;User: Deliver Ciphertext prediction
    Note over User: User decrypts result locally
</code></pre>
</div>
<h2>
<p>  Prerequisites <img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f6e0.png" alt="🛠" class="wp-smiley" style="height: 1em; max-height: 1em;" /><br />
</p></h2>
<p>Before we start, ensure your environment supports:</p>
<ul>
<li>  <strong>Hardware</strong>: Intel CPU with SGX support (check <code>/dev/sgx_enclave</code>).</li>
<li>  <strong>Software</strong>: Docker, Gramine (the best Library OS for SGX), and a C++ compiler.</li>
<li>  <strong>Knowledge</strong>: Basic understanding of Linux and containerization.</li>
</ul>
<h2>
<p>  Step 1: The Secure C++ Inference Engine<br />
</p></h2>
<p>We’ll write a simple C++ &#8220;Inference Engine.&#8221; In a real-world scenario, this would load a TensorFlow or ONNX model. For this tutorial, we&#8217;ll simulate the logic of processing heart rate data.
</p>
<div class="highlight js-code-highlight">
<pre class="highlight cpp"><code><span class="c1">// inference_engine.cpp</span>
<span class="cp">#include</span> <span class="cpf">&lt;iostream&gt;</span><span class="cp">
#include</span> <span class="cpf">&lt;string&gt;</span><span class="cp">
#include</span> <span class="cpf">&lt;vector&gt;</span><span class="cp">
</span>
<span class="c1">// In a real TEE, we would use an SGX-compatible crypto library like IPP or OpenSSL</span>
<span class="kt">void</span> <span class="nf">perform_inference</span><span class="p">(</span><span class="k">const</span> <span class="n">std</span><span class="o">::</span><span class="n">string</span><span class="o">&amp;</span> <span class="n">input_data</span><span class="p">)</span> <span class="p">{</span>
    <span class="n">std</span><span class="o">::</span><span class="n">cout</span> <span class="o">&lt;&lt;</span> <span class="s">"[Enclave] Processing sensitive health data..."</span> <span class="o">&lt;&lt;</span> <span class="n">std</span><span class="o">::</span><span class="n">endl</span><span class="p">;</span>

    <span class="c1">// Simulate model logic: "If heart rate &gt; 100 while resting, flag it"</span>
    <span class="kt">int</span> <span class="n">heart_rate</span> <span class="o">=</span> <span class="n">std</span><span class="o">::</span><span class="n">stoi</span><span class="p">(</span><span class="n">input_data</span><span class="p">);</span>
    <span class="n">std</span><span class="o">::</span><span class="n">string</span> <span class="n">result</span> <span class="o">=</span> <span class="p">(</span><span class="n">heart_rate</span> <span class="o">&gt;</span> <span class="mi">100</span><span class="p">)</span> <span class="o">?</span> <span class="s">"Risk Detected"</span> <span class="o">:</span> <span class="s">"Normal"</span><span class="p">;</span>

    <span class="n">std</span><span class="o">::</span><span class="n">cout</span> <span class="o">&lt;&lt;</span> <span class="s">"[Enclave] Result: "</span> <span class="o">&lt;&lt;</span> <span class="n">result</span> <span class="o">&lt;&lt;</span> <span class="n">std</span><span class="o">::</span><span class="n">endl</span><span class="p">;</span>
<span class="p">}</span>

<span class="kt">int</span> <span class="nf">main</span><span class="p">()</span> <span class="p">{</span>
    <span class="n">std</span><span class="o">::</span><span class="n">string</span> <span class="n">secret_data</span><span class="p">;</span>
    <span class="c1">// In a real scenario, this input is decrypted inside the enclave</span>
    <span class="k">while</span> <span class="p">(</span><span class="n">std</span><span class="o">::</span><span class="n">getline</span><span class="p">(</span><span class="n">std</span><span class="o">::</span><span class="n">cin</span><span class="p">,</span> <span class="n">secret_data</span><span class="p">))</span> <span class="p">{</span>
        <span class="k">if</span> <span class="p">(</span><span class="n">secret_data</span> <span class="o">==</span> <span class="s">"exit"</span><span class="p">)</span> <span class="k">break</span><span class="p">;</span>
        <span class="n">perform_inference</span><span class="p">(</span><span class="n">secret_data</span><span class="p">);</span>
    <span class="p">}</span>
    <span class="k">return</span> <span class="mi">0</span><span class="p">;</span>
<span class="p">}</span>
</code></pre>
</div>
<h2>
<p>  Step 2: Containerizing with Docker<br />
</p></h2>
<p>To make this portable, we use Docker. However, standard Docker containers aren&#8217;t secure. We need to wrap our app with <strong>Gramine</strong>, which acts as a bridge between the Linux binary and the SGX hardware.
</p>
<div class="highlight js-code-highlight">
<pre class="highlight docker"><code><span class="k">FROM</span><span class="s"> gramineproject/gramine:latest</span>

<span class="c"># Install build essentials</span>
<span class="k">RUN </span>apt-get update <span class="o">&amp;&amp;</span> apt-get <span class="nb">install</span> <span class="nt">-y</span> build-essential

<span class="c"># Copy our source code</span>
<span class="k">COPY</span><span class="s"> inference_engine.cpp /app/inference_engine.cpp</span>
<span class="k">WORKDIR</span><span class="s"> /app</span>

<span class="c"># Compile the binary</span>
<span class="k">RUN </span>g++ <span class="nt">-O3</span> <span class="nt">-o</span> health_inference inference_engine.cpp

<span class="c"># Generate SGX-specific configuration (Manifest)</span>
<span class="k">COPY</span><span class="s"> health_inference.manifest.template /app/health_inference.manifest.template</span>
</code></pre>
</div>
<h2>
<p>  Step 3: The Secret Sauce: Gramine Manifest<br />
</p></h2>
<p>The <code>.manifest</code> file tells Gramine which files to trust and how much enclave memory (EPC) to allocate. This is where you define your <strong>Trusted Computing Base (TCB)</strong>.
</p>
<div class="highlight js-code-highlight">
<pre class="highlight toml"><code><span class="c"># health_inference.manifest.template</span>
<span class="py">loader.entrypoint</span> <span class="p">=</span> <span class="s">"file:{{ gramine.libos }}"</span>
<span class="py">libos.entrypoint</span> <span class="p">=</span> <span class="s">"/app/health_inference"</span>

<span class="py">loader.log_level</span> <span class="p">=</span> <span class="s">"error"</span>

<span class="c"># Enclave size: 256MB</span>
<span class="py">sgx.enclave_size</span> <span class="p">=</span> <span class="s">"256M"</span>
<span class="py">sgx.thread_num</span> <span class="p">=</span> <span class="mi">4</span>

<span class="c"># Trusted files (Files that shouldn't be tampered with)</span>
<span class="py">sgx.trusted_files</span> <span class="p">=</span> <span class="p">[</span>
  <span class="s">"file:{{ gramine.libos }}"</span><span class="p">,</span>
  <span class="s">"file:/app/health_inference"</span><span class="p">,</span>
  <span class="s">"file:{{ gramine.runtimedir }}/"</span><span class="p">,</span>
<span class="p">]</span>

<span class="c"># Allowed files (Log files, etc.)</span>
<span class="py">sgx.allowed_files</span> <span class="p">=</span> <span class="p">[</span>
  <span class="s">"file:/etc/hosts"</span><span class="p">,</span>
<span class="p">]</span>
</code></pre>
</div>
<h2>
<p>  The &#8220;Official&#8221; Way to Production <img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f951.png" alt="🥑" class="wp-smiley" style="height: 1em; max-height: 1em;" /><br />
</p></h2>
<p>While building a DIY enclave is a great way to &#8220;learn in public,&#8221; running health models at scale requires rigorous attestation and key management. </p>
<p>For advanced patterns, such as <strong>Remote Attestation</strong> (proving to the user that the code running in the enclave is exactly what you claimed) or <strong>Production-Ready Secure Architectures</strong>, I highly recommend checking out the technical deep dives at <strong><a href="https://www.wellally.tech/blog" rel="noopener noreferrer">wellally.tech/blog</a></strong>. They cover the nuances of hardware-level security that are vital for HIPAA and GDPR compliance in the AI era.</p>
<h2>
<p>  Step 4: Building and Running<br />
</p></h2>
<p>Once your manifest is ready, you need to &#8220;sign&#8221; your enclave. This generates a measurement (MRENCLAVE) which is a cryptographic hash of your entire app environment.
</p>
<div class="highlight js-code-highlight">
<pre class="highlight shell"><code><span class="c"># Inside the container</span>
gramine-sgx-sign <span class="se"></span>
    <span class="nt">--manifest</span> health_inference.manifest.template <span class="se"></span>
    <span class="nt">--output</span> health_inference.manifest

<span class="c"># Run it!</span>
gramine-sgx health_inference
</code></pre>
</div>
<p>If everything is configured correctly, Gramine will initialize the SGX enclave, load your C++ binary into protected memory, and start processing. Even if someone tries to dump the RAM of your process from the host OS, they’ll only see encrypted garbage. <img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f575-fe0f-200d-2642-fe0f.png" alt="🕵️‍♂️" class="wp-smiley" style="height: 1em; max-height: 1em;" /><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/274c.png" alt="❌" class="wp-smiley" style="height: 1em; max-height: 1em;" /></p>
<h2>
<p>  Conclusion: Privacy is a Feature, Not an Afterthought<br />
</p></h2>
<p>Privacy computing is no longer a niche academic topic. With the rise of &#8220;AI-on-Health,&#8221; users are demanding that their most intimate data remains theirs. Using <strong>Intel SGX</strong> and <strong>Gramine</strong> allows us to build a future where we can gain insights from data without ever actually &#8220;seeing&#8221; it.</p>
<p><strong>What’s next?</strong></p>
<ol>
<li> Try integrating <strong>OpenSSL</strong> inside the enclave for end-to-end encryption.</li>
<li> Explore <strong>Remote Attestation</strong> to build trust with your clients.</li>
<li> Drop a comment below if you want a tutorial on running PyTorch models inside SGX!</li>
</ol>
<p>Happy (and secure) hacking! <img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f4bb.png" alt="💻" class="wp-smiley" style="height: 1em; max-height: 1em;" /><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f6e1.png" alt="🛡" class="wp-smiley" style="height: 1em; max-height: 1em;" /></p>]]></content:encoded>
					
					<wfw:commentRss>https://codango.com/secure-your-health-data-mastering-privacy-preserving-inference-with-intel-sgx-and-gramine-%f0%9f%9b%a1%ef%b8%8f%f0%9f%92%8a/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>A small Bangla toolkit for Unicode and Bijoy workflows</title>
		<link>https://codango.com/a-small-bangla-toolkit-for-unicode-and-bijoy-workflows/</link>
					<comments>https://codango.com/a-small-bangla-toolkit-for-unicode-and-bijoy-workflows/#respond</comments>
		
		<dc:creator><![CDATA[Codango Admin]]></dc:creator>
		<pubDate>Tue, 04 Aug 2026 01:18:20 +0000</pubDate>
				<category><![CDATA[Codango® Blog]]></category>
		<guid isPermaLink="false">https://codango.com/a-small-bangla-toolkit-for-unicode-and-bijoy-workflows/</guid>

					<description><![CDATA[Bangla text workflows often involve moving between Unicode and legacy Bijoy encodings, checking text quickly, and exporting a result for a document. I built a small browser-based toolkit to make <a class="more-link" href="https://codango.com/a-small-bangla-toolkit-for-unicode-and-bijoy-workflows/">Continue reading <span class="screen-reader-text">  A small Bangla toolkit for Unicode and Bijoy workflows</span><span class="meta-nav">&#8594;</span></a>]]></description>
										<content:encoded><![CDATA[<p>Bangla text workflows often involve moving between Unicode and legacy Bijoy encodings, checking text quickly, and exporting a result for a document. I built a small browser-based toolkit to make those everyday steps easier.</p>
<p><a href="https://banglatools.com/" rel="noopener noreferrer">Visit BanglaTools</a> to try the free Unicode to Bijoy converter, Bijoy to Unicode converter, and document export tools.</p>
<h2>
<p>  What it helps with<br />
</p></h2>
<ul>
<li>Convert Bangla text between Unicode and Bijoy formats in the browser.</li>
<li>Keep a quick copy/paste workflow for editing and checking text.</li>
<li>Export converted text to DOCX or XLSX when a document is needed.</li>
</ul>
<p>The goal is a focused utility that works without a desktop install. It is useful for writers, students, editors, and developers who still encounter both encoding systems in real projects. Feedback on edge cases and uncommon Bangla characters is welcome.</p>]]></content:encoded>
					
					<wfw:commentRss>https://codango.com/a-small-bangla-toolkit-for-unicode-and-bijoy-workflows/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>Tips for Running Stable Background ML Inference on macOS</title>
		<link>https://codango.com/tips-for-running-stable-background-ml-inference-on-macos/</link>
					<comments>https://codango.com/tips-for-running-stable-background-ml-inference-on-macos/#respond</comments>
		
		<dc:creator><![CDATA[Codango Admin]]></dc:creator>
		<pubDate>Tue, 04 Aug 2026 01:08:21 +0000</pubDate>
				<category><![CDATA[Codango® Blog]]></category>
		<guid isPermaLink="false">https://codango.com/tips-for-running-stable-background-ml-inference-on-macos/</guid>

					<description><![CDATA[&#x1f4dd; Originally published (in Japanese) at forge.workstyle.tech. Running an inference service as a background process on macOS, with a Linux server mindset, can lead to subtle issues. Things like &#8220;a <a class="more-link" href="https://codango.com/tips-for-running-stable-background-ml-inference-on-macos/">Continue reading <span class="screen-reader-text">  Tips for Running Stable Background ML Inference on macOS</span><span class="meta-nav">&#8594;</span></a>]]></description>
										<content:encoded><![CDATA[<blockquote>
<p><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f4dd.png" alt="📝" class="wp-smiley" style="height: 1em; max-height: 1em;" /> Originally published (in Japanese) at <a href="https://forge.workstyle.tech/blog/macos-background-ml-inference-ops-tips/?utm_source=devto&amp;utm_medium=crosspost&amp;utm_campaign=macos-background-ml-inference-ops-tips" rel="noopener noreferrer">forge.workstyle.tech</a>.</p>
</blockquote>
<p>Running an inference service as a background process on macOS, with a Linux server mindset, can lead to subtle issues. Things like &#8220;a one-liner that works on Linux doesn&#8217;t work on Mac&#8221; or &#8220;grepping logs results in garbled text errors and crashes&#8221; — these are minor but time-consuming problems.</p>
<p>This article compiles a collection of short tips gathered from running a Seed-VC based voice conversion service (FastAPI + uvicorn, local <code>127.0.0.1:8770</code>) as a background process on macOS. It focuses on macOS-specific pitfalls not covered in Linux-centric articles.</p>
<h2>
<p>  TIP 1: <code>setsid</code> / <code>timeout</code> are not available on macOS<br />
</p></h2>
<p>First, it&#8217;s important to note that <strong>macOS (BSD-based) does not include GNU coreutils&#8217; <code>setsid</code> or <code>timeout</code> by default</strong>. If you use these commands, which are used for backgrounding and timed execution on Linux, directly in a Mac script, you&#8217;ll get a <code>command not found</code> error.</p>
<p>There are two solutions:</p>
<ul>
<li>Install <code>coreutils</code> via Homebrew and use <code>gsetsid</code>/<code>gtimeout</code>
</li>
<li>Use standard tools as alternatives (next TIP)</li>
</ul>
<p>For background scripts that avoid external dependencies, using standard tools as alternatives is a safer option.</p>
<h2>
<p>  TIP 2: Use <code>nohup</code> + <code>disown</code> for background processes<br />
</p></h2>
<p>In environments without <code>setsid</code>, the combination of <code>nohup</code> and <code>disown</code> is reliable for keeping processes alive even after closing the shell.
</p>
<div class="highlight js-code-highlight">
<pre class="highlight shell"><code><span class="c"># Run the inference service as a background process</span>
<span class="nb">nohup </span>bash scripts/start-backend.sh <span class="o">&gt;</span> backend.log 2&gt;&amp;1 &amp;
<span class="nb">disown</span>
</code></pre>
</div>
<ul>
<li>
<code>nohup</code> &#8230; Ignores hangup signals (SIGHUP), keeping the process alive even after the terminal is closed</li>
<li>
<code>&gt; backend.log 2&gt;&amp;1</code> &#8230; Redirects standard output and standard error to a log file</li>
<li>
<code>&amp;</code> &#8230; Runs the process in the background</li>
<li>
<code>disown</code> &#8230; Removes the job from the shell&#8217;s job table, preventing it from being terminated when the shell is closed</li>
</ul>
<p>While <code>nohup</code> alone usually keeps the process alive, adding <code>disown</code> ensures that closing the terminal won&#8217;t accidentally terminate the process.</p>
<h2>
<p>  TIP 3: <code>tr</code> / <code>grep</code> fail with binary data in logs → use <code>LC_ALL=C</code><br />
</p></h2>
<p>This was the most problematic issue on Mac. Inference logs may contain progress bar control characters or, occasionally, garbled multibyte sequences. When processed by macOS&#8217;s <code>tr</code> or <code>grep</code>, you&#8217;ll see:</p>
<blockquote>
<p><code>tr: Illegal byte sequence</code></p>
</blockquote>
<p>This happens because the locale is set to UTF-8, causing invalid byte sequences to be treated as &#8220;invalid characters&#8221; and throwing an exception.</p>
<p>The solution is to set the locale to <strong>C (pass-through as byte sequences)</strong> for those commands.
</p>
<div class="highlight js-code-highlight">
<pre class="highlight shell"><code><span class="c"># Remove unwanted control characters from logs (avoids Illegal byte sequence)</span>
<span class="nv">LC_ALL</span><span class="o">=</span>C <span class="nb">tr</span> <span class="nt">-d</span> <span class="s1">'r'</span> &lt; backend.log <span class="o">&gt;</span> backend.clean.log
<span class="nv">LC_ALL</span><span class="o">=</span>C <span class="nb">grep</span> <span class="s2">"ERROR"</span> backend.log
</code></pre>
</div>
<p>Setting <code>LC_ALL=C</code> treats text as &#8220;bytes&#8221; rather than &#8220;characters,&#8221; preventing crashes due to invalid sequences. This is safer for pipelines that process or search logs programmatically.</p>
<h2>
<p>  TIP 4: Wait for startup completion using the health endpoint<br />
</p></h2>
<p>Loading models takes time, so sending requests immediately after starting with <code>nohup</code> will fail because the service isn&#8217;t ready. Using <code>sleep 10</code> as a workaround is unreliable—too short for slow machines and too long for fast ones.</p>
<p>The proper approach is to <strong>poll the service&#8217;s health endpoint until it returns a 200 status</strong>. In this setup, the health endpoint is <code>http://127.0.0.1:8770/health</code>, so we poll it.
</p>
<div class="highlight js-code-highlight">
<pre class="highlight shell"><code><span class="c"># Wait for the health endpoint to be ready before proceeding</span>
<span class="k">until </span>curl <span class="nt">-sf</span> http://127.0.0.1:8770/health <span class="o">&gt;</span>/dev/null<span class="p">;</span> <span class="k">do
  </span><span class="nb">sleep </span>1
<span class="k">done
</span><span class="nb">echo</span> <span class="s2">"backend ready"</span>
</code></pre>
</div>
<p><code>curl -sf</code> exits with a non-zero status on failure, so combining it with <code>until</code> allows you to wait until the service is ready. Waiting based on <strong>state</strong>, not a fixed delay, significantly improves the reliability of startup scripts.</p>
<h2>
<p>  TIP 5: Stop processes with <code>pkill</code> using pattern matching<br />
</p></h2>
<p>For background processes without a saved PID, <code>pkill</code> with a command-line pattern is convenient for stopping them.
</p>
<div class="highlight js-code-highlight">
<pre class="highlight shell"><code><span class="c"># Stop the inference service started with uvicorn</span>
pkill <span class="nt">-f</span> <span class="s2">"uvicorn server:app"</span>
</code></pre>
</div>
<p>The <code>-f</code> option matches the entire command line, so including specific details like the port or app name in the pattern prevents unrelated processes from being affected. For more precision, save the PID during startup and target it directly.</p>
<h2>
<p>  Summary<br />
</p></h2>
<ul>
<li>macOS <strong>does not have <code>setsid</code>/<code>timeout</code></strong>. Either install Homebrew&#8217;s <code>coreutils</code> (<code>gsetsid</code>/<code>gtimeout</code>) or use standard tool alternatives</li>
<li>Use <strong><code>nohup ... &amp; disown</code></strong> for background processes. Ignore SIGHUP and remove the job from the job table to prevent termination when the terminal is closed</li>
<li>Binary data in logs causes <code>tr</code>/<code>grep</code> to fail with <strong><code>Illegal byte sequence</code></strong> → Temporarily set <strong><code>LC_ALL=C</code></strong> to treat data as byte sequences</li>
<li>Instead of fixed <code>sleep</code> delays, <strong>poll the health endpoint with <code>until curl -sf</code></strong> to wait based on state</li>
<li>Stop processes with <strong><code>pkill -f "specific pattern"</code></strong>. Use a detailed pattern to avoid affecting unrelated processes</li>
</ul>]]></content:encoded>
					
					<wfw:commentRss>https://codango.com/tips-for-running-stable-background-ml-inference-on-macos/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>Handling Notification Taps in expo-notifications: Launch vs. Runtime</title>
		<link>https://codango.com/handling-notification-taps-in-expo-notifications-launch-vs-runtime/</link>
					<comments>https://codango.com/handling-notification-taps-in-expo-notifications-launch-vs-runtime/#respond</comments>
		
		<dc:creator><![CDATA[Codango Admin]]></dc:creator>
		<pubDate>Tue, 04 Aug 2026 01:03:44 +0000</pubDate>
				<category><![CDATA[Codango® Blog]]></category>
		<guid isPermaLink="false">https://codango.com/handling-notification-taps-in-expo-notifications-launch-vs-runtime/</guid>

					<description><![CDATA[This article is an English translation of the original Japanese article. When a user taps a push notification, the entry point differs depending on whether the app is running. If <a class="more-link" href="https://codango.com/handling-notification-taps-in-expo-notifications-launch-vs-runtime/">Continue reading <span class="screen-reader-text">  Handling Notification Taps in expo-notifications: Launch vs. Runtime</span><span class="meta-nav">&#8594;</span></a>]]></description>
										<content:encoded><![CDATA[<p>This article is an English translation of the original Japanese article.</p>
<p>When a user taps a push notification, the entry point differs depending on whether the app is running. If you only use one listener with Expo Router to navigate to the notification target screen, you may miss launches from the terminated state.</p>
<p>In my app, I handle both cases with two separate mechanisms:</p>
<ul>
<li>
<code>getLastNotificationResponseAsync()</code>: when the app launches from a notification tap</li>
<li>
<code>addNotificationResponseReceivedListener()</code>: when a notification is tapped while the app is running</li>
</ul>
<h2>
<p>  Structuring Notification Data<br />
</p></h2>
<p>The notifications sent from the server include the destination path in <code>data.url</code>.
</p>
<div class="highlight js-code-highlight">
<pre class="highlight typescript"><code><span class="p">{</span>
  <span class="nl">title</span><span class="p">:</span> <span class="dl">"</span><span class="s2">Attendance Deadline Reminder</span><span class="dl">"</span><span class="p">,</span>
  <span class="nx">body</span><span class="p">:</span> <span class="dl">"</span><span class="s2">Please confirm your attendance for tomorrow's practice</span><span class="dl">"</span><span class="p">,</span>
  <span class="nx">data</span><span class="p">:</span> <span class="p">{</span>
    <span class="nl">url</span><span class="p">:</span> <span class="dl">"</span><span class="s2">/organizations/org_123/schedules/schedule_456</span><span class="dl">"</span>
  <span class="p">}</span>
<span class="p">}</span>
</code></pre>
</div>
<p>Rather than parsing the displayed <code>title</code> or <code>body</code> to determine the destination, I pass a path the app can handle as separate data.</p>
<h2>
<p>  Handling Launch from Notification<br />
</p></h2>
<p>When the app is in a terminated state, I check for the last notification response after launch.
</p>
<div class="highlight js-code-highlight">
<pre class="highlight typescript"><code><span class="k">void</span> <span class="nx">Notifications</span><span class="p">.</span><span class="nf">getLastNotificationResponseAsync</span><span class="p">().</span><span class="nf">then</span><span class="p">((</span><span class="nx">response</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">{</span>
  <span class="k">if </span><span class="p">(</span><span class="nx">response</span><span class="p">)</span> <span class="p">{</span>
    <span class="nf">handleNotificationTap</span><span class="p">(</span><span class="nx">response</span><span class="p">.</span><span class="nx">notification</span><span class="p">);</span>
  <span class="p">}</span>
<span class="p">});</span>
</code></pre>
</div>
<p>This process is asynchronous. If you call <code>router.push</code> before verifying authentication or before the Navigator is ready, navigation can conflict. In my app, I run this after the login check completes.
</p>
<div class="highlight js-code-highlight">
<pre class="highlight typescript"><code><span class="nf">useRegisterPushToken</span><span class="p">(</span><span class="nx">authChecked</span><span class="p">);</span>
</code></pre>
</div>
<h2>
<p>  Handling Notification Taps While Running<br />
</p></h2>
<p>When a notification is tapped from either the background or foreground, the listener receives it.
</p>
<div class="highlight js-code-highlight">
<pre class="highlight typescript"><code><span class="kd">const</span> <span class="nx">subscription</span> <span class="o">=</span>
  <span class="nx">Notifications</span><span class="p">.</span><span class="nf">addNotificationResponseReceivedListener</span><span class="p">((</span><span class="nx">response</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">{</span>
    <span class="nf">handleNotificationTap</span><span class="p">(</span><span class="nx">response</span><span class="p">.</span><span class="nx">notification</span><span class="p">);</span>
  <span class="p">});</span>

<span class="k">return </span><span class="p">()</span> <span class="o">=&gt;</span> <span class="nx">subscription</span><span class="p">.</span><span class="nf">remove</span><span class="p">();</span>
</code></pre>
</div>
<p>To avoid duplicate listeners from screen remounts or Fast Refresh, I call <code>remove()</code> in the cleanup.</p>
<h2>
<p>  Unifying Both Entry Points<br />
</p></h2>
<p>Although the entry points differ, notification data validation and navigation are shared.
</p>
<div class="highlight js-code-highlight">
<pre class="highlight typescript"><code><span class="kd">function</span> <span class="nf">handleNotificationTap</span><span class="p">(</span><span class="nx">notification</span><span class="p">:</span> <span class="nx">Notifications</span><span class="p">.</span><span class="nx">Notification</span><span class="p">)</span> <span class="p">{</span>
  <span class="kd">const</span> <span class="nx">data</span> <span class="o">=</span> <span class="nx">notification</span><span class="p">.</span><span class="nx">request</span><span class="p">.</span><span class="nx">content</span><span class="p">.</span><span class="nx">data</span> <span class="k">as</span>
    <span class="o">|</span> <span class="p">{</span> <span class="nx">url</span><span class="p">?:</span> <span class="kr">string</span><span class="p">;</span> <span class="nl">linkUrl</span><span class="p">?:</span> <span class="kr">string</span> <span class="p">}</span>
    <span class="o">|</span> <span class="kc">null</span><span class="p">;</span>

  <span class="kd">const</span> <span class="nx">raw</span> <span class="o">=</span> <span class="nx">data</span><span class="p">?.</span><span class="nx">url</span> <span class="o">??</span> <span class="nx">data</span><span class="p">?.</span><span class="nx">linkUrl</span><span class="p">;</span>
  <span class="k">if </span><span class="p">(</span><span class="o">!</span><span class="nx">raw</span><span class="p">)</span> <span class="k">return</span><span class="p">;</span>

  <span class="kd">const</span> <span class="nx">path</span> <span class="o">=</span> <span class="nx">raw</span><span class="p">.</span><span class="nf">startsWith</span><span class="p">(</span><span class="dl">"</span><span class="s2">/</span><span class="dl">"</span><span class="p">)</span>
    <span class="p">?</span> <span class="nx">raw</span>
    <span class="p">:</span> <span class="p">(</span><span class="nx">raw</span><span class="p">.</span><span class="nf">match</span><span class="p">(</span><span class="sr">/^https</span><span class="se">?</span><span class="sr">:</span><span class="se">//[^/]</span><span class="sr">+</span><span class="se">(/</span><span class="sr">.+</span><span class="se">)</span><span class="sr">$/</span><span class="p">)?.[</span><span class="mi">1</span><span class="p">]</span> <span class="o">??</span> <span class="kc">null</span><span class="p">);</span>

  <span class="k">if </span><span class="p">(</span><span class="o">!</span><span class="nx">path</span><span class="p">)</span> <span class="k">return</span><span class="p">;</span>
  <span class="nx">router</span><span class="p">.</span><span class="nf">push</span><span class="p">(</span><span class="nx">path</span> <span class="k">as</span> <span class="nx">never</span><span class="p">);</span>
<span class="p">}</span>
</code></pre>
</div>
<p>For backward compatibility with previously sent notifications, I accept both <code>url</code> and <code>linkUrl</code>. For new implementations, using a single key is simpler.</p>
<p>Also, passing externally provided URLs directly to <code>router.push</code> is less safe. I define allowed hosts and path formats, converting them to app-internal routes before passing.</p>
<h2>
<p>  Not Navigating on Received Notification<br />
</p></h2>
<p><code>addNotificationReceivedListener</code> and <code>addNotificationResponseReceivedListener</code> serve different purposes.
</p>
<div class="highlight js-code-highlight">
<pre class="highlight typescript"><code><span class="nx">Notifications</span><span class="p">.</span><span class="nf">addNotificationReceivedListener</span><span class="p">((</span><span class="nx">notification</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">{</span>
  <span class="c1">// When notification is received</span>
<span class="p">});</span>

<span class="nx">Notifications</span><span class="p">.</span><span class="nf">addNotificationResponseReceivedListener</span><span class="p">((</span><span class="nx">response</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">{</span>
  <span class="c1">// When user taps the notification</span>
<span class="p">});</span>
</code></pre>
</div>
<p>Switching screens immediately when a notification arrives can disrupt the user&#8217;s current screen, such as an input form. In my app, screen navigation happens only with the response after a tap.</p>
<h2>
<p>  Unified Hook<br />
</p></h2>
<p>Simplifying the actual structure, it looks like this:
</p>
<div class="highlight js-code-highlight">
<pre class="highlight typescript"><code><span class="k">export</span> <span class="kd">function</span> <span class="nf">useNotificationNavigation</span><span class="p">(</span><span class="nx">enabled</span><span class="p">:</span> <span class="nx">boolean</span><span class="p">)</span> <span class="p">{</span>
  <span class="nf">useEffect</span><span class="p">(()</span> <span class="o">=&gt;</span> <span class="p">{</span>
    <span class="k">if </span><span class="p">(</span><span class="o">!</span><span class="nx">enabled</span><span class="p">)</span> <span class="k">return</span><span class="p">;</span>

    <span class="kd">let</span> <span class="nx">active</span> <span class="o">=</span> <span class="kc">true</span><span class="p">;</span>

    <span class="k">void</span> <span class="nx">Notifications</span><span class="p">.</span><span class="nf">getLastNotificationResponseAsync</span><span class="p">().</span><span class="nf">then</span><span class="p">((</span><span class="nx">response</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">{</span>
      <span class="k">if </span><span class="p">(</span><span class="nx">active</span> <span class="o">&amp;&amp;</span> <span class="nx">response</span><span class="p">)</span> <span class="p">{</span>
        <span class="nf">handleNotificationTap</span><span class="p">(</span><span class="nx">response</span><span class="p">.</span><span class="nx">notification</span><span class="p">);</span>
      <span class="p">}</span>
    <span class="p">});</span>

    <span class="kd">const</span> <span class="nx">subscription</span> <span class="o">=</span>
      <span class="nx">Notifications</span><span class="p">.</span><span class="nf">addNotificationResponseReceivedListener</span><span class="p">((</span><span class="nx">response</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">{</span>
        <span class="nf">handleNotificationTap</span><span class="p">(</span><span class="nx">response</span><span class="p">.</span><span class="nx">notification</span><span class="p">);</span>
      <span class="p">});</span>

    <span class="k">return </span><span class="p">()</span> <span class="o">=&gt;</span> <span class="p">{</span>
      <span class="nx">active</span> <span class="o">=</span> <span class="kc">false</span><span class="p">;</span>
      <span class="nx">subscription</span><span class="p">.</span><span class="nf">remove</span><span class="p">();</span>
    <span class="p">};</span>
  <span class="p">},</span> <span class="p">[</span><span class="nx">enabled</span><span class="p">]);</span>
<span class="p">}</span>
</code></pre>
</div>
<p>Rather than treating launches from the terminated state and taps while running as the same thing, separating the entry points and then passing them to a common navigation function made the structure easier to maintain.</p>
<h2>
<p>  References<br />
</p></h2>
<ul>
<li><a href="https://docs.expo.dev/versions/latest/sdk/notifications/" rel="noopener noreferrer">Expo Notifications</a></li>
<li><a href="https://docs.expo.dev/push-notifications/receiving-notifications/" rel="noopener noreferrer">Expo: Handle incoming notifications</a></li>
</ul>]]></content:encoded>
					
					<wfw:commentRss>https://codango.com/handling-notification-taps-in-expo-notifications-launch-vs-runtime/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>Claude Code subagents: how .claude/agents files work, and why Claude never uses yours</title>
		<link>https://codango.com/claude-code-subagents-how-claude-agents-files-work-and-why-claude-never-uses-yours/</link>
					<comments>https://codango.com/claude-code-subagents-how-claude-agents-files-work-and-why-claude-never-uses-yours/#respond</comments>
		
		<dc:creator><![CDATA[Codango Admin]]></dc:creator>
		<pubDate>Tue, 04 Aug 2026 01:00:20 +0000</pubDate>
				<category><![CDATA[Codango® Blog]]></category>
		<guid isPermaLink="false">https://codango.com/claude-code-subagents-how-claude-agents-files-work-and-why-claude-never-uses-yours/</guid>

					<description><![CDATA[Claude Code lets you define your own subagents — Markdown files that give Claude a specialist it can delegate to, with its own system prompt, its own tool access, and <a class="more-link" href="https://codango.com/claude-code-subagents-how-claude-agents-files-work-and-why-claude-never-uses-yours/">Continue reading <span class="screen-reader-text">  Claude Code subagents: how .claude/agents files work, and why Claude never uses yours</span><span class="meta-nav">&#8594;</span></a>]]></description>
										<content:encoded><![CDATA[<p>Claude Code lets you define your own subagents — Markdown files that give Claude a specialist it can delegate to, with its own system prompt, its own tool access, and its own context window. The mechanism is simple, but most &#8220;my subagent doesn&#8217;t work&#8221; problems come from three details the docs mention once and people skim past: the <code>description</code> field is the router, <code>name</code> collisions silently drop a file, and one bad <code>tools</code> entry stops the agent from launching at all.</p>
<p>Here&#8217;s the whole system, verified against the current docs.</p>
<h2>
<p>  The 30-second version<br />
</p></h2>
<p>A subagent is one Markdown file with YAML frontmatter:
</p>
<div class="highlight js-code-highlight">
<pre class="highlight markdown"><code><span class="nn">---</span>
<span class="na">name</span><span class="pi">:</span> <span class="s">code-improver</span>
<span class="na">description</span><span class="pi">:</span> <span class="s2">"</span><span class="s">Scans</span><span class="nv"> </span><span class="s">files</span><span class="nv"> </span><span class="s">and</span><span class="nv"> </span><span class="s">suggests</span><span class="nv"> </span><span class="s">improvements</span><span class="nv"> </span><span class="s">for</span><span class="nv"> </span><span class="s">readability,</span><span class="nv"> </span><span class="s">performance,</span><span class="nv"> </span><span class="s">and</span><span class="nv"> </span><span class="s">best</span><span class="nv"> </span><span class="s">practices.</span><span class="nv"> </span><span class="s">Use</span><span class="nv"> </span><span class="s">after</span><span class="nv"> </span><span class="s">writing</span><span class="nv"> </span><span class="s">or</span><span class="nv"> </span><span class="s">modifying</span><span class="nv"> </span><span class="s">code."</span>
<span class="na">tools</span><span class="pi">:</span> <span class="s">Read, Grep, Glob</span>
<span class="na">model</span><span class="pi">:</span> <span class="s">sonnet</span>
<span class="nn">---</span>

You are a code review specialist. When given files, analyze them for
readability, performance, and adherence to best practices. Report
concrete, minimal suggestions with file:line references.
</code></pre>
</div>
<p>Where you put it decides who gets it:</p>
<ul>
<li>
<code>.claude/agents/</code> in your project → this project (usually committed, so your team shares it)</li>
<li>
<code>~/.claude/agents/</code> → every project on your machine</li>
</ul>
<p>Both locations are scanned recursively, so you can organize files into subfolders like <code>agents/review/</code>. The subfolder path changes nothing about how the agent is identified — identity comes only from the <code>name</code> field, not the filename or path.</p>
<p>Only <code>name</code> and <code>description</code> are required. Everything else is optional.</p>
<h2>
<p>  Delegation is just description matching<br />
</p></h2>
<p>Claude reads every subagent&#8217;s <code>description</code> and decides to delegate when a task matches it. That&#8217;s the entire routing mechanism. There is no registration step, no config toggle — the quality of your <code>description</code> <em>is</em> the trigger.</p>
<p>Which means the most common failure is writing a description like a title:
</p>
<div class="highlight js-code-highlight">
<pre class="highlight yaml"><code><span class="c1"># never gets used</span>
<span class="na">description</span><span class="pi">:</span> <span class="s">Database expert</span>

<span class="c1"># gets used</span>
<span class="na">description</span><span class="pi">:</span> <span class="s">Reviews SQL queries and schema changes for slow patterns,</span>
  <span class="s">missing indexes, and migration risks. Use when SQL or migration files change.</span>
</code></pre>
</div>
<p>The second one works because it describes <em>when</em> to delegate, not just what the agent is. If you want delegation to happen without being asked, say so in the description — phrasing like &#8220;use proactively after code changes&#8221; is exactly what the official examples do.</p>
<p>You can always bypass routing and invoke one explicitly: &#8220;Use the code-improver subagent on the files I just changed.&#8221;</p>
<h2>
<p>  The fields that actually matter<br />
</p></h2>
<p>The full frontmatter list is longer, but these are the ones I reach for:</p>
<div class="table-wrapper-paragraph">
<table>
<thead>
<tr>
<th>Field</th>
<th>What it does</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>tools</code></td>
<td>Allowlist. Omit it and the agent inherits every tool available to subagents.</td>
</tr>
<tr>
<td><code>disallowedTools</code></td>
<td>Denylist, subtracted from the inherited or specified list.</td>
</tr>
<tr>
<td><code>model</code></td>
<td>
<code>sonnet</code>, <code>opus</code>, <code>haiku</code>, a full model ID, or <code>inherit</code> (the default).</td>
</tr>
<tr>
<td><code>maxTurns</code></td>
<td>Hard cap on agentic turns before the subagent stops.</td>
</tr>
<tr>
<td><code>skills</code></td>
<td>Skills preloaded into the subagent&#8217;s context at startup — full content, not just the description.</td>
</tr>
<tr>
<td><code>memory</code></td>
<td>
<code>user</code>, <code>project</code>, or <code>local</code> — gives the agent persistent memory across sessions.</td>
</tr>
<tr>
<td><code>background</code></td>
<td>
<code>true</code> forces background execution. Left unset, Claude chooses (and current versions default to background).</td>
</tr>
<tr>
<td><code>isolation</code></td>
<td>
<code>worktree</code> runs the agent in a temporary git worktree so its edits can&#8217;t collide with yours.</td>
</tr>
</tbody>
</table>
</div>
<p>Two sharp edges in <code>tools</code>: the entries must resolve to real tool names — if none of them do, the subagent fails to launch with an error naming the bad entries. And if you want a Skill preloaded, use the <code>skills</code> field; listing <code>Skill</code> in <code>tools</code> only grants the invocation tool, it doesn&#8217;t load anything.</p>
<p>One sharp edge in <code>name</code>: lowercase letters and hyphens, and no <code>:</code> — colons are reserved for plugin-scoped identifiers like <code>my-plugin:reviewer</code>. Current versions refuse to load a file whose name contains one, and the only symptom is a line in the debug log.</p>
<h2>
<p>  Precedence: who wins when names collide<br />
</p></h2>
<p>When multiple subagents share a name, the higher-priority location wins: managed (organization-deployed) definitions beat project definitions, which beat user definitions, which beat plugin agents. Across nested project directories, the definition closest to your working directory wins.</p>
<p>The dangerous case is two files with the same <code>name</code> under the <em>same</em> <code>.claude/agents/</code> tree — including subfolders. Claude Code loads only one, chosen by filesystem read order, not by any documented rule. Nothing warns you at runtime; your carefully updated definition may simply not be the one running. <code>/doctor</code> reports same-directory duplicates, so run it whenever a subagent behaves like an older version of itself.</p>
<p>Also worth knowing: a project or user subagent named <code>Explore</code> overrides the built-in read-only Explore agent. That&#8217;s occasionally useful (for example, pinning exploration to a cheaper model with <code>model: haiku</code>) — and occasionally an accident, when someone names a general agent &#8220;explore&#8221; and quietly replaces the built-in.</p>
<h2>
<p>  A note on the /agents command<br />
</p></h2>
<p>Older writeups tell you to run <code>/agents</code> for an interactive creation wizard. That wizard is gone in current versions — <code>/agents</code> now just points you at editing <code>.claude/agents/</code> directly, or you ask Claude to write the file for you. The file format and locations didn&#8217;t change, so any existing agent files keep working.</p>
<h2>
<p>  Debug checklist<br />
</p></h2>
<p>When a subagent isn&#8217;t being used, this order finds it fastest:</p>
<ol>
<li>
<strong>Does the file load at all?</strong> Name has a colon, or YAML is malformed → silently skipped. Check the debug log.</li>
<li>
<strong>Is your definition the one running?</strong> Duplicate <code>name</code> anywhere in the tree → run <code>/doctor</code>.</li>
<li>
<strong>Does the description say when to use it?</strong> Rewrite it as a trigger condition, not a job title.</li>
<li>
<strong>Do the <code>tools</code> entries resolve?</strong> A typo like <code>Greps</code> fails the launch with a zero-tools error.</li>
<li>
<strong>Still nothing?</strong> Invoke it explicitly by name once. If explicit invocation works but automatic delegation doesn&#8217;t, it&#8217;s always the description.</li>
</ol>
<p><em>I publish daily practical notes on Claude Code, Cursor, and Codex on Bluesky — <a href="https://bsky.app/profile/ai-shop.bsky.social" rel="noopener noreferrer">@ai-shop.bsky.social</a>. The tested skill and rules packs I maintain live at <a href="https://rulestack.gumroad.com/?ref=devto" rel="noopener noreferrer">Rulestack</a>.</em></p>]]></content:encoded>
					
					<wfw:commentRss>https://codango.com/claude-code-subagents-how-claude-agents-files-work-and-why-claude-never-uses-yours/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>Why QA Testing Is Important for AI-Generated Code</title>
		<link>https://codango.com/why-qa-testing-is-important-for-ai-generated-code/</link>
					<comments>https://codango.com/why-qa-testing-is-important-for-ai-generated-code/#respond</comments>
		
		<dc:creator><![CDATA[Codango Admin]]></dc:creator>
		<pubDate>Mon, 03 Aug 2026 11:15:56 +0000</pubDate>
				<category><![CDATA[Codango® Blog]]></category>
		<guid isPermaLink="false">https://codango.com/why-qa-testing-is-important-for-ai-generated-code/</guid>

					<description><![CDATA[1. Why AI-Generated Code Can Look Correct but Still Fail AI coding tools generate code by predicting patterns from your prompt, the surrounding code, and examples they were trained on. <a class="more-link" href="https://codango.com/why-qa-testing-is-important-for-ai-generated-code/">Continue reading <span class="screen-reader-text">  Why QA Testing Is Important for AI-Generated Code</span><span class="meta-nav">&#8594;</span></a>]]></description>
										<content:encoded><![CDATA[<h2>
<p>  1. Why AI-Generated Code Can Look Correct but Still Fail<br />
</p></h2>
<p>AI coding tools generate code by predicting patterns from your prompt, the surrounding code, and examples they were trained on. They don&#8217;t understand your application the way your engineering or product team does.</p>
<p>Take this simple discount function:
</p>
<div class="highlight js-code-highlight">
<pre class="highlight typescript"><code><span class="kd">function</span> <span class="nf">calculateDiscount</span><span class="p">(</span><span class="nx">total</span><span class="p">:</span> <span class="kr">number</span><span class="p">,</span> <span class="nx">isPremium</span><span class="p">:</span> <span class="nx">boolean</span><span class="p">):</span> <span class="kr">number</span> <span class="p">{</span>
  <span class="k">if </span><span class="p">(</span><span class="nx">isPremium</span><span class="p">)</span> <span class="p">{</span>
    <span class="k">return</span> <span class="nx">total</span> <span class="o">*</span> <span class="mf">0.2</span><span class="p">;</span>
  <span class="p">}</span>
  <span class="k">return</span> <span class="nx">total</span> <span class="o">*</span> <span class="mf">0.1</span><span class="p">;</span>
<span class="p">}</span>
</code></pre>
</div>
<p>It&#8217;s valid TypeScript. It might even pass a basic test. But it leaves real questions unanswered:</p>
<ul>
<li>Should non-premium users always get a discount?</li>
<li>Is there a minimum order value?</li>
<li>Is the discount capped?</li>
<li>Does it apply to tax or shipping?</li>
<li>What happens with a negative total?</li>
<li>Can it stack with other promotions?</li>
</ul>
<p>The code can be technically correct while still violating the actual business requirement. <strong><a href="https://www.synfinitydynamics.com/blogs/importance-of-qa-testingutm_source=devto&amp;utm_medium=social&amp;utm_campaign=blog_distribution" rel="noopener noreferrer">QA testing validates behavior, not just syntax</a></strong> &#8211; and that distinction is the core reason this whole article exists.</p>
<h2>
<p>  2. Five Ways AI-Generated Code Goes Wrong<br />
</p></h2>
<h3>
<p>  2.1 Misunderstood Business Requirements<br />
</p></h3>
<p>AI-generated code often solves a slightly different problem than the one the business actually needs solved.</p>
<p>Say the rule is: <em>&#8220;Users can access premium features until the end of their paid billing period, even after cancelling renewal.&#8221;</em></p>
<p>A generated check might look like this:
</p>
<div class="highlight js-code-highlight">
<pre class="highlight typescript"><code><span class="kd">function</span> <span class="nf">canAccessPremium</span><span class="p">(</span><span class="nx">subscription</span><span class="p">:</span> <span class="nx">Subscription</span><span class="p">):</span> <span class="nx">boolean</span> <span class="p">{</span>
  <span class="k">return</span> <span class="nx">subscription</span><span class="p">.</span><span class="nx">status</span> <span class="o">===</span> <span class="dl">'</span><span class="s1">active</span><span class="dl">'</span><span class="p">;</span>
<span class="p">}</span>
</code></pre>
</div>
<p>This revokes access the moment status flips to <code>cancelled</code> &#8211; even though the customer already paid for the remaining period. A correct version needs to consider the expiry date instead:
</p>
<div class="highlight js-code-highlight">
<pre class="highlight typescript"><code><span class="kd">function</span> <span class="nf">canAccessPremium</span><span class="p">(</span>
  <span class="nx">subscription</span><span class="p">:</span> <span class="nx">Subscription</span><span class="p">,</span>
  <span class="nx">now</span><span class="p">:</span> <span class="nb">Date</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">Date</span><span class="p">()</span>
<span class="p">):</span> <span class="nx">boolean</span> <span class="p">{</span>
  <span class="k">return</span> <span class="nx">subscription</span><span class="p">.</span><span class="nx">expiresAt</span> <span class="o">&gt;</span> <span class="nx">now</span><span class="p">;</span>
<span class="p">}</span>
</code></pre>
</div>
<p>QA needs to check the real scenarios: active, cancelled-but-paid, expired, failed renewal, trial, grace period, refunded. Skip these, and you get billing disputes and angry support tickets not compiler errors.</p>
<h3>
<p>  2.2 Hidden Edge Cases<br />
</p></h3>
<p>AI-generated code tends to handle the happy path well and little else. Production doesn&#8217;t stay on the happy path it deals with empty values, invalid formats, duplicate requests, slow networks, API failures, and concurrent updates.
</p>
<div class="highlight js-code-highlight">
<pre class="highlight typescript"><code><span class="kd">function</span> <span class="nf">isValidEmail</span><span class="p">(</span><span class="nx">email</span><span class="p">:</span> <span class="kr">string</span><span class="p">):</span> <span class="nx">boolean</span> <span class="p">{</span>
  <span class="k">return</span> <span class="nx">email</span><span class="p">.</span><span class="nf">includes</span><span class="p">(</span><span class="dl">'</span><span class="s1">@</span><span class="dl">'</span><span class="p">);</span>
<span class="p">}</span>
</code></pre>
</div>
<p>This happily accepts <code>@</code>, <code>user@</code>, and <code>@domain.com</code>. A stronger version and a test suite that defines exactly which formats your app accepts closes that gap:
</p>
<div class="highlight js-code-highlight">
<pre class="highlight typescript"><code><span class="nf">describe</span><span class="p">(</span><span class="dl">'</span><span class="s1">isValidEmail</span><span class="dl">'</span><span class="p">,</span> <span class="p">()</span> <span class="o">=&gt;</span> <span class="p">{</span>
  <span class="nf">it</span><span class="p">(</span><span class="dl">'</span><span class="s1">accepts a valid email</span><span class="dl">'</span><span class="p">,</span> <span class="p">()</span> <span class="o">=&gt;</span> <span class="p">{</span>
    <span class="nf">expect</span><span class="p">(</span><span class="nf">isValidEmail</span><span class="p">(</span><span class="dl">'</span><span class="s1">user@example.com</span><span class="dl">'</span><span class="p">)).</span><span class="nf">toBe</span><span class="p">(</span><span class="kc">true</span><span class="p">);</span>
  <span class="p">});</span>
  <span class="nf">it</span><span class="p">(</span><span class="dl">'</span><span class="s1">rejects an empty value</span><span class="dl">'</span><span class="p">,</span> <span class="p">()</span> <span class="o">=&gt;</span> <span class="p">{</span>
    <span class="nf">expect</span><span class="p">(</span><span class="nf">isValidEmail</span><span class="p">(</span><span class="dl">''</span><span class="p">)).</span><span class="nf">toBe</span><span class="p">(</span><span class="kc">false</span><span class="p">);</span>
  <span class="p">});</span>
  <span class="nf">it</span><span class="p">(</span><span class="dl">'</span><span class="s1">rejects a missing domain</span><span class="dl">'</span><span class="p">,</span> <span class="p">()</span> <span class="o">=&gt;</span> <span class="p">{</span>
    <span class="nf">expect</span><span class="p">(</span><span class="nf">isValidEmail</span><span class="p">(</span><span class="dl">'</span><span class="s1">user@</span><span class="dl">'</span><span class="p">)).</span><span class="nf">toBe</span><span class="p">(</span><span class="kc">false</span><span class="p">);</span>
  <span class="p">});</span>
  <span class="nf">it</span><span class="p">(</span><span class="dl">'</span><span class="s1">rejects a missing username</span><span class="dl">'</span><span class="p">,</span> <span class="p">()</span> <span class="o">=&gt;</span> <span class="p">{</span>
    <span class="nf">expect</span><span class="p">(</span><span class="nf">isValidEmail</span><span class="p">(</span><span class="dl">'</span><span class="s1">@example.com</span><span class="dl">'</span><span class="p">)).</span><span class="nf">toBe</span><span class="p">(</span><span class="kc">false</span><span class="p">);</span>
  <span class="p">});</span>
<span class="p">});</span>
</code></pre>
</div>
<p><a href="https://www.synfinitydynamics.com/blogs/vibe-coding-vs-traditional-programming-vs-ai-assisted-development?utm_source=devto&amp;utm_medium=article&amp;utm_campaign=blog_distribution" rel="noopener noreferrer">AI can absolutely write tests like these</a> the risk is that it generates them based on the same incomplete assumptions as the original code.</p>
<h3>
<p>  2.3 Hidden Security Problems<br />
</p></h3>
<p>Working code isn&#8217;t the same as safe code. Common risks in AI-generated output include missing input validation, SQL injection, broken access control, and weak auth logic.
</p>
<div class="highlight js-code-highlight">
<pre class="highlight typescript"><code><span class="kd">const</span> <span class="nx">query</span> <span class="o">=</span> <span class="s2">`SELECT * FROM users WHERE email = '</span><span class="p">${</span><span class="nx">email</span><span class="p">}</span><span class="s2">'`</span><span class="p">;</span>
<span class="kd">const</span> <span class="nx">result</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">database</span><span class="p">.</span><span class="nf">query</span><span class="p">(</span><span class="nx">query</span><span class="p">);</span>
</code></pre>
</div>
<p>This runs fine in testing and opens an SQL injection hole in production. A parameterized query fixes it:
</p>
<div class="highlight js-code-highlight">
<pre class="highlight typescript"><code><span class="kd">const</span> <span class="nx">result</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">database</span><span class="p">.</span><span class="nf">query</span><span class="p">(</span>
  <span class="dl">'</span><span class="s1">SELECT * FROM users WHERE email = $1</span><span class="dl">'</span><span class="p">,</span>
  <span class="p">[</span><span class="nx">email</span><span class="p">]</span>
<span class="p">);</span>
</code></pre>
</div>
<p>Access control slips through just as easily:
</p>
<div class="highlight js-code-highlight">
<pre class="highlight typescript"><code><span class="nx">app</span><span class="p">.</span><span class="k">delete</span><span class="p">(</span><span class="dl">'</span><span class="s1">/api/documents/:id</span><span class="dl">'</span><span class="p">,</span> <span class="nx">authenticate</span><span class="p">,</span> <span class="k">async </span><span class="p">(</span><span class="nx">req</span><span class="p">,</span> <span class="nx">res</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">{</span>
  <span class="k">await</span> <span class="nx">documentRepository</span><span class="p">.</span><span class="k">delete</span><span class="p">(</span><span class="nx">req</span><span class="p">.</span><span class="nx">params</span><span class="p">.</span><span class="nx">id</span><span class="p">);</span>
  <span class="nx">res</span><span class="p">.</span><span class="nf">status</span><span class="p">(</span><span class="mi">204</span><span class="p">).</span><span class="nf">send</span><span class="p">();</span>
<span class="p">});</span>
</code></pre>
</div>
<p>This checks that a user is <em>logged in</em> not that they <em>own</em> the document. Any authenticated user could delete anyone&#8217;s file. Security testing needs to specifically cover authentication, role permissions, resource ownership, tenant isolation, and rate limits not just &#8220;does it run.&#8221;</p>
<h3>
<p>  2.4 Integration Failures<br />
</p></h3>
<p>AI-generated code is usually tested in isolation, but production systems are made of many connected parts: frontend, backend, database, payment providers, queues, third-party APIs.</p>
<p>A function can work perfectly alone and still fail once it&#8217;s wired up. For example, the frontend expects:
</p>
<div class="highlight js-code-highlight">
<pre class="highlight json"><code><span class="p">{</span><span class="w"> </span><span class="nl">"userId"</span><span class="p">:</span><span class="w"> </span><span class="s2">"123"</span><span class="p">,</span><span class="w"> </span><span class="nl">"fullName"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Maya Shah"</span><span class="w"> </span><span class="p">}</span><span class="w">
</span></code></pre>
</div>
<p>but the generated backend returns:
</p>
<div class="highlight js-code-highlight">
<pre class="highlight json"><code><span class="p">{</span><span class="w"> </span><span class="nl">"id"</span><span class="p">:</span><span class="w"> </span><span class="s2">"123"</span><span class="p">,</span><span class="w"> </span><span class="nl">"name"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Maya Shah"</span><span class="w"> </span><span class="p">}</span><span class="w">
</span></code></pre>
</div>
<p>Both are reasonable on their own and incompatible together. Integration and regression testing catch this class of bug: mismatched fields, wrong types, broken event payloads, and small AI-generated changes that quietly break features that already worked.</p>
<h3>
<p>  2.5 Performance Problems<br />
</p></h3>
<p>Logically correct code can still be slow. Classic example an N+1 query:
</p>
<div class="highlight js-code-highlight">
<pre class="highlight typescript"><code><span class="kd">const</span> <span class="nx">orders</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">orderRepository</span><span class="p">.</span><span class="nf">findAll</span><span class="p">();</span>

<span class="k">for </span><span class="p">(</span><span class="kd">const</span> <span class="nx">order</span> <span class="k">of</span> <span class="nx">orders</span><span class="p">)</span> <span class="p">{</span>
  <span class="nx">order</span><span class="p">.</span><span class="nx">customer</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">customerRepository</span><span class="p">.</span><span class="nf">findById</span><span class="p">(</span><span class="nx">order</span><span class="p">.</span><span class="nx">customerId</span><span class="p">);</span>
<span class="p">}</span>
</code></pre>
</div>
<p>Fine with 10 orders. A serious problem with 10,000. Other common issues: repeated API calls, missing indexes, loading full datasets into memory, and missing pagination. Performance testing needs to reflect <em>realistic</em> data volumes, not just the sample size in the original prompt.</p>
<h2>
<p>  3. Why AI-Generated Tests Aren&#8217;t Enough on Their Own<br />
</p></h2>
<p>AI is genuinely useful for scaffolding tests templates, mocks, sample data, common failure cases. But generated tests shouldn&#8217;t be treated as independent proof of correctness, because the same model can write both the bug and the test that confirms it.
</p>
<div class="highlight js-code-highlight">
<pre class="highlight typescript"><code><span class="kd">function</span> <span class="nf">calculateShipping</span><span class="p">(</span><span class="nx">total</span><span class="p">:</span> <span class="kr">number</span><span class="p">):</span> <span class="kr">number</span> <span class="p">{</span>
  <span class="k">return</span> <span class="nx">total</span> <span class="o">&gt;</span> <span class="mi">100</span> <span class="p">?</span> <span class="mi">0</span> <span class="p">:</span> <span class="mi">10</span><span class="p">;</span>
<span class="p">}</span>
</code></pre>
</div>
<div class="highlight js-code-highlight">
<pre class="highlight typescript"><code><span class="nf">it</span><span class="p">(</span><span class="dl">'</span><span class="s1">returns free shipping above 100</span><span class="dl">'</span><span class="p">,</span> <span class="p">()</span> <span class="o">=&gt;</span> <span class="p">{</span>
  <span class="nf">expect</span><span class="p">(</span><span class="nf">calculateShipping</span><span class="p">(</span><span class="mi">150</span><span class="p">)).</span><span class="nf">toBe</span><span class="p">(</span><span class="mi">0</span><span class="p">);</span>
<span class="p">});</span>
</code></pre>
</div>
<p>This test passes because it repeats the same assumption baked into the function. If the real rule is <em>&#8220;free shipping at ₹1,000 or more, excluding tax,&#8221;</em> both the code and the test are wrong, and the green checkmark tells you nothing.</p>
<p>AI-generated tests also tend to lean on happy paths only, use weak assertions, over-mock dependencies, and validate implementation details instead of actual business outcomes. Use AI to speed up test creation but have a human confirm the tests reflect the real requirement, not just the code as written.</p>
<h2>
<p>  4. Testing Types AI-Generated Code Needs<br />
</p></h2>
<div class="table-wrapper-paragraph">
<table>
<thead>
<tr>
<th>Testing type</th>
<th>What it validates</th>
</tr>
</thead>
<tbody>
<tr>
<td>Unit testing</td>
<td>Individual functions and components</td>
</tr>
<tr>
<td>Integration testing</td>
<td>Communication between modules, APIs, and databases</td>
</tr>
<tr>
<td>End-to-end testing</td>
<td>Complete user workflows</td>
</tr>
<tr>
<td>Regression testing</td>
<td>Existing features still work after changes</td>
</tr>
<tr>
<td>Security testing</td>
<td>Permissions, validation, vulnerabilities</td>
</tr>
<tr>
<td>Performance testing</td>
<td>Speed, stability, scalability</td>
</tr>
<tr>
<td>Exploratory testing</td>
<td>Unexpected behavior automated tests miss</td>
</tr>
</tbody>
</table>
</div>
<p>Not every feature needs the same depth of testing. A text-formatting helper doesn&#8217;t carry the same risk as a payment workflow match testing effort to business impact.</p>
<h2>
<p>  5. A Practical QA Workflow<br />
</p></h2>
<div class="highlight js-code-highlight">
<pre class="highlight plaintext"><code>Define the requirement
        ↓
Generate code with AI
        ↓
Review the generated output
        ↓
Run linting and static analysis
        ↓
Create and review test cases
        ↓
Run unit and integration tests
        ↓
Test edge cases and permissions
        ↓
Deploy to staging
        ↓
Perform human validation
        ↓
Deploy and monitor
</code></pre>
</div>
<p><strong>Define the requirement clearly</strong> &#8211; document expected inputs, outputs, business rules, failure behavior, permissions, and performance expectations <em>before</em> generating code.</p>
<p><strong>Review the generated code</strong> &#8211; check it against your architecture, approved libraries, error handling, naming conventions, and how it handles sensitive data.</p>
<p><strong>Run automated quality checks</strong> &#8211; linters, type checkers, static analysis, dependency scanners, and CI quality gates as a fast first filter.</p>
<p><strong>Test realistic scenarios</strong> &#8211; go beyond the prompt&#8217;s example. Invalid inputs, slow services, duplicate actions, expired data, unauthorized users, large datasets.</p>
<p><strong>Validate in staging</strong> &#8211; against systems that resemble production: real databases, real APIs, real permission structures.</p>
<p><strong>Monitor after deployment</strong> &#8211; no amount of pre-release testing predicts every production scenario. Track error rates, slow requests, failed transactions, and unexpected logs.</p>
<h2>
<p>  6. When Extra QA Is Non-Negotiable<br />
</p></h2>
<p>Some categories of code deserve more scrutiny than others, because the cost of a defect is disproportionately high:</p>
<ul>
<li>Payments</li>
<li>Authentication</li>
<li>Subscription access</li>
<li>Personal or healthcare data</li>
<li>Financial calculations</li>
<li>User permissions</li>
<li>Database migrations</li>
<li>File deletion</li>
<li>Legal or compliance workflows</li>
</ul>
<p>AI can help generate the implementation for these &#8211; but final responsibility has to stay with the engineering and QA team. A small defect here doesn&#8217;t just mean a bug ticket; it can mean financial loss, data exposure, or a compliance violation.</p>
<h2>
<p>  7. Best Practices Checklist<br />
</p></h2>
<ul>
<li>Provide clear requirements and constraints upfront</li>
<li>Treat generated code as a draft, not a finished product</li>
<li>Review every external dependency it pulls in</li>
<li>Test business rules separately from implementation logic</li>
<li>Include negative and boundary-condition tests</li>
<li>Verify authentication and authorization explicitly</li>
<li>Run automated checks before merging</li>
<li>Validate in a staging environment that mirrors production</li>
<li>Keep a human accountable for final approval</li>
<li>Monitor production behavior after release</li>
</ul>
<p>The goal isn&#8217;t to avoid AI-generated code it&#8217;s to use it without lowering your engineering standards.</p>
<h2>
<p>  8. Final Thoughts<br />
</p></h2>
<p>AI coding tools can dramatically improve development speed, but faster implementation doesn&#8217;t automatically mean higher-quality software. Generated code can compile and pass basic tests while still carrying incorrect business logic, missing edge cases, security holes, integration mismatches, or performance problems.</p>
<p>QA testing is what turns generated output into <em>verified</em> software. AI can write code, suggest tests, and flag possible issues but it can&#8217;t replace the responsibility of understanding requirements, weighing risk, and confirming the system behaves correctly.</p>
<blockquote>
<p>AI can generate code quickly. Only testing can give you confidence it works correctly in the real world.</p>
</blockquote>
<h2>
<p>  <img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f4da.png" alt="📚" class="wp-smiley" style="height: 1em; max-height: 1em;" /> Related Reading<br />
</p></h2>
<ul>
<li><a href="https://www.synfinitydynamics.com/blogs/ai-and-the-future-of-work?utm_source=devto&amp;utm_medium=article&amp;utm_campaign=blog_distribution" rel="noopener noreferrer">AI and the Future of Work: How Businesses and Employees Can Prepare for AI</a></li>
<li><a href="https://www.synfinitydynamics.com/blogs/ai-transforming-flutter-app-development-2026?utm_source=devto&amp;utm_medium=article&amp;utm_campaign=blog_distribution" rel="noopener noreferrer">How AI is Transforming Flutter App Development in 2026</a></li>
<li><a href="https://www.synfinitydynamics.com/blogs/ai-in-fintech?utm_source=devto&amp;utm_medium=article&amp;utm_campaign=blog_distribution" rel="noopener noreferrer">AI in FinTech: Use Cases, Benefits, Challenges, and Future Trends</a></li>
<li><a href="https://www.synfinitydynamics.com/blogs/vibe-coding-vs-traditional-programming-vs-ai-assisted-development?utm_source=devto&amp;utm_medium=article&amp;utm_campaign=blog_distribution" rel="noopener noreferrer">Vibe Coding vs Traditional Programming vs AI-Assisted Development</a></li>
</ul>]]></content:encoded>
					
					<wfw:commentRss>https://codango.com/why-qa-testing-is-important-for-ai-generated-code/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>Facebook Developer Tools MCP คืออะไร? — คู่มือ Meta MCP สำหรับคนทำโฆษณา วิเคราะห์ข้อมูล และ Automation</title>
		<link>https://codango.com/facebook-developer-tools-mcp-%e0%b8%84%e0%b8%b7%e0%b8%ad%e0%b8%ad%e0%b8%b0%e0%b9%84%e0%b8%a3-%e0%b8%84%e0%b8%b9%e0%b9%88%e0%b8%a1%e0%b8%b7%e0%b8%ad-meta-mcp-%e0%b8%aa%e0%b8%b3%e0%b8%ab/</link>
					<comments>https://codango.com/facebook-developer-tools-mcp-%e0%b8%84%e0%b8%b7%e0%b8%ad%e0%b8%ad%e0%b8%b0%e0%b9%84%e0%b8%a3-%e0%b8%84%e0%b8%b9%e0%b9%88%e0%b8%a1%e0%b8%b7%e0%b8%ad-meta-mcp-%e0%b8%aa%e0%b8%b3%e0%b8%ab/#respond</comments>
		
		<dc:creator><![CDATA[Codango Admin]]></dc:creator>
		<pubDate>Mon, 03 Aug 2026 11:13:31 +0000</pubDate>
				<category><![CDATA[Codango® Blog]]></category>
		<guid isPermaLink="false">https://codango.com/facebook-developer-tools-mcp-%e0%b8%84%e0%b8%b7%e0%b8%ad%e0%b8%ad%e0%b8%b0%e0%b9%84%e0%b8%a3-%e0%b8%84%e0%b8%b9%e0%b9%88%e0%b8%a1%e0%b8%b7%e0%b8%ad-meta-mcp-%e0%b8%aa%e0%b8%b3%e0%b8%ab/</guid>

					<description><![CDATA[Facebook Developer Tools MCP คืออะไร? — คู่มือ Meta MCP สำหรับคนทำโฆษณา วิเคราะห์ข้อมูล และ Automation โดย Nokka (นก-กา) &#124; 2 สิงหาคม 2026 บทความนี้เขียนโดย AI (deepseek-v4-pro) ผ่าน Hermes Agent ภายใต้การควบคุมและตรวจสอบคุณภาพโดยมนุษย์ — Nokka (นก-กา) <a class="more-link" href="https://codango.com/facebook-developer-tools-mcp-%e0%b8%84%e0%b8%b7%e0%b8%ad%e0%b8%ad%e0%b8%b0%e0%b9%84%e0%b8%a3-%e0%b8%84%e0%b8%b9%e0%b9%88%e0%b8%a1%e0%b8%b7%e0%b8%ad-meta-mcp-%e0%b8%aa%e0%b8%b3%e0%b8%ab/">Continue reading <span class="screen-reader-text">  Facebook Developer Tools MCP คืออะไร? — คู่มือ Meta MCP สำหรับคนทำโฆษณา วิเคราะห์ข้อมูล และ Automation</span><span class="meta-nav">&#8594;</span></a>]]></description>
										<content:encoded><![CDATA[<h1>
<p>  Facebook Developer Tools MCP คืออะไร? — คู่มือ Meta MCP สำหรับคนทำโฆษณา วิเคราะห์ข้อมูล และ Automation<br />
</p></h1>
<p><em>โดย Nokka (นก-กา) | 2 สิงหาคม 2026</em></p>
<p><em>บทความนี้เขียนโดย AI (deepseek-v4-pro) ผ่าน Hermes Agent ภายใต้การควบคุมและตรวจสอบคุณภาพโดยมนุษย์ — Nokka (นก-กา)</em></p>
<p>ถ้าคุณทำโฆษณา Facebook หรือดูแลระบบ Marketing API ของ Meta มาก่อน คงรู้ดีว่าการเช็คสถานะแอป เช็ค API health หรือตั้งค่า webhook เป็นงานที่ต้องสลับหลายหน้าจอไปมา</p>
<p>บางทีต้องเปิด Ads Manager คู่กับ Business Manager คู่กับ Events Manager แล้วยังต้องไปอ่าน API changelog อีก</p>
<p>Meta เปิดตัว <strong>Developer Tools MCP</strong> เมื่อกลางปี 2026 เพื่อให้ AI agent หรือ IDE assistant เข้าถึงข้อมูลเหล่านี้ได้จากจุดเดียว ผ่าน <strong>Model Context Protocol (MCP)</strong></p>
<p>MCP เป็นสตандарต์ที่ Anthropic เริ่ม และตอนนี้ OpenAI, Cursor, Claude Desktop ก็รองรับแล้ว</p>
<p>ในมุมมองของผม สิ่งนี้ไม่ได้เปลี่ยนวิธีทำโฆษณาโดยตรง แต่เปลี่ยนความเร็วของ <strong>debugging, analytics และ automation</strong> ที่อยู่รอบๆ การทำโฆษณา</p>
<p>Meta ระบุว่า MCP ตัวนี้กำลังเปิดให้ใช้ทยอยๆ (rolling out gradually) ดังนั้นบาง account อาจยังไม่เห็นตัวเลือกนี้ในทันที [1]</p>
<h2>
<p>  Developer Tools MCP ทำงานยังไง<br />
</p></h2>
<p>Meta Developer Tools MCP server เป็นตัวกลางที่เชื่อมต่อ AI assistant ของคุณเข้ากับแพลตฟอร์มนักพัฒนาของ Meta โดยตรง [1]</p>
<div class="table-wrapper-paragraph">
<table>
<thead>
<tr>
<th>รายละเอียด</th>
<th>ค่าที่ใช้</th>
</tr>
</thead>
<tbody>
<tr>
<td>ชื่อ server</td>
<td>Meta Developer Tools</td>
</tr>
<tr>
<td>Endpoint</td>
<td><code>https://mcp.facebook.com/devtools</code></td>
</tr>
<tr>
<td>Transport</td>
<td>Streamable HTTP</td>
</tr>
<tr>
<td>การยืนยันตัวตน</td>
<td>OAuth ผ่าน Meta developer account</td>
</tr>
<tr>
<td>สถานะ</td>
<td>Beta — อินเทอร์เฟซและเครื่องมืออาจเปลี่ยน</td>
</tr>
</tbody>
</table>
</div>
<p>ข้อดีคือ <strong>ไม่ต้องไปหา App ID หรือ App Secret มาใส่ใน config</strong> แค่ล็อกอินด้วย Meta account ผ่าน OAuth แล้วเลือกว่าจะให้ AI agent เข้าถึงแอปไหนบ้าง</p>
<p>จากนั้น agent ก็สามารถอ่านข้อมูลหรือจัดการ webhook subscription ได้ตามสิทธิที่คุณกำหนด [1]</p>
<h2>
<p>  10 เครื่องมือที่เปิดให้ AI agent ใช้<br />
</p></h2>
<p>Developer Tools MCP เปิดให้ agent เรียกใช้งานได้ 10 เครื่องมือ แบ่งเป็น 2 กลุ่มหลักๆ [1]</p>
<h3>
<p>  กลุ่มอ่านข้อมูล (Read scope)<br />
</p></h3>
<p><strong>1. <code>devtools_app_list</code></strong> — ดูรายชื่อแอปที่คุณมีสิทธิ์เข้าถึง พร้อมบอกบทบาท (developer, admin, tester) และสิทธิ์ที่ให้ไว้ (read หรือ manage)</p>
<p><strong>2. <code>devtools_app</code></strong> — อ่านการตั้งค่า สิทธิ์ และคอนฟิกของแอป เช่น basic settings, advanced settings, security configuration, platform restrictions</p>
<p><strong>3. <code>devtools_app_review</code></strong> — เช็คสถานะ App Review ว่าแอปขอสิทธิ์หรือฟีเจอร์อะไรไปแล้ว อนุมัติหรือยัง</p>
<p><strong>4. <code>devtools_compliance</code></strong> — เช็คสถานะ compliance ของแอป ว่ามี violation หรือ required actions อะไรตกค้างอยู่หรือไม่</p>
<p><strong>5. <code>devtools_api_usage</code></strong> — ดู API health ของแอป รวมถึง rate limit ที่ใกล้เต็มหรือไม่ call volume เป็นอย่างไร และมี API ไหนที่กำลังจะเลิกใช้ (deprecated) บ้าง</p>
<p><strong>6. <code>devtools_webhook_list</code></strong> — ดูรายชื่อ webhook topic ที่แอปสามารถ subscribe ได้ และว่าตอนนี้ subscribe อะไรไว้แล้วบ้าง</p>
<p><strong>7. <code>devtools_api_changelog</code></strong> — ค้นหา changelog product ของ Meta และ RSS feed URL เพื่อติดตามว่ามีอะไรเปลี่ยนแปลงเร็วๆ นี้</p>
<p><strong>8. <code>devtools_discovery</code></strong> — ค้นหาเอกสาร official ของ Meta ได้ด้วยคำถามธรรมชาติ เช่น &#8220;how to set up WhatsApp Cloud API webhooks&#8221;</p>
<h3>
<p>  กลุ่มจัดการ (Manage scope — เขียนได้เฉพาะ webhook)<br />
</p></h3>
<p><strong>9. <code>devtools_webhook_manage</code></strong> — สร้าง อัปเดต หรือลบ webhook subscription ของแอป โดยต้องมี callback URL ที่เป็น HTTPS และผ่านการ verify จาก Meta แล้ว</p>
<p><strong>10. <code>devtools_webhook_test</code></strong> — ส่ง test payload ไปยัง webhook subscription เพื่อตรวจสอบว่า endpoint รับ event ได้จริง</p>
<p>จุดสำคัญคือ write access ของ MCP นี้มีแค่ <strong>webhook management</strong> เท่านั้น ตัว agent ยังไม่สามารถแก้ campaign budget, สร้างโฆษณา หรือเปลี่ยน targeting ได้โดยตรง [1]</p>
<h2>
<p>  ตั้งค่าในเครื่องมือที่คนทำโฆษณาใช้บ่อย<br />
</p></h2>
<p>Meta ออกแบบให้รองรับหลาย client ตั้งแต่ Claude Code, Claude Desktop, OpenAI Codex App, ChatGPT ไปจนถึง Cursor [1]</p>
<h3>
<p>  Claude Code<br />
</p></h3>
<div class="highlight js-code-highlight">
<pre class="highlight shell"><code>claude mcp add <span class="nt">--transport</span> http meta_developer_tools https://mcp.facebook.com/devtools
</code></pre>
</div>
<p>จากนั้นพิมพ์ <code>/mcp</code> ในเซสชันแล้วเลือก <code>meta_developer_tools</code> เพื่อล็อกอิน</p>
<h3>
<p>  Claude Desktop<br />
</p></h3>
<p>ไปที่ <strong>Settings &gt; Connectors &gt; Add custom connector</strong> แล้วตั้งค่า:</p>
<ul>
<li>Type: HTTP</li>
<li>URL: <code>https://mcp.facebook.com/devtools</code>
</li>
</ul>
<h3>
<p>  OpenAI Codex App<br />
</p></h3>
<p>ไปที่ <strong>Settings &gt; MCP Servers &gt; Add servers</strong> เลือก <strong>Streamable HTTP</strong> แล้วใส่:</p>
<ul>
<li>Name: <code>Meta Developer Tools</code>
</li>
<li>URL: <code>https://mcp.facebook.com/devtools</code>
</li>
</ul>
<h3>
<p>  ChatGPT (ต้องเปิด Developer Mode ก่อน)<br />
</p></h3>
<p>ไปที่ <strong>Settings &gt; Connectors &gt; Advanced settings</strong> เปิด Developer Mode จากนั้นสร้าง connector ใหม่ด้วย URL <code>https://mcp.facebook.com/devtools</code></p>
<h3>
<p>  Cursor<br />
</p></h3>
<p>เพิ่มลงใน <code>~/.cursor/mcp.json</code> หรือ <code>.cursor/mcp.json</code> ของโปรเจกต์:
</p>
<div class="highlight js-code-highlight">
<pre class="highlight json"><code><span class="p">{</span><span class="w">
  </span><span class="nl">"mcpServers"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="nl">"MetaDeveloperTools"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
      </span><span class="nl">"url"</span><span class="p">:</span><span class="w"> </span><span class="s2">"https://mcp.facebook.com/devtools"</span><span class="p">,</span><span class="w">
      </span><span class="nl">"type"</span><span class="p">:</span><span class="w"> </span><span class="s2">"http"</span><span class="w">
    </span><span class="p">}</span><span class="w">
  </span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre>
</div>
<p>หลังบันทึก Cursor จะแสดง &#8220;Meta Developer Tools&#8221; ในหน้า Tools &amp; Integrations พร้อมปุ่ม <strong>Needs login</strong> ให้คลิกเพื่อเริ่ม OAuth flow [1]</p>
<h2>
<p>  สิทธิ์ Read กับ Manage ต่างกันอย่างไร<br />
</p></h2>
<p>Meta แบ่งสิทธิ์เป็น 2 ระดับ ตั้งค่าได้ต่อแอปผ่าน <strong>Settings &gt; Business Integrations</strong> บน Facebook [1]</p>
<div class="table-wrapper-paragraph">
<table>
<thead>
<tr>
<th>Scope</th>
<th>สิ่งที่ทำได้</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>Read</strong></td>
<td>อ่าน app config, App Review status, compliance status, API usage, webhook topics/subscriptions</td>
</tr>
<tr>
<td><strong>Manage</strong></td>
<td>ทุกอย่างใน Read บวกกับสร้าง/อัปเดต/ลบ webhook subscriptions</td>
</tr>
</tbody>
</table>
</div>
<p>คำแนะนำของผมคือ เริ่มจาก <strong>Read scope ก่อน</strong> อย่างน้อย 1-2 สัปดาห์ จนมั่นใจว่า agent อ่านข้อมูลได้ถูกต้อง ค่อยเปิด Manage สำหรับงาน webhook เฉพาะที่จำเป็นจริงๆ</p>
<h2>
<p>  คนทำโฆษณาเอาไปใช้อะไรได้บ้าง<br />
</p></h2>
<p>ถึง MCP นี้ชื่อ Developer Tools แต่คนทำโฆษณาที่มีแอปของตัวเองบน Meta หรือทีมที่ดูแล Conversion API (CAPI), Pixel, Lead Ads สามารถเอาไปใช้ได้หลายอย่าง</p>
<p>ด้านล่างนี้คือห้าการใช้งานที่เห็นผลเร็วที่สุด</p>
<h3>
<p>  1. ตรวจสุขภาพแอปและ API ก่อนรันแคมเปญใหญ่<br />
</p></h3>
<p>ก่อนที่จะเท budget หนักใน campaign ใหญ่ ให้ agent เช็ค <code>devtools_api_usage</code> ว่าแอปใกล้ชน rate limit หรือไม่ API ไหนที่ใช้อยู่กำลังจะเลิกใช้หรือเปล่า และ <code>devtools_compliance</code> ว่ามี violation ค้างอยู่หรือไม่</p>
<p>เคยมีหลายครั้งที่ campaign ดันไม่ได้เพราะ API token ถูกจำกัดหรือแอปติด compliance action ซึ่งถ้าเช็คก่อนได้จะประหยัดเวลาไปได้เป็นชั่วโมง</p>
<h3>
<p>  2. ติดตาม App Review สำหรับสิทธิ์พิเศษ<br />
</p></h3>
<p>บางฟีเจอร์ของ Meta เช่น WhatsApp Business API, Lead Access หรือ Advanced Matching ต้องผ่าน App Review ก่อน ใช้ <code>devtools_app_review</code> ถาม agent ได้ว่าแอปของคุณได้รับการอนุมัติสิทธิ์ไหนบ้างแล้ว ยังขาดอะไร ต้อง submit อะไรเพิ่ม</p>
<h3>
<p>  3. จัดการ webhook สำหรับ Lead Ads หรือ Messenger<br />
</p></h3>
<p>ถ้าคุณรับ lead จาก Facebook Lead Ads แล้วส่งเข้า CRM ผ่าน webhook การ subscribe topic ผิดหรือ callback URL มีปัญหาจะทำให้ lead หายได้ ด้วย <code>devtools_webhook_list</code>, <code>devtools_webhook_manage</code> และ <code>devtools_webhook_test</code> คุณสามารถให้ agent ช่วยตรวจสอบและส่ง test event เพื่อยืนยันว่าทุกอย่างทำงานจริง [1]</p>
<h3>
<p>  4. ติดตาม changelog ของ Marketing API<br />
</p></h3>
<p>Meta เปลี่ยนแปลง Marketing API บ่อย บางครั้ง field ที่ใช้ดึง insights หายไปหรือเปลี่ยนชื่อ ใช้ <code>devtools_api_changelog</code> ให้ agent ดึง RSS feed แล้วสรุปว่ามีอะไรเปลี่ยนแปลงในช่วง 7-14 วันที่ผ่านมา ก่อนที่รายงานประจำสัปดาห์จะพัง</p>
<h3>
<p>  5. ค้นหาเอกสาร official ด้วยคำถามธรรมชาติ<br />
</p></h3>
<p>แทนที่จะเปิด Meta for Developers แล้วค้นหาเอง ให้ agent ใช้ <code>devtools_discovery</code> ถามว่า &#8220;วิธีตั้งค่า Conversions API สำหรับ e-commerce ต้องทำอย่างไร&#8221; หรือ &#8220;webhook ของ Messenger Platform ต้อง verify signature ยังไง&#8221; agent จะค้นหาเอกสาร official และสรุปขั้นตอนให้ [1]</p>
<h2>
<p>  ข้อจำกัดที่ควรรู้ก่อนใช้<br />
</p></h2>
<p>Developer Tools MCP ยังไม่ใช่เครื่องมือจัดการโฆษณาโดยตรง คือมันยังไม่สามารถ:</p>
<ul>
<li>สร้างหรือแก้ไข campaign, ad set, ad</li>
<li>เปลี่ยน budget หรือ bidding strategy</li>
<li>ดึง insights ของ ad account โดยตรง (ต้องใช้ Marketing API ผ่านทางอื่น)</li>
<li>อัปโหลด creative asset</li>
</ul>
<p>หากต้องการทำสิ่งเหล่านี้ผ่าน AI agent ต้องใช้ <strong>Meta Ads CLI</strong> ซึ่งเปิดตัวเมื่อเดือนเมษายน 2026 และต้องใช้ร่วมกับ MCP server หรือ agentic coding tool เช่น Claude Code/Codex [2]</p>
<p>MCP ของ Meta ยังอยู่ในช่วง Beta อินเทอร์เฟซและชุดเครื่องมืออาจเปลี่ยนแปลง ดังนั้นไม่ควรพึ่งพาใน workflow ที่สำคัญมากจนไม่มีแผนสำรอง [1]</p>
<h2>
<p>  สรุป<br />
</p></h2>
<p>Facebook Developer Tools MCP คือจุดเชื่อมต่อใหม่ที่ให้ AI agent เข้าถึงข้อมูลทางเทคนิคของ Meta app ได้จากที่เดียว สำหรับคนทำโฆษณา ประโยชน์ที่ชัดที่สุดคือ <strong>การตรวจสุขภาพแอป, ติดตาม App Review, จัดการ webhook และติดตาม changelog</strong> ซึ่งเป็นงานรองที่กินเวลามากแต่กระทบ campaign ได้จริง</p>
<p>จงจำไว้ว่า มันยังเป็น Beta ยังเขียนได้แค่ webhook subscription และยังไม่ใช่ตัวแทน Ads Manager ถ้าต้องการจัดการ campaign โดยตรงต้องใช้ Meta Ads CLI แทน [2]</p>
<h2>
<p>  แหล่งอ้างอิง<br />
</p></h2>
<p>[1] Meta for Developers. &#8220;Developer Tools MCP.&#8221; Official documentation, updated June 12, 2026. <a href="https://developers.facebook.com/documentation/mcp/devtools-mcp" rel="noopener noreferrer">https://developers.facebook.com/documentation/mcp/devtools-mcp</a></p>
<p>[2] Baker Team. &#8220;Meta Ads CLI and MCP Explained: How It Changes Your Campaign Strategy in 2026.&#8221; withbaker.com, April 30, 2026. <a href="https://withbaker.com/blog/meta-ads-cli-mcp-2026" rel="noopener noreferrer">https://withbaker.com/blog/meta-ads-cli-mcp-2026</a></p>
<p><em>ชอบบทความนี้? ติดตาม Nokka บน <a href="https://dev.to/sarantoon">dev.to/sarantoon</a> เพื่ออ่านเทคนิค digital marketing, AI และ automation ได้ที่นี่ — หรือแชร์คำถามไว้ใต้โพสต์นี้ได้เลย</em></p>]]></content:encoded>
					
					<wfw:commentRss>https://codango.com/facebook-developer-tools-mcp-%e0%b8%84%e0%b8%b7%e0%b8%ad%e0%b8%ad%e0%b8%b0%e0%b9%84%e0%b8%a3-%e0%b8%84%e0%b8%b9%e0%b9%88%e0%b8%a1%e0%b8%b7%e0%b8%ad-meta-mcp-%e0%b8%aa%e0%b8%b3%e0%b8%ab/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>You Don&#8217;t Need a CSS Framework in 2026 — the Platform Caught Up</title>
		<link>https://codango.com/you-dont-need-a-css-framework-in-2026-the-platform-caught-up/</link>
					<comments>https://codango.com/you-dont-need-a-css-framework-in-2026-the-platform-caught-up/#respond</comments>
		
		<dc:creator><![CDATA[Codango Admin]]></dc:creator>
		<pubDate>Mon, 03 Aug 2026 11:07:54 +0000</pubDate>
				<category><![CDATA[Codango® Blog]]></category>
		<guid isPermaLink="false">https://codango.com/you-dont-need-a-css-framework-in-2026-the-platform-caught-up/</guid>

					<description><![CDATA[Here&#8217;s the take: if you&#8217;re reaching for a CSS framework in 2026 out of habit rather than a specific, named requirement, you&#8217;re probably shipping more code than the problem needs. <a class="more-link" href="https://codango.com/you-dont-need-a-css-framework-in-2026-the-platform-caught-up/">Continue reading <span class="screen-reader-text">  You Don&#8217;t Need a CSS Framework in 2026 — the Platform Caught Up</span><span class="meta-nav">&#8594;</span></a>]]></description>
										<content:encoded><![CDATA[<p>Here&#8217;s the take: if you&#8217;re reaching for a CSS framework in 2026 out of habit rather than a specific, named requirement, you&#8217;re probably shipping more code than the problem needs. Not because frameworks got worse — because the platform quietly closed almost every gap they existed to patch.</p>
<p>This isn&#8217;t &#8220;frameworks are bad.&#8221; Bootstrap, Tailwind, and friends were the right call for most of the last decade, because CSS genuinely couldn&#8217;t do the things they made easy. That&#8217;s the part that&#8217;s changed. Grid systems, component-level breakpoints, conditional styling, specificity control, color theming — these used to require a framework or a preprocessor because CSS itself had no answer. It has answers now, and most teams haven&#8217;t gone back to check.</p>
<h2>
<p>  What frameworks were actually solving<br />
</p></h2>
<p>To make the case fairly, it&#8217;s worth naming what problem each piece of a typical framework was actually there for — because &#8220;just use vanilla CSS&#8221; has been bad advice for a decade, and I want to be specific about why it stopped being bad advice rather than just asserting it.</p>
<div class="table-wrapper-paragraph">
<table>
<thead>
<tr>
<th>What you reached for</th>
<th>What it was actually working around</th>
</tr>
</thead>
<tbody>
<tr>
<td>A 12-column grid system</td>
<td>CSS had no native concept of &#8220;columns that adapt to available space&#8221;</td>
</tr>
<tr>
<td>Sass nesting and variables</td>
<td>Flat CSS selectors got repetitive fast; no native scoping</td>
</tr>
<tr>
<td>Utility classes (<code>.mt-4</code>, <code>.flex</code>, <code>.text-center</code>)</td>
<td>Writing semantic class names for every one-off style was slow, and specificity was hard to manage at scale</td>
</tr>
<tr>
<td>A breakpoint mixin (<code>@include md { ... }</code>)</td>
<td>Media queries only ever knew the viewport, never the component&#8217;s actual available space</td>
</tr>
<tr>
<td>A theme/color system</td>
<td>No native way to compute color variants (tints, shades, mixes) without precompiling every value</td>
</tr>
<tr>
<td>
<code>.card:hover .card-title { ... }</code>-style JS toggles</td>
<td>CSS couldn&#8217;t style a parent based on what&#8217;s inside it</td>
</tr>
</tbody>
</table>
</div>
<p>Every row in that table used to be true. None of them are anymore.</p>
<h2>
<p>  Grid and subgrid replace the 12-column system<br />
</p></h2>
<p>CSS Grid has been supported everywhere for years, but the part that actually finished the job — subgrid — is what let go of the last reason to reach for a prebuilt column system. A grid of cards where each card&#8217;s internal rows (image, title, meta, button) align across the whole row, not just within each card, used to need real work. Now it&#8217;s a couple of lines:
</p>
<div class="highlight js-code-highlight">
<pre class="highlight css"><code><span class="nc">.card-grid</span> <span class="p">{</span>
  <span class="nl">display</span><span class="p">:</span> <span class="n">grid</span><span class="p">;</span>
  <span class="py">grid-template-columns</span><span class="p">:</span> <span class="nb">repeat</span><span class="p">(</span><span class="n">auto-fit</span><span class="p">,</span> <span class="n">minmax</span><span class="p">(</span><span class="m">240px</span><span class="p">,</span> <span class="m">1</span><span class="n">fr</span><span class="p">));</span>
  <span class="py">gap</span><span class="p">:</span> <span class="m">1.5rem</span><span class="p">;</span>
<span class="p">}</span>

<span class="nc">.card</span> <span class="p">{</span>
  <span class="nl">display</span><span class="p">:</span> <span class="n">grid</span><span class="p">;</span>
  <span class="py">grid-template-rows</span><span class="p">:</span> <span class="n">subgrid</span><span class="p">;</span>
  <span class="nl">grid-row</span><span class="p">:</span> <span class="n">span</span> <span class="m">4</span><span class="p">;</span>
<span class="p">}</span>
</code></pre>
</div>
<p>That single <code>auto-fit</code>/<code>minmax()</code> line does the job of an entire responsive column system — it reflows the number of columns based on available width with zero media queries and zero JavaScript resize listeners.</p>
<h2>
<p>  Container queries replace component-level breakpoint hacks<br />
</p></h2>
<p>This is the one that actually changes how you architect CSS, not just how you write it. A media query only ever knows the viewport. It has no idea if your card is in a wide main column or a narrow sidebar — which is exactly why component libraries ended up full of <code>.card--compact</code> modifier classes that had to be manually applied wherever a component landed in a tight space.</p>
<p>Container queries let the component ask about its own available space instead:
</p>
<div class="highlight js-code-highlight">
<pre class="highlight css"><code><span class="nc">.card</span> <span class="p">{</span>
  <span class="py">container-type</span><span class="p">:</span> <span class="n">inline-size</span><span class="p">;</span>
<span class="p">}</span>

<span class="k">@container</span> <span class="p">(</span><span class="n">min-width</span><span class="p">:</span> <span class="m">380px</span><span class="p">)</span> <span class="p">{</span>
  <span class="nc">.card__body</span> <span class="p">{</span>
    <span class="nl">display</span><span class="p">:</span> <span class="n">grid</span><span class="p">;</span>
    <span class="py">grid-template-columns</span><span class="p">:</span> <span class="m">120px</span> <span class="m">1</span><span class="n">fr</span><span class="p">;</span>
    <span class="py">gap</span><span class="p">:</span> <span class="m">1rem</span><span class="p">;</span>
  <span class="p">}</span>
<span class="p">}</span>
</code></pre>
</div>
<p>Drop that same card into a sidebar, a modal, or a full-width section, and it adapts to whichever container it&#8217;s actually sitting in — no modifier class, no JavaScript <code>ResizeObserver</code>, no knowledge of the page layout required at the component level at all. This is the single biggest reason a component library&#8217;s &#8220;make it responsive&#8221; problem doesn&#8217;t need a framework&#8217;s help anymore.</p>
<h2>
<p>  <code>:has()</code> replaces a surprising amount of your JavaScript<br />
</p></h2>
<p><code>:has()</code> lets a parent&#8217;s style depend on what&#8217;s inside it — something CSS flatly couldn&#8217;t do before, which is why so many small interactions ended up as JavaScript that toggled a class on a parent element whenever a child changed state.
</p>
<div class="highlight js-code-highlight">
<pre class="highlight css"><code><span class="c">/* Highlight a form field's wrapper only when it contains an invalid input */</span>
<span class="nc">.field</span><span class="nd">:has</span><span class="o">(</span><span class="nd">:invalid</span><span class="o">)</span> <span class="p">{</span>
  <span class="nl">border-color</span><span class="p">:</span> <span class="n">var</span><span class="p">(</span><span class="n">--color-danger</span><span class="p">);</span>
<span class="p">}</span>

<span class="c">/* Style a card differently if it happens to contain an image */</span>
<span class="nc">.card</span><span class="nd">:has</span><span class="o">(</span><span class="nt">img</span><span class="o">)</span> <span class="p">{</span>
  <span class="py">grid-template-rows</span><span class="p">:</span> <span class="nb">auto</span> <span class="m">1</span><span class="n">fr</span> <span class="nb">auto</span><span class="p">;</span>
<span class="p">}</span>

<span class="c">/* Style a fieldset's label based on a checkbox elsewhere in the same fieldset */</span>
<span class="nt">fieldset</span><span class="nd">:has</span><span class="o">(</span><span class="nt">input</span><span class="o">[</span><span class="nt">type</span><span class="o">=</span><span class="s1">"checkbox"</span><span class="o">]</span><span class="nd">:checked</span><span class="o">)</span> <span class="nt">legend</span> <span class="p">{</span>
  <span class="nl">color</span><span class="p">:</span> <span class="n">var</span><span class="p">(</span><span class="n">--color-accent</span><span class="p">);</span>
<span class="p">}</span>
</code></pre>
</div>
<p>Multiply that pattern across a real form or a component with a dozen conditional states, and <code>:has()</code> is quietly deleting hundreds of lines of state-toggling JavaScript that never needed to exist once CSS could ask the question directly.</p>
<h2>
<p>  Native nesting replaces the entire reason most teams installed Sass<br />
</p></h2>
<p>If a project only used Sass for nesting and variables — which, honestly, describes most projects — there&#8217;s no longer a reason to have a build step for it:
</p>
<div class="highlight js-code-highlight">
<pre class="highlight css"><code><span class="nc">.nav</span> <span class="p">{</span>
  <span class="nl">display</span><span class="p">:</span> <span class="n">flex</span><span class="p">;</span>

  <span class="err">ul</span> <span class="err">{</span>
    <span class="nl">list-style</span><span class="p">:</span> <span class="nb">none</span><span class="p">;</span>
    <span class="nl">display</span><span class="p">:</span> <span class="n">flex</span><span class="p">;</span>
    <span class="py">gap</span><span class="p">:</span> <span class="m">1rem</span><span class="p">;</span>

    <span class="err">li</span> <span class="err">a</span> <span class="err">{</span>
      <span class="nl">text-decoration</span><span class="p">:</span> <span class="nb">none</span><span class="p">;</span>
      <span class="nl">color</span><span class="p">:</span> <span class="n">var</span><span class="p">(</span><span class="n">--color-text</span><span class="p">);</span>

      <span class="err">&amp;:hover</span> <span class="err">{</span>
        <span class="nl">color</span><span class="p">:</span> <span class="n">var</span><span class="p">(</span><span class="n">--color-accent</span><span class="p">);</span>
      <span class="p">}</span>
    <span class="err">}</span>
  <span class="err">}</span>
<span class="err">}</span>
</code></pre>
</div>
<p>That&#8217;s not a preprocessor output — it&#8217;s what ships to the browser, unmodified. The CSS file structure mirrors the HTML structure, which was the actual appeal of Sass nesting the whole time. If your team&#8217;s Sass usage is genuinely just this, the build step is now pure overhead: one more tool in the pipeline, one more thing that can break CI, for a feature the browser already has.</p>
<h2>
<p>  Cascade layers replace the specificity war utility classes were built to dodge<br />
</p></h2>
<p>This is the one people miss. A big part of why utility-first frameworks feel necessary is that they sidestep CSS specificity entirely — every class has the same low specificity, so the last one written always wins, and you never fight <code>.header .nav ul li a</code> versus <code>.nav-link</code>.</p>
<p><code>@layer</code> gives you that same guarantee without giving up semantic class names:
</p>
<div class="highlight js-code-highlight">
<pre class="highlight css"><code><span class="k">@layer</span> <span class="n">reset</span><span class="p">,</span> <span class="n">base</span><span class="p">,</span> <span class="n">components</span><span class="p">,</span> <span class="n">utilities</span><span class="p">;</span>

<span class="k">@layer</span> <span class="n">base</span> <span class="p">{</span>
  <span class="nt">a</span> <span class="p">{</span> <span class="nl">color</span><span class="p">:</span> <span class="n">var</span><span class="p">(</span><span class="n">--color-text</span><span class="p">);</span> <span class="p">}</span>
<span class="p">}</span>

<span class="k">@layer</span> <span class="n">components</span> <span class="p">{</span>
  <span class="nc">.card</span> <span class="nt">a</span> <span class="p">{</span> <span class="nl">color</span><span class="p">:</span> <span class="n">var</span><span class="p">(</span><span class="n">--color-accent</span><span class="p">);</span> <span class="p">}</span>
<span class="p">}</span>

<span class="k">@layer</span> <span class="n">utilities</span> <span class="p">{</span>
  <span class="nc">.text-muted</span> <span class="p">{</span> <span class="nl">color</span><span class="p">:</span> <span class="n">var</span><span class="p">(</span><span class="n">--color-muted</span><span class="p">);</span> <span class="p">}</span>
<span class="p">}</span>
</code></pre>
</div>
<p>Layers are resolved in the order they&#8217;re declared, regardless of selector specificity or source order — a single class in <code>utilities</code> will always beat a deeply nested selector in <code>components</code>, on purpose, without an <code>!important</code> in sight. This was the actual argument for switching to utility classes: predictable override order. Cascade layers hand you that guarantee directly, so you can keep writing <code>.card-title</code> instead of <code>.text-lg.font-bold.mb-2</code> and still win the specificity fight when you need to.</p>
<h2>
<p>  <code>color-mix()</code> and <code>oklch()</code> replace the theme-generation layer<br />
</p></h2>
<p>Preprocessor color functions (<code>darken()</code>, <code>lighten()</code>, <code>mix()</code>) existed because plain CSS could only store a color, not compute a new one from it. That&#8217;s gone too:
</p>
<div class="highlight js-code-highlight">
<pre class="highlight css"><code><span class="nd">:root</span> <span class="p">{</span>
  <span class="py">--color-brand</span><span class="p">:</span> <span class="n">oklch</span><span class="p">(</span><span class="m">58%</span> <span class="m">0.18</span> <span class="m">250</span><span class="p">);</span>
<span class="p">}</span>

<span class="nc">.button</span><span class="nd">:hover</span> <span class="p">{</span>
  <span class="nl">background</span><span class="p">:</span> <span class="n">color-mix</span><span class="p">(</span><span class="n">in</span> <span class="n">oklch</span><span class="p">,</span> <span class="n">var</span><span class="p">(</span><span class="n">--color-brand</span><span class="p">),</span> <span class="no">white</span> <span class="m">15%</span><span class="p">);</span>
<span class="p">}</span>

<span class="nc">.button</span><span class="nd">:active</span> <span class="p">{</span>
  <span class="nl">background</span><span class="p">:</span> <span class="n">color-mix</span><span class="p">(</span><span class="n">in</span> <span class="n">oklch</span><span class="p">,</span> <span class="n">var</span><span class="p">(</span><span class="n">--color-brand</span><span class="p">),</span> <span class="no">black</span> <span class="m">15%</span><span class="p">);</span>
<span class="p">}</span>
</code></pre>
</div>
<p><code>oklch()</code> also happens to interpolate more predictably than hex or RGB — mixing two OKLCH colors doesn&#8217;t produce the muddy, desaturated middle tones you get from mixing in RGB space, which was always the annoying part of building a tint/shade scale by hand.</p>
<h2>
<p>  <code>clamp()</code> replaces the typography-scale mixin<br />
</p></h2>
<p>This one you may already be doing — it&#8217;s the same technique from fluid typography with <code>clamp()</code>, but it&#8217;s worth restating as part of the bigger case: a whole category of &#8220;responsive spacing/type scale&#8221; mixins collapses into single-property declarations:
</p>
<div class="highlight js-code-highlight">
<pre class="highlight css"><code><span class="nt">h1</span> <span class="p">{</span>
  <span class="nl">font-size</span><span class="p">:</span> <span class="n">clamp</span><span class="p">(</span><span class="m">2rem</span><span class="p">,</span> <span class="m">1.2rem</span> <span class="err">+</span> <span class="m">3vw</span><span class="p">,</span> <span class="m">3.5rem</span><span class="p">);</span>
<span class="p">}</span>

<span class="nc">.section</span> <span class="p">{</span>
  <span class="py">padding-block</span><span class="p">:</span> <span class="n">clamp</span><span class="p">(</span><span class="m">2rem</span><span class="p">,</span> <span class="m">5vw</span><span class="p">,</span> <span class="m">6rem</span><span class="p">);</span>
<span class="p">}</span>
</code></pre>
</div>
<p>No breakpoint table, no mixin, no JavaScript recalculating on resize — the value scales continuously with the viewport and clamps at both ends.</p>
<h2>
<p>  Where a framework still earns its keep<br />
</p></h2>
<p>None of this means frameworks are pointless now — it means their remaining justification is narrower and more specific than &#8220;CSS is hard,&#8221; and worth naming honestly:</p>
<ul>
<li>
<strong>Prebuilt, accessible interactive components.</strong> A native <code>&lt;dialog&gt;</code> or <code>&lt;details&gt;</code> handles a lot, but a fully-featured date picker, combobox, or rich data table with proper ARIA behavior is still real work that a component library has already done correctly.</li>
<li>
<strong>Enforced consistency across a large team.</strong> A design system with strict, enforced tokens (via a utility framework&#8217;s config) can genuinely prevent drift better than a style guide people are supposed to remember to follow — this is a people problem as much as a technical one, and utility classes are a decent people-problem solution.</li>
<li>
<strong>Faster prototyping under real time pressure.</strong> Reaching for known utility classes is still faster than making design decisions from scratch when you&#8217;re throwing together an internal tool by Friday.</li>
<li>
<strong>Legacy browser support.</strong> If your analytics show a meaningful slice of traffic on genuinely old browsers, several of the features above aren&#8217;t available to them, and a framework&#8217;s fallback behavior earns its cost.</li>
</ul>
<p>If your actual situation is one of these, that&#8217;s a real reason — not a rationalization. The point isn&#8217;t &#8220;never use a framework.&#8221; It&#8217;s that &#8220;we&#8217;ve always used one&#8221; stopped being a technical reason sometime in the last two years, and it&#8217;s worth checking which category your project is actually in.</p>
<h2>
<p>  A worked example: the thing frameworks were built for<br />
</p></h2>
<p>Here&#8217;s the classic case — a responsive card grid where cards need internal layout that adapts to available space, not just viewport width — built with nothing but the platform:
</p>
<div class="highlight js-code-highlight">
<pre class="highlight html"><code><span class="nt">&lt;section</span> <span class="na">class=</span><span class="s">"card-grid"</span><span class="nt">&gt;</span>
  <span class="nt">&lt;article</span> <span class="na">class=</span><span class="s">"card"</span><span class="nt">&gt;</span>
    <span class="nt">&lt;img</span> <span class="na">src=</span><span class="s">"thumb.jpg"</span> <span class="na">alt=</span><span class="s">""</span> <span class="nt">/&gt;</span>
    <span class="nt">&lt;div</span> <span class="na">class=</span><span class="s">"card__body"</span><span class="nt">&gt;</span>
      <span class="nt">&lt;h3&gt;</span>Card title<span class="nt">&lt;/h3&gt;</span>
      <span class="nt">&lt;p&gt;</span>Supporting copy that might run long or short.<span class="nt">&lt;/p&gt;</span>
    <span class="nt">&lt;/div&gt;</span>
  <span class="nt">&lt;/article&gt;</span>
  <span class="c">&lt;!-- more cards --&gt;</span>
<span class="nt">&lt;/section&gt;</span>
</code></pre>
</div>
<div class="highlight js-code-highlight">
<pre class="highlight css"><code><span class="nc">.card-grid</span> <span class="p">{</span>
  <span class="nl">display</span><span class="p">:</span> <span class="n">grid</span><span class="p">;</span>
  <span class="py">grid-template-columns</span><span class="p">:</span> <span class="nb">repeat</span><span class="p">(</span><span class="n">auto-fit</span><span class="p">,</span> <span class="n">minmax</span><span class="p">(</span><span class="m">240px</span><span class="p">,</span> <span class="m">1</span><span class="n">fr</span><span class="p">));</span>
  <span class="py">gap</span><span class="p">:</span> <span class="m">1.5rem</span><span class="p">;</span>
<span class="p">}</span>

<span class="nc">.card</span> <span class="p">{</span>
  <span class="py">container-type</span><span class="p">:</span> <span class="n">inline-size</span><span class="p">;</span>
  <span class="nl">border</span><span class="p">:</span> <span class="m">1px</span> <span class="nb">solid</span> <span class="n">var</span><span class="p">(</span><span class="n">--color-border</span><span class="p">);</span>
  <span class="nl">border-radius</span><span class="p">:</span> <span class="m">0.75rem</span><span class="p">;</span>
  <span class="nl">overflow</span><span class="p">:</span> <span class="n">clip</span><span class="p">;</span>

  <span class="err">img</span> <span class="err">{</span>
    <span class="nl">width</span><span class="p">:</span> <span class="m">100%</span><span class="p">;</span>
    <span class="py">aspect-ratio</span><span class="p">:</span> <span class="m">16</span> <span class="p">/</span> <span class="m">9</span><span class="p">;</span>
    <span class="nl">object-fit</span><span class="p">:</span> <span class="n">cover</span><span class="p">;</span>
  <span class="p">}</span>

  <span class="o">&amp;</span><span class="nd">:has</span><span class="o">(</span><span class="nt">img</span><span class="o">)</span> <span class="nc">.card__body</span> <span class="p">{</span>
    <span class="py">padding-block-start</span><span class="p">:</span> <span class="m">1rem</span><span class="p">;</span>
  <span class="p">}</span>

  <span class="o">&amp;</span><span class="nt">__body</span> <span class="p">{</span>
    <span class="nl">padding</span><span class="p">:</span> <span class="m">1rem</span> <span class="m">1.25rem</span><span class="p">;</span>

    <span class="err">h3</span> <span class="err">{</span>
      <span class="nl">font-size</span><span class="p">:</span> <span class="n">clamp</span><span class="p">(</span><span class="m">1.1rem</span><span class="p">,</span> <span class="m">1rem</span> <span class="err">+</span> <span class="m">0.5vw</span><span class="p">,</span> <span class="m">1.35rem</span><span class="p">);</span>
      <span class="nl">margin</span><span class="p">:</span> <span class="m">0</span> <span class="m">0</span> <span class="m">0.5rem</span><span class="p">;</span>
    <span class="p">}</span>
  <span class="err">}</span>
<span class="err">}</span>

<span class="k">@container</span> <span class="p">(</span><span class="n">min-width</span><span class="p">:</span> <span class="m">360px</span><span class="p">)</span> <span class="p">{</span>
  <span class="nc">.card__body</span> <span class="p">{</span>
    <span class="nl">display</span><span class="p">:</span> <span class="n">grid</span><span class="p">;</span>
    <span class="py">grid-template-columns</span><span class="p">:</span> <span class="m">1</span><span class="n">fr</span> <span class="nb">auto</span><span class="p">;</span>
    <span class="nl">align-items</span><span class="p">:</span> <span class="n">start</span><span class="p">;</span>
  <span class="p">}</span>
<span class="p">}</span>
</code></pre>
</div>
<p>No grid framework, no Sass, no JavaScript, and it responds to its own container rather than the viewport. Five years ago this genuinely required either a framework or a pile of custom code to get right; now it&#8217;s the direct, obvious way to write it.</p>
<h2>
<p>  Why the habit outlasts the reason<br />
</p></h2>
<p>If none of this is news to you, that&#8217;s exactly the point — most of these features have been broadly supported for a year or more, and adoption still lags the capability by a wide margin. That&#8217;s not a technical gap anymore, it&#8217;s an inertia gap: framework choice gets made once at a project&#8217;s start, rarely revisited, and &#8220;the team already knows Tailwind&#8221; is a perfectly good reason to keep using it on an existing codebase. It&#8217;s a much weaker reason to reach for it by default on a new one.</p>
<p>The honest version of this take isn&#8217;t &#8220;rip out your framework.&#8221; It&#8217;s: the next time you start a project and install one out of reflex, spend fifteen minutes checking whether the specific problem you&#8217;re reaching for it to solve is actually still a problem. For a lot of projects in 2026, the answer is genuinely no.</p>
<p>Am I wrong about this? What&#8217;s the actual dealbreaker keeping your team on a framework right now — I&#8217;d genuinely like to know what I&#8217;m missing.</p>
<p><strong>At ArtClick, we build fast, scalable WordPress websites, company websites and custom web systems that balance design, performance and long-term maintainability.</strong> Whether you&#8217;re starting from scratch or improving an existing platform, we&#8217;d love to help.</p>
<p><a href="https://artclickdev.com/" rel="noopener noreferrer">https://artclickdev.com/</a></p>]]></content:encoded>
					
					<wfw:commentRss>https://codango.com/you-dont-need-a-css-framework-in-2026-the-platform-caught-up/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
	</channel>
</rss>
