Sticking Feathers Up Your Butt Doesn’t Make You a Chicken

Nor does declaring your company an ‘AI Company’.

Or, in the words of Ludacris:

James: Shit man, I got myself a little internet business myself

Willy Beamen: Oh yeah? What’s that?

James: cocks gun Robbin’, muthafucka

Customers don’t care. Investors do. AWS does (or whoever your ‘partner’ hyperscaler is). Anthropic does (J/K they don’t care). I guess they all give you money in one way or another, some in exchange for labor, some in exchange for your soul.

This is very evident when I look at the positioning of some technology consulting companies (and no, I am not referring to a former employer). It’s a problem with the industry as a whole, and AWS did a poor job of articulating it to partners (my experience was an obsessive, not in the customer obsessed way, of consulting offers, or I mean solutions).

What do customers think? Customers don’t care (I think I said that already).

What do they want?

1) Value

Customers are fundamentally seeking value. This means they want a product or service that fulfills a genuine need, and they want it delivered in a way that either costs them less or generates more value than alternatives. For instance, if a company can automate a process that saves a client significant time and money, that’s value. It’s not about the complexity of the technology behind it, but rather the tangible benefits it brings to the customer’s bottom line.

2) Consistency

Consistency is crucial. Customers want to know they can rely on a company to show up and perform every day, no matter what. This includes being available, dependable, and delivering a high-quality service whether the team is dealing with personal challenges or not. Consistency breeds trust, and trust is the cornerstone of long-term customer relationships.

3) Safety and Performance

Customers also look for safety in their business partners. This can be demonstrated through a company’s past performance and reliability. They want assurance that the solutions they adopt won’t fail them and that their peers are working with proven, reliable technologies. Moreover, customers want to feel that they are not missing out on industry trends and best practices. This is why seeing what other successful businesses are using or avoiding can be a significant factor in their decision-making process.

4) Vision and Innovation

While not always a primary concern, some customers are drawn to a company’s vision and innovation. They might be excited about the latest technology, like the release of a new version of a software or platform, and want to leverage it to stay ahead. Conversely, other customers might feel intimidated by rapid technological changes and prefer to avoid them unless guided by an expert. For these customers, a company’s vision and ability to navigate complex technologies safely are key differentiators.

The Bottom Line

So, while touting your ability to use the latest AI technology might impress some, it doesn’t necessarily make you a hero. What truly matters is how you use that technology to deliver visionary solutions consistently, that create value and don’t expose your customers to risk. When you prevent issues before they arise and ensure your services are reliable, that’s when you build the trust and loyalty that lead to lasting business success.

Context:Title: Sticking Feathers Up Your Butt Doesn’t Make You a ChickenTags: AI Company, Customer Value, Value Creation, Customer Loyalty, Consistency, Safety, VisionDescription: Discover why labeling your company as an ‘AI Company’ doesn’t resonate with customers; learn what truly matters to them, from value and consistency to safety and vision, in this insightful blog post. Explore how genuine value creation and reliable service, not flashy labels, drive customer loyalty and business success.

Most Cybersecurity Tools Are Solving the Wrong Problem

In one published case study of an industrial intrusion detection deployment, the system flagged more than 27,000 events as potentially malicious in a single observation window. Of those, 76 turned out to be real threats. The remaining 26,924 were noise. The false positive rate was 99.7 percent.

That number is not an outlier. It is the part of modern cybersecurity that almost no vendor will put in a sales deck, because the entire industry is built on telling customers they need to see more, not less. The 2025 SANS Detection and Response Survey found that 73 percent of security teams now rank false positives as their single biggest detection challenge. Microsoft and Omdia’s State of the SOC 2026 work puts the average false positive rate across modern security operations at 46 percent. Praetorian’s analysis goes higher: most security tools sit between 50 and 80 percent. A typical security operations center generates around 960 alerts per day. Roughly 63 percent of them are never investigated at all.

I am seventeen, and I have been building a network monitoring tool called Sentinel Security. The closer I get to a production system, the more I am convinced that the hardest engineering problem in this field is not catching attacks. It is deciding what to ignore. And the entire industry has been arguing about the wrong half of that question for two decades.

The math that does not add up
The pitch from almost every cybersecurity vendor for the last fifteen years has been visibility. More sensors. More logs. More data sources. More integrations. More dashboards. More alerts. The reasoning sounds correct on first hearing: a missed attack is worse than a false alarm, so the safe move feels like catching everything.

Then you look at the outcomes.

The 2025 Verizon Data Breach Investigations Report analyzed 22,052 security incidents and 12,195 confirmed breaches across 139 countries. Ransomware appeared in 44 percent of all breaches, up from 32 percent the year before. IBM’s 2025 Cost of a Data Breach Report put the average global breach cost at $4.44 million and the average lifecycle at 241 days from initial compromise to containment. That works out to roughly $18,400 in damage per day of undetected access. The United States average hit an all-time high of $10.22 million per incident.

So the picture is this. The industry has spent more on detection tooling than at any point in its history. Breach costs are at record highs. And the analysts running these tools say the single biggest thing standing between them and their job is noise from the tools themselves. Something in that equation is broken.

My argument is that the broken part is not the tools. It is the design philosophy underneath them.

How detection works

There are two fundamental ways a security tool decides something is suspicious, and almost every product on the market is some blend of the two.

The first is signature-based detection. The system looks for known indicators of compromise. A specific byte sequence in a packet. A hash matching documented malware. A DNS lookup to a domain on a blocklist. Signatures are fast, cheap to run, and produce very few false positives when they are accurate. The limit is that signatures only catch what has already been documented. A novel attack slides past untouched.

The second is behavioral detection. The system builds a baseline of what normal traffic looks like, then alerts when current activity deviates from that baseline. Behavioral detection catches things signatures miss, including zero-day attacks and novel adversary techniques. The limit is that “unusual” is a slippery word. End-of-quarter reporting is unusual. New hires are unusual. A printer pulling down a firmware update from a domain it has never contacted before is, technically, unusual. None of these are attacks. All of them trigger alerts.

The honest reality of modern detection is that the engine is rarely the problem. The engine usually works. What does not work is the assumption baked into nearly every commercial product: that more alerts is always better than fewer.

What I am learning building Sentinel

When I started writing detection logic for Sentinel, I expected the difficulty to be in the engine. I thought the hard part would be catching real adversary behavior. That turned out to be the easier problem. Open-source libraries like Scapy handle most of the heavy lifting on packet capture. Public threat intelligence feeds cover a large share of known indicators. The detection layer, in isolation, is largely a solved problem.

What is not solved is what happens between the detection layer and the human reading the result.

Three design questions have shaped how I am thinking about the current build. These are also the questions I would push hardest on if I were evaluating any commercial detection product, not just my own.

The first is how the system expresses confidence. Most detection tools today produce a flat list of alerts, all marked “suspicious,” and leave it to the analyst to figure out which ones matter. That is not a detection problem. It is a prioritization problem that the software is offloading onto a person. The version of Sentinel I am working on treats confidence as a first-class output: every alert carries a numeric score reflecting how certain the system is that an event is genuinely anomalous, not just unusual. A connection to a known command-and-control domain scores high. A new device on the network at midnight scores medium. An off-baseline DNS query from a known device with a predictable update schedule scores low. The first version of my logic treated all of these the same. The current direction is to use confidence as the gate that decides whether to alert at all.

The second is context. A workstation behaving like a workstation is normal. A workstation suddenly behaving like a server is not. Without context about a device’s role, history, and typical behavior, the same packet can look completely benign or completely hostile. The current design treats every device on the network as having a learned profile that shapes how its activity gets interpreted. This sounds straightforward in writing. It is not. The data model has gone through three iterations and is still not where I want it.

The third is what happens after a false positive. A system that fires an alert, gets told it was benign, and then keeps firing the same alert on the same pattern is not a detection system. It is a notification system that no one will read after the first week. The design direction I am working toward is what the literature calls allowlisting with context. The system learns that a specific pattern, on a specific device, in a specific behavioral envelope, is acceptable for this organization. Not as generic machine learning, which on smaller networks tends to overfit immediately. As a structured rule.

None of these are solved problems. They are open engineering questions that I expect to keep working on for a long time. The thing I would defend is that they are the right questions, and most of the industry is still answering the wrong ones.

Why scale changes the cost, not the problem

The false positive problem affects every organization, but it does not affect them in the same way.

In a large enterprise SOC, the impact is measurable in analyst capacity. At 960 alerts per day across an 8-hour shift, an analyst has roughly 90 seconds per alert. If half of those alerts are false positives, half of every analyst’s day is spent ruling out noise. Praetorian frames this as a doubling of detection capacity: an organization that reduces its false positive rate from 70 percent to 30 percent effectively doubles its real investigation throughput without hiring anyone. The financial implications scale with company size. IBM’s 2025 data shows that organizations using AI and automation extensively cut their breach lifecycle by 80 days and saved an average of $1.9 million per incident, almost entirely by detecting things faster.

In a smaller organization without a dedicated security team, the math collapses entirely. The “team” is the IT generalist, or the founder, or whoever ended up with admin access to the router. The 90-second-per-alert budget does not exist. The threshold where people stop checking the dashboard is much lower than the industry assumes.

The detection problem is universal. The cost of getting it wrong is not. That asymmetry is one of the reasons I think this is a problem worth working on across the entire spectrum of network security, not just at the enterprise end where the budgets are.

What I actually think happens next

I am not going to claim Sentinel has solved any of this. The current build is better than the first version was, but the deeper challenge is that confidence scoring needs data, contextual rules need feedback, and both of those things take time on a live network to calibrate. A system designed to get quieter as it learns has to spend its early life being loud. That tension is not a marketing problem. It is a real engineering constraint, and I do not think any tool in the category has fully figured it out yet.
Here is what I have come to believe after several months of building. The cybersecurity industry will keep selling “more visibility” because the business model demands it. Vendors get paid by alert volume, sensor count, and dashboard complexity. None of those metrics correlates with how well an organization actually detects threats. The metric that does correlate is the one almost nobody markets on: precision. A system that fires only when it has high confidence catches more real attacks than one that fires constantly, because the human on the other end of it still bothers to look.

That is the part of this problem I think is worth working on. Not louder tools. Sharper ones.

Quick check: three questions

  1. What does the article identify as the central design flaw in most modern detection products, and why does the author argue this is not actually an engine problem?

  2. What are the three design properties the article argues every alert should carry before it reaches a human?

  3. Why does the author argue that the false positive problem is universal but the cost of getting it wrong is asymmetric across organizations of different sizes?

Answers

  1. Most detection products are built around the assumption that more alerts is always better than fewer, which produces an industry-wide false positive rate between 46 and 80 percent depending on the source. The engines themselves are rarely the problem. They usually catch what they are designed to catch. The flaw is in the design philosophy underneath: a flat list of “suspicious” alerts forces a human to do the prioritization that the software should be doing, and at scale this fails predictably.

  2. Confidence (a numeric score reflecting how certain the system is that an event is genuinely anomalous, not just unusual), context (a learned profile for each device that shapes how its activity gets interpreted), and suppression behavior (whether the system actually learns from being told an alert was benign, or keeps firing on the same pattern). All three are open engineering questions, not solved problems.

  3. Every organization deals with the same false positive rates because they use the same underlying detection logic. The cost varies sharply with how many people are available to absorb the noise. A large SOC measures the impact in lost analyst capacity. A smaller organization without a dedicated security team has no capacity at all, and the threshold where people stop checking the alerts is much lower than the industry assumes.

Sources

Verizon. 2025 Data Breach Investigations Report.

https://www.verizon.com/business/resources/reports/dbir/

IBM and Ponemon Institute. Cost of a Data Breach Report 2025.

https://www.ibm.com/reports/data-breach

SANS Institute. 2025 Detection and Response Survey.
Microsoft and Omdia. State of the SOC 2026.
Praetorian. Alert Fatigue and False Positives: Why More Alerts Mean Less Security.

https://www.praetorian.com/security-101/alert-fatigue-and-false-positives/

Vectra AI. Alert Fatigue: Causes, Real Cost, and How to Fix It.

https://www.vectra.ai/topics/alert-fatigue

Sentinel Security is a network monitoring product I am currently building. If you work in detection engineering, run a security operations function, or build security tools yourself, I would be glad to compare notes. You can reach me at [email protected].

Day 9 — a 41k-follower Douyin AI-agent creator showed me what I’m missing

Day 9 — A 41k-follower Douyin AI-agent creator showed me what I’m missing

8 days into the cold-start log. Yesterday someone sent me a 5-minute Douyin video by 柱子哥 — a Chinese creator with 41k followers, 556k total likes — talking about the AI-agent gold rush. I watched it twice. He’s selling roughly the same product shape I am (packaged AI tooling) to roughly the same buyer (people who don’t want to wire up open-source themselves). His one video has 6.5k likes, 342 comments, 5.8k saves. Mine, on average, gets 10 dev.to views and zero comments.

I’m not going to pretend I can replicate his audience overnight. But I lined up his playbook against mine and the gap is concrete enough to act on.

What he does that I don’t

1. He replies to every single comment, and every reply seeds the funnel.

I scrolled the top 5 comments on that video. He’d already answered all five. Three of them ended with some variant of “the full technical stack is in the member group” — and one of them, in response to “isn’t Xiaohongshu suing over this?”, was: “different situation, but the compliance discussion is in the member group.”

Not spam. Not link-drops. Useful answers that left a hook.

I have a single dev.to comment across 11 posts. I never replied to it.

2. The video has chapter markers.

00:35 definition. 02:01 technical viability. 03:42 market analysis. 05:20 closing. Douyin’s player surfaces them as a side rail. Skimmers find the section they want, watch that, and decide whether to commit to the full 5 minutes.

dev.to doesn’t have built-in chapter markers, but I can fake the same UX with anchor links in a TOC at the top of each long post. None of my 11 posts have a TOC.

3. It’s episode 17 of a labeled series.

Not “Day 17 of build log” — that’s me. His is “Episode 17 of #ChannelName” where the channel name is the brand. Returning viewers see “Oh, 柱子哥 dropped a new episode” before they see what it’s about.

My titles are all over the place. “Day 6”, “7 articles 1 star”, “Day 7”, “Day 8”. Forensically I made them a series two days ago via the dev.to series: frontmatter. But the brand isn’t load-bearing.

4. Public 30% / paywall 70%.

The video is the bait. The “member group” is the rod. He cheerfully tells you the project is ~200 lines of Python wiring up 5 open-source repos. He doesn’t tell you which 5. That sentence — “I won’t tell you which 5, but here’s why my pick works” — is structurally identical to a Gumroad PWYW listing where the download has the actual file paths.

I currently dump the full source code in a GitHub repo MIT licensed. The bundle is convenience packaging, not a gated answer. That’s a defensible choice for a developer-tools market — devs hate gated tutorials — but it means my hook isn’t “here’s the secret” the way his is. It’s “here’s the convenience.” Convenience converts at a lower rate than secret.

5. He’s in Japan recording for a Chinese audience.

Geographic arbitrage as a soft moat. He produces from a low-cost base, sells to a market with higher willingness-to-pay. I’m in Vietnam recording for an English-speaking dev audience. Same structural advantage. I’ve never said it on a post. He says it casually in his comments — “this is from Japan” — and the location stamp shows up next to his replies. Tiny credibility signal that compounds.

What I’m copying this week

For the next 5 dev.to posts, I’m running three changes and measuring the delta:

Reply-to-every-comment policy. Even when I get one comment per post, I respond within 24h. Every response ends with a useful follow-up question, not a CTA.

TOC at the top of any post longer than 600 words. Anchor links to numbered sections.

A single brand label across the series. “Cold-start build log” was a description, not a brand. I’m picking one short tag — probably cold-start-log — and putting it identically at the top of every post.

What I’m not copying: the gated member group. The dev market punishes that. But I’m leaning harder into the bundle being the “convenient pre-wired version” rather than the “MIT-licensed source you can also get free” framing — which means the listing’s first sentence has to shift from feature-list to time-saved.

Where I think the real gap is

Watching that video, the thing that hit hardest wasn’t any single tactic. It was that he treats his comment section as the product. The video is the funnel widget; the threaded replies are where conviction gets built. Every comment is a chance to demonstrate domain depth in public.

I’ve been treating the comment section like an afterthought because it’s empty. That’s the wrong direction. The way to fill it is to start running it like it’s full.

Starting today I’m opening a GitHub Discussion on the repo specifically as the “ask me anything about Gmail-as-CRM, stalled-thread triage, refresh-token OAuth, KVS quota patterns” thread. Linked from every post. I’ll seed it with three questions I get asked offline. We’ll see who shows up.

Day 9 score: $0 revenue, 0 sales, 1 star, 11 posts. Same as Day 8. The shift starts in what I do tomorrow, not what shows up tonight.

P.S. — The AMA thread mentioned above is now live: Gmail-as-CRM AMA on GitHub Discussions. Drop any question about stalled-thread scoring, refresh-token OAuth, KVS quota, dry-run mode — answers go back into the docs.

Armorer Guard: runtime control should start at the tool call

  1. # Armorer Guard: runtime control should start at the tool call

The more I work on local agent systems, the less I believe static policy alone is enough.

Once an agent can actually read, write, send, or purchase, the runtime boundary becomes the real control point. That is where I want action classes, execution receipts, and a clear human stop point.

That is the direction I am exploring with Armorer Guard right now.

I do not think the interesting question is just ‘can we scan prompts and outputs?’
I think the more useful question is:

  • where should runtime control begin?
  • what evidence should exist after a tool call?
  • how should risky actions pause, continue, or escalate?

Repo for context: https://github.com/ArmorerLabs/Armorer

I would love feedback from people building with MCP, local agents, or self-hosted automation.

Google AI Edge Gallery Now Runs MCP On-Device. The Privacy Architecture

This is a submission for the Google I/O Writing Challenge

On-device AI has spent most of its existence being impressive in demos and limited everywhere else. Google just changed the constraint that mattered most: the model couldn’t reach anything outside the app sandbox.

The Problem It’s Solving

Local inference is great for privacy and latency. It’s lousy for usefulness. A model running entirely on your phone can answer questions from its training data and nothing else — no calendar, no inbox, no live web, no external tools. You get an isolated reasoning engine that can’t act on the world around it.

That’s the fundamental tension in edge AI: the moment you connect a model to external systems, you typically route the requests through a server. The privacy story falls apart. The latency goes up. The offline capability disappears.

Google AI Edge Gallery just shipped an answer to this. The May 19 update adds Model Context Protocol (MCP) support to the Android app, alongside scheduled notification reminders and persistent chat history. Together, these three features move the app from a model playground into something that starts to look like an actual on-device agent runtime.

How It Actually Works

The MCP integration runs over Streamable HTTP, currently experimental and Android-only (iOS support is coming). The architecture is worth understanding carefully, because it’s not what you might expect.

When you register an MCP server URL in the app, it dynamically pulls tool definitions and resource schemas directly into Gemma 4’s system prompt on-device. The reasoning happens entirely on the phone. Gemma 4 decides locally which tool to call, generates the request locally, and then sends that request to wherever the MCP server lives — your home computer, a cloud endpoint, wherever. The model itself never leaves the device.

This is a meaningful architectural choice. The tool selection and orchestration logic stays private. Only the structured API call goes out over the network, not your raw query or whatever context the model was working with.

The notification system works differently: it’s a “Schedule Notification” skill that sets local OS-level reminders. When you tap one, the app opens directly to the right tool and launches a Gemma 4 session automatically. No server involved at all.

Chat history persistence runs through the LiteRT-LM backend’s fast prefill capability. On modern phone GPUs, prefill can hit over 3,000 tokens per second, which means the model can reconstruct a long previous session almost instantly when you reopen the app. Sessions maintain state across text, images, and audio.

What Developers Are Actually Using It For

The MCP use cases Google demos are practical rather than speculative. Connect to a Google Workspace MCP to query your calendar or check your inbox. Use a Google Maps MCP to ask about travel times in natural language. Connect a web fetch MCP to pull live documentation or news into the model’s context.

The notification + session continuity combination opens up something more interesting: scheduled routines that actually maintain context. A mood tracking workflow that reminds you every evening at 10 PM, opens to Gemma 4, and — because chat history persists — can look back at previous entries to surface trends. A morning briefing that reads your local calendar and gives you a summary before you leave the house. A daily “learn something new” prompt that generates a shareable visual infographic from whatever topic you pick.

The community-built skills on the GitHub Discussions page are already going further: lightweight web search integrations for live weather and currency data, parsers that turn images and HTML into structured data for semantic search, quiz generators, language translators, offline puzzle games.

Google has also added the ability to edit the system prompt directly from chat settings, which is the right call for a developer-facing app. You can define personas, set output constraints, or experiment with prompting approaches without touching any config files.

One practical note for anyone building on this: on-device models have smaller context windows than their server-side counterparts. Google explicitly recommends keeping MCP tool descriptions short and returning bite-sized data snippets rather than long text blocks. The architecture rewards lean, well-scoped tool definitions.

Why This Is a Bigger Deal Than It Looks

MCP has spent most of 2025 and early 2026 as an enterprise and desktop story. The tooling, the infrastructure, the conversation — it’s been aimed at developers building server-side agents with access to large context windows and cloud compute.

Putting MCP into a phone app, powered by a model running entirely on-device, moves the protocol into a different category of deployment. The reasoning stays on the device. Only structured tool calls go out over the network. That’s a viable architecture for healthcare apps, legal tools, or anything else where raw query data can’t leave the device.

There’s also something worth noting about the open-source angle here. The Google AI Edge Gallery repository is public, the skill system is extensible, and the community is already building on it. This isn’t a closed platform with a curated app store of approved integrations. Anyone can write an MCP server, register it in the app, and extend what on-device Gemma can reach.

The combination of persistent sessions, proactive notifications, and external tool access is basically the minimum viable definition of an ambient agent: something that maintains context over time, reaches external systems when needed, and can act without being explicitly invoked. Google shipped all three in one update.

Availability and Access

The MCP integration is live now in the Android version of Google AI Edge Gallery. iOS support is listed as coming soon. Technical documentation and example MCP configurations are in the GitHub repository. The app is free on both the Play Store and App Store.

The edge AI stack — Gemma 4 running locally, MCP bridging to external tools, LiteRT-LM handling fast prefill — is now available to any developer who wants to build on it. The interesting question is which use cases the community finds that Google hasn’t thought of yet.

MCP’s reach just extended to every Android phone. That’s a different surface area than any enterprise deployment.

Follow for more coverage on MCP, agentic AI, and AI infrastructure.

Database Transaction Isolation Levels: Why They Are Always Critical?

The isolation levels of database transactions are a topic that beginners in software development often see as a “detail,” but as I gained experience, I understood how critical it is. Over the years, I’ve repeatedly experienced how this seemingly simple setting deeply impacts everything from an application’s data integrity and performance to the accuracy of business processes. This post will explain why, in my opinion, it’s so important and at which points it has come up in my career.

While working on a production ERP system, we were experiencing inconsistencies with shipment lists. When the report was pulled a few times during the day, different totals were seen, causing significant confusion. At first, I searched for a bug in the application layer, but the root cause was actually stemming from the database’s default transaction isolation level and other concurrently running transactions. Such subtle details often don’t come to mind, but they can be key to solving underlying problems at the heart of the system.

Database Transaction Isolation Levels: A Fundamental Overview

Transaction Isolation Levels determine how much concurrent transactions in a database will affect each other. The ANSI SQL standard defines four main levels: Read Uncommitted, Read Committed, Repeatable Read, and Serializable. However, each database system implements these levels differently based on its internal architecture. For example, PostgreSQL does not support the Read Uncommitted level, and Read Committed is the default.

In my experience, these levels are not just theoretical definitions but practical decisions that directly affect the reliability and data integrity of an application. A wrong choice can lead to serious consequences such as inconsistencies in financial reports, stock errors, or corruption of customer data. Therefore, understanding and using these levels correctly when designing a system or debugging an existing one is, in my opinion, a must-have.

ℹ️ Why is it Important?

Database transaction isolation levels control how much concurrent transactions will affect each other. This is critically important for data consistency and the accuracy of business workflows, especially in systems with high concurrency. Proceeding without understanding the default level can invite insidious errors that are difficult to resolve later.

In a production ERP, an operator pulling daily production reports seeing different numbers in the morning versus the afternoon shook operational confidence. The reason was simple: the reporting process was running at the READ COMMITTED level, while new production records were being added and some were being updated during the day. Since the report saw the latest committed data with each SELECT query, it produced inconsistent results due to the changing data during the transaction’s duration. This was a perfect example of “Phantom Read” and “Non-Repeatable Read” scenarios and was the first signal for me that I needed to research this topic more deeply.

READ COMMITTED and Its Unexpected Outcomes

READ COMMITTED is the default isolation level for many databases (including PostgreSQL) and is generally considered sufficient for most applications. At this level, a transaction only sees data that has been committed by other transactions. This prevents “dirty reads,” meaning you don’t read data that is uncommitted and potentially will be rolled back. However, this level is susceptible to “non-repeatable reads” and “phantom reads.”

In my experience, the flexibility offered by READ COMMITTED has sometimes led to insidious problems. Especially when dealing with long-running transactions or reports involving multiple SELECT queries, I’ve seen data I read at the beginning of a transaction change by the time the transaction progresses, because another transaction updated and committed it. This situation can create critical inconsistencies, particularly in areas like data analysis or financial reporting.

We had a transaction in the inventory management module of an e-commerce platform that checked the stock status of a product and then made a reservation. The transaction would query the current stock of the product, then reserve a certain quantity for the customer, and then update the stock quantity. If another transaction changed the stock of the same product between the first query and the update, the transaction running at the READ COMMITTED level could make a reservation with an incorrect stock quantity. This carried the risk of telling a customer “the product is available” and then later saying “it’s out of stock.”

BEGIN;
-- First SELECT: stock 100
SELECT stock_quantity FROM products WHERE id = 123;

-- Meanwhile, another transaction reduces the stock to 90 and commits.

-- Second SELECT: stock 90
SELECT stock_quantity FROM products WHERE id = 123;
COMMIT;

The scenario above illustrates how inconsistent reads can occur even within the same transaction under READ COMMITTED. If the business logic is sensitive to such fluctuations, it might be necessary to move to a higher isolation level or use additional locking mechanisms at the application layer. For me, this showed that not only database settings but also the design of the business workflow and the application are directly related to these levels.

REPEATABLE READ: A Step Towards Consistency

The REPEATABLE READ isolation level is designed to address the non-repeatable read problem that READ COMMITTED cannot solve. At this level, all data read by a transaction remains the same until that transaction completes. This means that no matter how many times you run the same SELECT query within the same transaction, you will always get the same result set. This is very important for ensuring data consistency, especially in reporting and analytical operations.

However, even REPEATABLE READ does not always provide full protection against “phantom reads.” Phantom reads occur when the set of rows returned by a query changes during a transaction; that is, if there were 5 rows satisfying a certain condition at the start of a transaction, and midway through the transaction another transaction adds a new row that satisfies the same condition, the same query might see this new row. PostgreSQL’s REPEATABLE READ implementation actually prevents phantom reads (thanks to its MVCC snapshot), but the situation might differ in other database systems. Therefore, it’s crucial to distinguish between standard definitions and specific database behaviors.

In the financial calculators of a side project, a task management application, I was generating complex reports. For these reports to be accurate, all financial records read during the report period needed to remain constant. When I ran the report with READ COMMITTED, my calculations were constantly changing because other transactions were adding new income/expense records intermittently. I resolved this issue by switching to REPEATABLE READ.

SET TRANSACTION ISOLATION LEVEL REPEATABLE READ;
BEGIN;
-- First SELECT: Get all income items
SELECT SUM(amount) FROM transactions WHERE type = 'income' AND date BETWEEN '2026-01-01' AND '2026-01-31';

-- Meanwhile, another transaction adds a new income record for the same dates and commits.

-- Second SELECT: Run the same query again, the result should be equal to the first
SELECT SUM(amount) FROM transactions WHERE type = 'income' AND date BETWEEN '2026-01-01' AND '2026-01-31';
COMMIT;

In this example, thanks to REPEATABLE READ, the second SELECT query will return the same result by using the snapshot of the data seen by the first query. This provides great reliability for applications that need to analyze data at a specific point in time or for reporting. However, it’s important not to forget that this level comes with increased resource usage and potential locking risks. Although PostgreSQL’s MVCC (Multi-Version Concurrency Control) structure minimizes these risks, being prepared for transaction retries is always good.

SERIALIZABLE: The Balance of Security and Performance

SERIALIZABLE is the highest isolation level in the ANSI SQL standard. At this level, all concurrently running transactions appear to have executed one after another, in sequence (serially). This completely prevents a transaction from seeing any inconsistency created by another transaction; meaning, all anomalies like dirty reads, non-repeatable reads, and phantom reads are eliminated. It is an ideal choice for critical applications where data integrity is of absolute priority.

However, this level of security comes at a cost: performance. SERIALIZABLE transactions can encounter serialization_failure errors when conflicting transactions occur and must be rolled back. This means the application needs to catch these errors and retry the transaction. This extra overhead can create a significant bottleneck, especially in systems with high concurrency and intensive write operations.

While designing a complex reconciliation process for a bank’s internal platform, I had to use SERIALIZABLE. This process involved money transfers and balance updates between different accounts, and even the slightest inconsistency was unacceptable. Initially, we experienced frequent transaction interruptions because we hadn’t adequately handled serialization_failure errors. The debugging process taught us that we needed to catch these errors and automatically retry the transactions.

import psycopg2
from psycopg2 import errors

def run_serializable_transaction(conn_string, query_func):
    for _ in range(5): # Give 5 retry attempts
        conn = None
        try:
            conn = psycopg2.connect(conn_string)
            conn.set_isolation_level(psycopg2.extensions.ISOLATION_LEVEL_SERIALIZABLE)
            cur = conn.cursor()
            query_func(cur)
            conn.commit()
            return True
        except errors.SerializationFailure:
            print("SerializationFailure detected, retrying...")
            if conn:
                conn.rollback() # Roll back the transaction
        except Exception as e:
            print(f"An unexpected error occurred: {e}")
            if conn:
                conn.rollback()
            return False
        finally:
            if conn:
                conn.close()
    print("Max retries reached, transaction failed.")
    return False

# Example usage
def my_complex_query(cursor):
    cursor.execute("UPDATE accounts SET balance = balance - 100 WHERE id = 1;")
    cursor.execute("UPDATE accounts SET balance = balance + 100 WHERE id = 2;")

# run_serializable_transaction("dbname=mydb user=myuser", my_complex_query)

This example shows how to catch and retry SerializationFailure errors when performing a SERIALIZABLE level transaction. This simple retry mechanism significantly increased the application’s resilience and reliability. When using SERIALIZABLE, it’s important to remember that the application layer must be prepared for such errors, which was a big lesson for me. To minimize performance losses, limiting this level to truly critical and few transactions is the wisest approach.

Trade-offs and Misconceptions in Real-World Applications

Choosing database isolation levels is always a matter of trade-offs: you have to strike a balance between data consistency, concurrency, and performance. The idea of “always use the highest level because it’s the safest” often leads to unsustainable performance issues in practice. On the other hand, the notion that “the default level is always sufficient” can pave the way for insidious data corruption.

In a client project, I worked with a team that insisted on using SERIALIZABLE in a system with heavy data entry. As a result, we were experiencing up to 15% transaction failure rates per day due to database deadlocks and serialization_failure errors. After detailed analysis and profiling, we demonstrated that most transactions could be sufficiently secured with READ COMMITTED or specific parts with REPEATABLE READ. After selecting the correct levels, the transaction failure rate dropped below 0.5%.

This situation showed that choosing the isolation level is not just a technical decision but must also reflect the nature of business processes and the application’s expectations. Business logic dictates how long certain data needs to remain consistent. For instance, while an instant change in the quantity of a product in a user’s cart might be acceptable, it’s unacceptable for a financial report to yield different results when run twice.

⚠️ Misconception: Always the Highest Level

Some developers believe that always using SERIALIZABLE is the best way to guarantee data integrity. However, this can lead to significant negative impacts on concurrency and performance. SERIALIZABLE, especially in high-concurrency write operations, can cause serialization_failure errors, requiring the application to handle retry mechanisms. In my experience, this level has only been suitable for the most critical and few transactions. Choosing the right level depends on a good understanding of the application’s business requirements and performance expectations.

Making the right decision regarding database isolation levels is also an integral part of database performance optimization. Using an unnecessarily high isolation level can increase locking contention, reducing the system’s overall throughput. Therefore, we should always choose the least restrictive level, but ensure that this level meets the business requirements. In my previous post, [related: my experiences in database performance optimization], I discussed these balances in more detail.

The Place of Isolation Levels in My Career: Why is This Important Knowledge?

There’s a specific reason I’m writing about this topic in the career category: a deep understanding of database transaction isolation levels, in my opinion, is a critical differentiator that directly indicates a software developer’s or system architect’s competency level. You can develop many applications with superficial knowledge, but when data integrity issues arise, knowing these details sets you apart.

In my career, this knowledge has enabled me to be a “hero” on multiple occasions. In a client project, there were minor inconsistencies that appeared periodically in financial reports, which had been unresolved for months. Everyone blamed the reporting tool, the business logic, or manual data entry errors. However, my knowledge of transaction isolation levels allowed me to realize that a complex multi-step transaction, running at the READ COMMITTED level, was being affected by another concurrent transaction changing the data, causing the report to take a different snapshot midway. This was resolved with a simple SET TRANSACTION ISOLATION LEVEL command, and I earned the team’s trust.

This kind of understanding not only helps in fixing bugs but also in designing more robust and scalable systems. When making system architecture decisions, transitioning from a monolith to microservices, or implementing architectural patterns like event-sourcing, knowing how transactions are isolated is critical. In my post [related: my experiences in enterprise software architecture decisions], I explained the motivations behind such architectural choices in more detail.

This knowledge allows you to foresee potential future problems and make the right design choices from the outset. It’s not enough for a system to just “work”; it must also “work correctly.” Database transaction isolation levels are one of the cornerstones of this “working correctly.”

Conclusion: Isolation Levels Are Not a Luxury, They Are a Necessity

Database transaction isolation levels are a critical topic, often overlooked, yet they form the foundation of data integrity and system reliability. In my nearly 20 years of experience, I’ve repeatedly seen that understanding this topic is more than just a technical detail; it’s the key to building robust and reliable software systems. While we live in a world where READ COMMITTED is the default, knowing when your business requirements necessitate REPEATABLE READ or SERIALIZABLE protects you from potential data disasters.

Investing time in this topic is essential not only to become a good database administrator or developer but also to become a professional who can identify and resolve insidious errors in complex systems, thus making a difference in their career. My clear position is: understanding what these levels mean, what problems they cause in which scenarios, and which level brings which trade-offs, is a fundamental competency that every technology professional should have in their knowledge base.

Anthropic Bought an SDK Factory and Hired Karpathy in the Same Week. Here Is What That Combination Means for a Solo Developer.

TL;DR. Anthropic acquired Stainless on May 18 (the SDK generator behind every official Anthropic SDK plus the SDKs of competitors), then hired Andrej Karpathy onto the pre-training team on May 19. Read together these are two ends of the same bet. One end says the API surface and the tooling around it must be first-class. The other end says the model capability behind that surface needs another step change. For a solo developer the practical takeaway is that the floor under what one person can ship just went up again, and the four pieces you should be touching this week are an official SDK with prompt caching, one Skill, one MCP server, and a context file in your repo root.

The two headlines, in order

Two announcements landed within roughly thirty hours of each other.

May 18. Anthropic announced it acquired Stainless, the company that has generated every official Anthropic SDK since the earliest API days and that also produces SDKs for several competitor labs. The Information reported the deal value at over $300 million (TechCrunch, Anthropic post). Anthropic also said it would wind down the hosted Stainless products that were available to competitor labs.

May 19. Andrej Karpathy posted that he had joined Anthropic. He is reporting into Nick Joseph on the pre-training team. The role is research, focused on using Claude to accelerate pre-training itself (TechCrunch, CNBC).

Hacker News reacted at the scale these stories deserved. The Stainless thread cleared 503 points. The Karpathy thread cleared 1,017 points the following day.

On the surface these are unrelated. One is corporate development. One is a hire. The reason they sit together is the shape of the bet underneath.

What Stainless actually does

Stainless takes an OpenAPI specification and generates SDKs in TypeScript, Python, Go, Java and other languages. Same input. Multiple language outputs. The output is hand-quality enough that companies ship it as their official SDK. Anthropic. OpenAI. Cloudflare. The Stainless customer list reads like a who’s who of the labs the dev community actually integrates with.

In the announcement Anthropic flagged that Stainless also produces MCP servers, not only SDKs (Anthropic post). Read that sentence twice. Same generator pipeline. Two outputs. SDKs for human developers. MCP servers for agents.

Anthropic also said competitors will no longer have access to the hosted Stainless products. The phrasing in the Anthropic blog and in TechCrunch is consistent on this point. Whether the open-source generator itself stays under any license is a separate question that the announcement does not fully resolve. The hosted product, the one labs were actually using, is now Anthropic’s.

The quote from Katelyn Lesse, head of platform engineering at Anthropic, is the line that tells you what the acquisition is for. “Agents are only as useful as what they can connect to.” That is the bet. SDK quality is connectivity. MCP server quality is connectivity. Both come out of the same pipeline. Owning the pipeline shortens the loop between an API change at Anthropic and a working SDK or MCP server in a developer’s hand.

What Karpathy actually brings

Karpathy is not joining a developer tools team. He is joining the pre-training team. His own statement and the Axios writeup are explicit. He wants to be back at the frontier of large language model research and the specific surface is using Claude to accelerate pre-training itself.

It is worth being honest about this. A first read of these two announcements as a single story tempts the angle that Karpathy is going to work on the agent stack. The reporting does not support that. He is on pre-training. The agent stack and the model stack are different teams.

So why do these belong in the same article. The answer is that the agent stack and the model stack only make sense together, and Anthropic is now investing visibly in both at the same time. Stainless raises the floor on the surface that agents and developers touch. Karpathy is signal that the model underneath is going to keep getting harder to compete with.

If Anthropic only acquired Stainless, the story would be a tooling story. If Anthropic only hired Karpathy, the story would be a research story. Both in the same week is the company telling the market that the surface and the core are getting pushed forward together. That is the platform play.

Why the two stories are one signal

A pattern is what makes two events one signal. The pattern here is the platform shape.

A platform needs two things to be safe to build on. The surface has to be stable and fast. The core has to keep improving faster than the alternatives. If either side stalls, builders leave. Stainless is the surface investment. Karpathy is the core investment. The same week is not an accident of timing. It is a market message.

Compare with the alternatives. A lab that puts all its weight on raw capability and lets the SDK lag will lose developers to whoever ships a better client. A lab that polishes the SDK while the model falls behind will lose developers to whoever has the smarter model. Anthropic is now visibly funding both ends in the same week. That is not a guarantee they will win. It is a guarantee that the bar for everybody else just moved.

For a solo developer the practical question is not which lab will win. The practical question is which surface to build on this week. The signal says the Anthropic surface is being actively invested in on both ends. That is enough to make it a defensible choice.

What this changes for a solo developer

Three things change in practice.

First. The cost of getting an SDK upgrade after a model release drops. With Stainless inside the company the pipeline from API change to working SDK in your hand is shorter. You will feel this in fewer days of waiting after a release before the client library catches up. Anyone who has shipped against a model that landed a feature the SDK had not caught up to yet knows what that delay costs. Less of that.

Second. The interface for agents and the interface for humans converge. Same generator pipeline now produces both SDKs and MCP servers. If you write a context engineering kit that targets one surface, the porting cost to the other surface is small. A skill you build to call your own service through MCP can become a Python SDK example in the same repo without a rewrite. This is one of those changes that is easy to undervalue in the short term and very hard to reverse once your codebase depends on it.

Third. The ceiling on what one person can ship moves up. This is the line that matters. The story of the last two years is that work that used to require a small team now fits inside one operator with a good context file, a few skills, and a couple of MCP servers. Anthropic acquiring the SDK factory and hiring a frontier researcher in the same week is the company stating that the direction of travel continues.

I will be specific about what one person can now ship in a week with this stack. The pieces I am running in production right now are a Telegram bot that does file read and write plus web search and URL fetch on a Windows machine, a sniper trading bot with automated health checks every four hours and a weekly memory consolidation pass, a daily journal generator that writes a post and pushes to git, and a self-validation cron that audits eighteen active projects every four hours. All of it runs on one Claude Max subscription. None of it needed a team. The Stainless acquisition makes the next version of this cheaper to assemble. The Karpathy hire makes the next version smarter when it runs.

There is a second-order effect worth naming. When the SDK and the MCP server come out of the same generator, the unit of distribution for a small product changes. A solo developer who used to ship a Python library can now ship a Python library plus a matching MCP server out of the same spec. A consumer of that product gets two surfaces for one release. That is leverage you used to need a tooling engineer to realize. The Stainless pipeline is what makes that leverage cheap.

Four pieces to touch this week

This is the actionable part. If the signal is real, the question is what to do about it.

The honest answer is the same four pieces I keep returning to. An official SDK with prompt caching enabled. One Skill. One MCP server. One context file in your repo root.

Piece one. The official SDK with prompt caching

Most builders on the Anthropic API leave prompt caching off. The cached prefix is roughly 90 percent cheaper to reuse than the equivalent uncached tokens. If your stable system prompt is four to six thousand tokens, every reuse after the first is near free. The Stainless acquisition is going to keep tightening this surface, so put it in your code now.

from anthropic import Anthropic

client = Anthropic()

response = client.messages.create(
    model="claude-opus-4-7",
    max_tokens=1024,
    system=[
        {
            "type": "text",
            "text": LARGE_STABLE_CONTEXT,  # persona, rules, schemas
            "cache_control": {"type": "ephemeral"},
        }
    ],
    messages=[
        {"role": "user", "content": user_query},
    ],
)

print(response.usage.cache_read_input_tokens)
print(response.usage.cache_creation_input_tokens)

The two usage fields at the bottom are what you watch. The first call writes the cache. Every subsequent call inside the cache window reads it. The ratio you want over a week of traffic is heavy on cache reads, light on cache creations. If the ratio is inverted, your stable context is not actually stable, and you need to look at what is changing in the prefix.

Piece two. One Skill

Skills are the unit of reusable agent capability inside Claude Code. A Skill is a directory with a SKILL.md plus any scripts or resources the skill needs. The SKILL.md frontmatter tells Claude when to load the skill. The body tells it what to do.

---
name: my-data-extractor
description: Extracts structured data from PDF invoices and returns a JSON line per invoice. Trigger when the user asks to parse, extract, or summarize invoices.
---

# My Data Extractor

You parse PDF invoices.

## When to load

The user mentions invoice parsing, PDF extraction, or asks for line-item totals across multiple invoices.

## What to do

1. Read each PDF with the Read tool.
2. Extract vendor, invoice date, line items, subtotal, tax, total.
3. Emit one JSON line per invoice to `extracted.jsonl`.
4. Print the count of invoices processed and the sum of totals.

## What not to do

Do not invent fields not in the source. Do not normalize dates without quoting the original string in a `source_date` field.

A working skill in your repo is the cheapest piece of agent infrastructure you can ship. It is portable across machines. It is portable across team members. It is auditable. And it is the unit Anthropic is leaning on for the agent surface.

Piece three. One MCP server

MCP is the protocol that lets a Claude agent reach a tool you control. Stainless was already generating MCP servers from OpenAPI specs before the acquisition. The acquisition is going to make this surface more central.

A minimal MCP server config in Claude Code looks like this.

{
  "mcpServers": {
    "my-service": {
      "command": "node",
      "args": ["./mcp-servers/my-service/index.js"],
      "env": {
        "API_KEY": "${env:MY_SERVICE_API_KEY}"
      }
    }
  }
}

The point of running your own MCP server is that whatever tool surface your daily work needs becomes a first-class capability of any Claude session you open. A read-only MCP server that exposes your project status files. A write-allowed MCP server that lets a session push a journal entry to your blog. A search MCP server over your own notes. Each one is hours of work and pays back across every session you open after it.

The shape of the four pieces together

The four pieces interact. Here is the topology in plain text.

+----------------------+        +---------------------+
|  Your IDE / Terminal |  --->  |  Claude Code        |
+----------------------+        |   session           |
                                |                     |
                                |   reads CLAUDE.md   |
                                |   loads Skills      |
                                |   calls MCP servers |
                                +---------+-----------+
                                          |
                            +-------------+-------------+
                            |                           |
                +-----------v---------+      +----------v-----------+
                |  Your MCP server(s) |      |  Anthropic API       |
                |  (your tools)       |      |  (prompt caching on) |
                +---------------------+      +----------------------+

The Claude Code session is the orchestrator. CLAUDE.md is its standing brief. Skills are its named capabilities. MCP servers are how it reaches your private tools. The API call with prompt caching is how the heavy stable context stays cheap to reuse. Each piece is small. The shape of the four together is what gives one person the surface area of a team.

Piece four. One context file in your repo root

The single highest leverage move I have made in the last six months is to write a CLAUDE.md at the root of each repo and keep it true.

The file is read by Claude Code on session start. The content is project facts, persona, rules, and pointers to the other context files in the project. The format is plain markdown. The cost is the time to write it once and the discipline to update it when the project changes.

A working CLAUDE.md paragraph from my agent-starter-kit repo:

# CLAUDE.md

This repo is a Telegram AI agent starter kit. The bot is built on Gemini
function calling. The five tools are file read, file write, web search,
URL fetch, and shell command. The shell command tool is sandboxed inside
`~/Desktop/agent-workspace/` via the `_safe_path` helper. Do not loosen
that sandbox. The allowlist in `ALLOWED_USER_IDS` is the only access
control, so any new tool must check user_id at the top.

When a session starts, read `bot.py`, `requirements.txt`, and `README.md`
first. The README is the user-facing setup guide and is also the source
of truth for the supported tool list.

When asked to add a tool, follow the existing pattern: a Python function
with a clear docstring, registered in the `tools` list, with an
allowlist check at the top and a `_safe_path` check on any path argument.
Do not introduce new dependencies without updating `requirements.txt`
with a pinned version.

This is not unusual writing. It is a clear paragraph about what the project is, what the rules are, and what the next session should do. The leverage comes from the fact that every Claude session in this repo starts with this file loaded into context. Twenty minutes of writing buys back hours of repeated explanation across the life of the project.

Where this lands for indie dev economics

The cleanest way to read the week of May 18 to May 19 is as a compression event.

Work that used to require an SDK engineer plus a research engineer plus an agent infrastructure engineer now fits inside the surface of one Claude Code session with a context file, a couple of skills, and one or two MCP servers. The Stainless acquisition tightens the surface. The Karpathy hire deepens the core. The two together raise the ceiling on what one operator can ship.

That is the indie dev moment. The team has been compressed into a person. Not because the person is doing more work. Because the platform is doing more of the work that used to need a team.

A reasonable counter is that platforms shift. Anthropic could change pricing. Anthropic could change the MCP spec. Skills could be deprecated. All of these are real risks. The mitigation is to keep the surface area you depend on small and portable. A SKILL.md is just markdown. An MCP server you own is just a process you run. A CLAUDE.md is just markdown. None of these lock you to one vendor more than the SDK you import already does. If the platform shifts, the cost of porting the context is hours, not months.

A second reasonable counter is that Anthropic shutting down the hosted Stainless products that competitors used is bad for the wider ecosystem. That is fair. If you are building tooling that talks to multiple labs at once, the loss of a shared generator is a real cost. The story for a solo developer who already picked a primary lab is different. You picked a lane, and the lane just got a faster surface and a stronger core. The market-level cost is separate from the per-developer benefit.

There is also the question of whether the same compression happens on competing platforms. The honest answer is probably yes, on a delay. OpenAI has invested heavily in its assistants and tools surface. Google has its own generation tooling. The Anthropic move is a forcing function on both. A solo developer who builds on portable surfaces (skill files, MCP servers, plain markdown context) is positioned to benefit from whichever platform leads, because the migration cost between platforms drops as the surfaces converge on the same primitives.

Closing

Two announcements. Thirty hours apart. Read separately they are a corporate development item and a hire. Read together they are the platform stating intent.

The intent is to invest in both the surface developers and agents touch and the core capability behind that surface, at the same time, visibly.

For a solo developer the question is simple. Are you on this surface yet. If not, the four pieces are an SDK call with prompt caching enabled, one skill, one MCP server, one context file. That is the entry. Everything else is iteration.

I will be running the same play this week. The next post in this thread will be a comparison of building the same small agent on the Anthropic stack versus a forge-style alternative, with cost and friction numbers. If you want to follow along, the agent-starter-kit and the context engineering material below are the working artifacts I am iterating from.

Related

Sources

ScaffoldX vs Vite vs Create-React-App: Which Project Scaffolder Is Right for You in 2026

Every JavaScript developer knows the feeling: you have a great idea, you open your terminal, and then… you spend the next 30 minutes setting up the project boilerplate.

ESLint config. Prettier. TypeScript. Testing framework. Git hooks. The list goes on.

By the time you actually start coding, the excitement has already faded.

Three tools promise to solve this problem in 2026: Create-React-App (the veteran), Vite (the speed demon), and ScaffoldX (the newcomer that does things differently). Let’s compare them head-to-head.

The Contenders

Feature Create-React-App Vite ScaffoldX
First release 2016 2020 2026
Templates 1 (React) Framework plugins 12+ built-in
Setup time ~2 min ~30 sec ~3 sec
TypeScript Opt-in Opt-in Default + strict
Linting None built-in None built-in ESLint + Prettier pre-configured
Testing Jest included Manual setup Vitest + Testing Library
Git hooks None None Husky + lint-staged
Build tool webpack esbuild/Rollup Vite (your choice)
Package size ~250MB ~180MB ~120MB
Interactive CLI No No Yes (pick what you need)

1. Create-React-App: The Familiar Choice

npx create-react-app my-app

CRA has been the default for years. It works. It’s battle-tested. But in 2026, it feels dated:

  • No TypeScript by default — you have to remember the --template typescript flag
  • No linting or formatting setup — you install ESLint and Prettier separately, then spend 20 minutes tweaking configs
  • webpack under the hood — slower builds compared to modern alternatives
  • No git hooks — no pre-commit linting, no commit message validation

CRA is like that reliable old car. It gets you there, but you wonder if there’s a better way.

2. Vite: The Speed Option

npm create vite@latest my-app -- --template react-ts

Vite is fast. Crazy fast. Hot Module Replacement is instant. The dev server starts in milliseconds.

But here’s the catch: Vite only scaffolds the framework. Everything else is on you:

# After `npm create vite`, you still need to:
npm install eslint prettier eslint-config-prettier --save-dev
npx eslint --init  # another 10 interactive prompts
npm install vitest @testing-library/react --save-dev
# Configure vitest.config.ts
# Set up husky + lint-staged manually
# Set up CI pipeline

Vite gives you the engine. You build the rest of the car.

3. ScaffoldX: The “Everything Included” Option

npx scaffoldx-cli my-app

This is where ScaffoldX takes a different approach. Instead of giving you a bare skeleton and wishing you luck, it asks you what you need — and sets up everything in one shot:

$ npx scaffoldx-cli my-app

? Select a template:
  ❯ React + TypeScript
    Vue 3 + TypeScript
    Next.js
    Express API
    CLI Tool
    Library
    Monorepo

? Additional tooling (space to select):
  ❯ ESLint + Prettier
  ❯ Husky + lint-staged
  ❯ Vitest + Testing Library
  ❯ GitHub Actions CI
  ❯ Docker

✨ Scaffolding project... done in 3.2s!

What you get:

  • ✅ TypeScript with strict mode
  • ✅ ESLint + Prettier with sensible defaults (no config debate)
  • ✅ Pre-commit hooks that lint and format automatically
  • ✅ Testing framework configured and ready
  • ✅ CI pipeline if you want it
  • ✅ Proper .gitignore, .editorconfig, and VS Code settings

No post-install checklist. No “now go configure X” blog posts to follow. You run one command and start coding.

The Real Test: Time to First Commit

Let’s measure what actually matters: how long until you can make your first meaningful commit?

Tool Scaffold Configure Total
Create-React-App 2 min 25 min (ESLint, Prettier, TS, test) ~27 min
Vite 30 sec 25 min (same setup) ~25 min
ScaffoldX 3 sec 0 min (all included) ~3 sec

That’s the difference between “let me set this up real quick” and actually doing it.

When to Use Each

Use Create-React-App if:

  • You’re maintaining a legacy CRA project
  • You need maximum ecosystem compatibility
  • You don’t mind manual setup

Use Vite if:

  • Build speed is your #1 priority
  • You’re comfortable configuring tooling yourself
  • You need framework flexibility beyond React

Use ScaffoldX if:

  • You want to go from idea to code in seconds
  • You’re tired of copying ESLint configs between projects
  • You believe boilerplate should be automated, not memorized
  • You work on multiple projects and want consistency

One Command to Try All Three

# Compare them yourself:
npx create-react-app test-cra
npm create vite@latest test-vite -- --template react-ts
npx scaffoldx-cli test-scaffoldx

# You'll be coding in the ScaffoldX one while the others are still installing

The Bottom Line

Create-React-App and Vite are excellent tools. But they solve half the problem: they give you a project skeleton, then leave you to configure the rest.

ScaffoldX takes the “batteries included” approach. It doesn’t just scaffold — it delivers a production-ready project with linting, testing, git hooks, and CI already wired up.

In 2026, that’s the standard developers should expect.

If this saves you setup time, buy me a coffee ☕

Try ScaffoldX: GitHub | npm

Check out my other tools:

  • DotGuard — Scan your .env files for exposed secrets
  • GitPulse — Git analytics without leaving the terminal
  • SnippetX — Organize your code snippets like a pro

How I Built a Habit Tracker With Living Trees Using React and Firebase at 17

I’m Ali, 17, CS student. I built JustGoBloom as a portfolio project and it accidentally became a real product with over 100 users. Here’s the full technical and personal journey.

The Idea

I wanted a habit tracker that wasn’t just checkboxes. The concept: every goal becomes a tree. Show up daily, your tree grows through 9 stages. Miss a day, it wilts. 7 unique tree types, 3 visual modes, team gardens, the works.

The Tech Stack

Frontend: React + Vite. Vite was used over CRA because the dev server is instant and hot reload actually feels hot. The entire app is a single page app with all routing handled in state, no React Router needed since the navigation is simple enough.

Backend: Firebase handles everything. Authentication with email and password, Firestore for the database, Cloud Messaging for push notifications, and Analytics for tracking. Firebase was the right call for a solo project.

Hosting: Vercel with a custom domain through GoDaddy. Auto-deploys on every git push. The entire deploy pipeline is just git push and Vercel handles the rest.

PWA: Service worker for offline support, web app manifest for install prompts. Bloom works as a progressive web app so users can add it to their home screen.

The Tree System

This was the most interesting technical challenge. Each tree is a pure SVG component that renders differently based on its growth stage and tree type.

The TreeSVG component takes a stage index (0 through 5+), a color, a size, and a tree type. Each tree type (Evergreen, Goldleaf, Blossom, Willow, Sage, Summit, Ironwood) has unique canopy shapes at every stage. Blossom trees have heart-shaped leaves with pink flowers. Goldleaf uses triangular polygons. Willow has drooping branch paths.

The color system uses HSL manipulation to create depth. A darken function converts the base color to HSL, reduces the lightness, and converts back to hex. This gives each tree canopy lighter and darker leaf clusters without needing separate color definitions for every tree type. One base color in, automatic shading out.

The Streak System

The streak calculation handles both daily and weekly goals differently.

For daily goals, it walks backwards from today counting consecutive days with check-offs. If today or yesterday has a check-off, the streak is alive.

For weekly goals (like “gym 3x per week”), it groups check-offs into calendar weeks (Sunday through Saturday) and counts consecutive weeks where the user hit their target frequency. One tricky bug I hit: on Sundays, the current week has zero days elapsed, so the old code killed the streak immediately. The fix was to not require any check-offs for the current week since it just started.

The Garden

The personal garden is an SVG scene with a sky gradient, hill silhouette, and grass ground. Trees are positioned in a grid pattern using foreignObject inside the SVG so that React components (the TreeSVG) can render inside an SVG context.

Each tree gets a glow halo using radial gradients, a trunk shadow using a low-opacity ellipse, and the tree is wrapped in a div with flex alignment so it sits on the grass naturally.

Three visual modes (Light, Dark, Dawn) swap the entire color palette. The sky gradients, grass colors, hill opacity, and even the celestial bodies change. Dark mode adds twinkling star animations. Dawn mode uses warm pinks and peaches.

Bloom Teams

The most complex feature. Teams use Firestore with subcollections for goals and encouragements nested under each team document.

When a user opens a team garden, two onSnapshot real-time listeners activate. One watches the team document for member changes (joins, leaves, renames) and one watches the goals subcollection for new goals, check-offs, and deletions. Everything updates instantly across all team members without refreshing.

The team garden renders member rows with grass strips separated by dirt furrows. Each member’s trees are positioned in a staggered zigzag pattern within their row. Name pills sit inside the SVG on mobile and outside with connecting tabs on desktop.

The encouragement system uses deterministic document IDs ({fromUid}_{goalId}_{date}) to enforce a one-per-day cooldown without needing composite Firestore indexes. When a user opens the app and has unread encouragements, a cinematic celebration animation plays with water drops, golden glow rings, sparkles, and the message “Keep Blooming.”

Push Notifications

Firebase Cloud Messaging with three Cloud Functions:

  1. Morning reminder at 8am in the user’s timezone
  2. Evening nudge at 8pm if they haven’t checked in
  3. Streak warning when they’re about to lose a streak

The timezone-aware part was important. A user in Montreal shouldn’t get their morning reminder at 3am because the server runs on UTC.

What I Learned

Mobile testing is non-negotiable. So many things that looked perfect on my laptop broke on actual phones. Trees disappearing, buttons getting clipped off screen, text too small to read. Always test on a real phone before pushing anything live.

Real-time features change everything. Before I added live listeners, users had to refresh to see changes. The moment I made team gardens update instantly when someone plants a goal or checks off a day, the whole experience felt alive. That’s the difference between a website and a product.

Building is the easy part. I spent weeks adding features like journals, push notifications, three languages, team gardens. But features don’t matter if people don’t come back to use them. I should have spent more time talking to users and less time adding things nobody asked for.

Ship before it’s ready. The first version of Bloom looked rough. The current version has cinematic animations, 7 tree types, team gardens, and 3 languages. But none of that would exist if I’d waited for perfection before launching.

The Numbers

About 110 registered users. 306 Instagram followers at @justgobloom. Users in Canada, UAE, Brazil, and Zimbabwe. All organic, zero ad spend. 9.6% week 1 retention which is my biggest problem right now.

What’s Next

React Native app for iOS and Android this summer. Bloom Pro with garden customization. And figuring out why 90% of users don’t come back after week 1.

If you want to try it: justgobloom.com. It’s free.

If you’re a young developer thinking about building something, just start. A portfolio project that turns into a real product is the best thing you can put on your resume.

Follow the journey: @justgobloom on Instagram and TikTok

The fastlane Matchfile Bundle ID Trap That Killed My CI After Adding a Widget

The fastlane Matchfile Bundle ID Trap That Killed My CI After Adding a Widget

TL;DR: If you add a widget / app extension / Action Extension to your iOS project, fastlane match will silently skip its provisioning profile unless you explicitly list the widget’s bundle ID in Matchfile. CI will fail at build_app with “Provisioning profile doesn’t support the App Group” — and the error message points you at the main app’s profile, not the missing widget profile, so you spend a day chasing the wrong bug.

How It Killed My CI

DaysUntil v1.0.7 shipped fine. v1.0.8 added a widget extension. Pushed tag. CI failed at build_app:

::error file=...DaysUntil.xcodeproj::Provisioning profile 
"match AppStore com.jiejuefuyou.daysuntil 1778731087" doesn't 
support the group.com.jiejuefuyou.daysuntil App Group. 
(in target 'DaysUntilWidget' from project 'DaysUntil')

The error names the main app’s provisioning profile. My first interpretation: the App Group capability wasn’t registered. So I logged into the Apple Developer Portal, added App Group, regenerated profiles. CI still failed.

Then I added the App Group capability via the ASC API:

curl -X POST https://api.appstoreconnect.apple.com/v1/bundleIdCapabilities 
  -H "Authorization: Bearer $JWT" 
  -d '{
    "data": {
      "type": "bundleIdCapabilities",
      "attributes": { "capabilityType": "APP_GROUPS" },
      "relationships": { "bundleId": { "data": { "type": "bundleIds", "id": "6J52R36XL5" } } }
    }
  }'
# 201 success

Verified via API the capability was attached. CI still failed.

I ran gh workflow run init_signing.yml -f force=true to regenerate match profiles. CI still failed.

The fix turned out to be in fastlane’s own config:

# fastlane/Matchfile (before)
app_identifier([ENV["APP_BUNDLE_ID"] || "com.jiejuefuyou.daysuntil"])

# fastlane/Matchfile (after)
app_identifier([
  ENV["APP_BUNDLE_ID"] || "com.jiejuefuyou.daysuntil",
  "com.jiejuefuyou.daysuntil.widget"
])

Plus the Fastfile sync_code_signing and update_code_signing_settings calls, which only referenced the main bundle.

The Mental Model That Was Wrong

I had assumed fastlane match was project-aware. That is, when you run match, it parses your Xcode project, finds every target with a bundle ID, and fetches profiles for all of them.

It does not.

fastlane match only manages profiles for bundles you explicitly list in Matchfile / pass to sync_code_signing.

So when you add a widget target via xcodegen (or Xcode GUI), fastlane is blind to it. The widget target gets no profile in your match storage. At build time, Xcode looks for a profile matching the widget’s bundle ID + capabilities — and finds nothing. The next-best match it finds is the main app’s profile (similar bundle ID prefix), which it tries to use, then fails because the main app’s profile doesn’t include the widget’s bundle ID.

The error message names the main app’s profile because that’s the one Xcode tried. The actual problem is the widget profile that doesn’t exist. The error message is technically true but maximally misleading.

The Complete Fix

For DaysUntil (and any project with a widget / app extension):

1. fastlane/Matchfile

app_identifier([
  ENV["APP_BUNDLE_ID"] || "com.jiejuefuyou.daysuntil",
  "com.jiejuefuyou.daysuntil.widget"
])

2. fastlane/Fastfile

Define a WIDGET_BUNDLE_ID constant:

BUNDLE_ID = ENV["APP_BUNDLE_ID"] || "com.jiejuefuyou.daysuntil"
WIDGET_BUNDLE_ID = "#{BUNDLE_ID}.widget"

Update init_signing and beta lanes’ sync_code_signing:

sync_code_signing(
  type:          "appstore",
  readonly:      is_ci,
  api_key:       api_key,
  app_identifier: [BUNDLE_ID, WIDGET_BUNDLE_ID]
)

Update update_code_signing_settings (call twice — once per bundle):

real_profile_name = ENV.fetch("sigh_#{BUNDLE_ID}_appstore_profile-name") {
  "match AppStore #{BUNDLE_ID}"
}
widget_profile_name = ENV.fetch("sigh_#{WIDGET_BUNDLE_ID}_appstore_profile-name") {
  "match AppStore #{WIDGET_BUNDLE_ID}"
}

update_code_signing_settings(
  use_automatic_signing: false,
  path: PROJECT,
  team_id: ENV.fetch("TEAM_ID"),
  bundle_identifier: BUNDLE_ID,
  profile_name: real_profile_name,
  code_sign_identity: "Apple Distribution"
)
update_code_signing_settings(
  use_automatic_signing: false,
  path: PROJECT,
  team_id: ENV.fetch("TEAM_ID"),
  bundle_identifier: WIDGET_BUNDLE_ID,
  profile_name: widget_profile_name,
  code_sign_identity: "Apple Distribution"
)

Update build_app‘s provisioningProfiles:

build_app(
  # ... other options ...
  export_options: {
    method: "app-store",
    provisioningProfiles: {
      BUNDLE_ID => real_profile_name,
      WIDGET_BUNDLE_ID => widget_profile_name
    },
    signingStyle: "manual",
    teamID: ENV.fetch("TEAM_ID")
  }
)

3. Regenerate match profiles

# Force-regen to pick up new bundle ID + current capabilities
gh workflow run init_signing.yml -f type=appstore -f force=true

# Wait for green, then push the next tag
git tag v1.0.9
git push origin v1.0.9

How to Audit Your Own Repos

I wrote a Python script that cross-checks fastlane config against xcodegen’s project.yml. It flags any bundle in project.yml that’s missing from Matchfile or sync_code_signing.

python orchestrator/lib/match_audit.py --all repos/

Output for my 5 repos before fixing:

=== autoapp-days-until ===
  project.yml targets: 4 (['com.jiejuefuyou.daysuntil', 'com.jiejuefuyou.daysuntil.widget', ...])
  Matchfile bundles:   ['com.jiejuefuyou.daysuntil']
  - error: Bundle(s) in project.yml but NOT in Matchfile: ['com.jiejuefuyou.daysuntil.widget']

=== autoapp-prompt-vault ===
  project.yml targets: 4 (['com.jiejuefuyou.promptvault', 'com.jiejuefuyou.promptvault.ActionExtension', ...])
  Matchfile bundles:   ['com.jiejuefuyou.promptvault']
  - error: Bundle(s) in project.yml but NOT in Matchfile: ['com.jiejuefuyou.promptvault.ActionExtension']

It correctly identified two repos with the same trap. The script is MIT under my autoapp repo.

Why This Bites Indie Devs Specifically

Big teams with full-time DevOps notice this on day 1 because they have a runbook for adding new targets. Indie devs adding a widget for the first time hit this on day N (the first time the widget needs a signed build, which is when you push the next TestFlight tag).

The error message points away from the real bug. Fastlane docs mention app_identifier accepts an array, but don’t say “you MUST list every signed target.”

I lost ~3 days of CI failures to this. The fix was 15 lines across 2 files.

If you’re about to add your first widget / Live Activity / Today Extension / Watch app:

  1. Add the target via xcodegen.
  2. Open fastlane/Matchfile. Add the new bundle ID.
  3. Open fastlane/Fastfile. Update sync_code_signing + update_code_signing_settings + build_app for the new bundle.
  4. Run init_signing.yml -f force=true to regen profiles.
  5. Then push your tag.

Or: drop my match_audit.py into your CI as a pre-commit / pre-push check.

Reusable Audit Script (excerpt)

import re, yaml
from pathlib import Path

def project_yml_bundles(repo: Path) -> set[str]:
    data = yaml.safe_load((repo / "project.yml").read_text())
    bundles = set()
    for tgt, defn in (data.get("targets") or {}).items():
        bid = (defn.get("settings") or {}).get("base", {}).get("PRODUCT_BUNDLE_IDENTIFIER")
        if bid and not bid.endswith((".tests", ".uitests")):
            bundles.add(bid)
    return bundles

def matchfile_bundles(repo: Path) -> set[str]:
    text = (repo / "fastlane" / "Matchfile").read_text()
    m = re.search(r'app_identifiers*(s*[(.*?)]s*)', text, re.DOTALL)
    if not m: return set()
    return set(re.findall(r'"(com.[w.-]+)"', m.group(1)))

def audit(repo: Path):
    missing = project_yml_bundles(repo) - matchfile_bundles(repo)
    if missing:
        print(f"{repo.name}: missing from Matchfile: {sorted(missing)}")

50 lines of Python catches a category of bugs that fastlane itself won’t tell you about.

Cover Image Prompt (1000×420)

“A Xcode project navigator with three target icons: main app, widget extension, and Apple Watch app. A red ‘missing’ badge floats over the widget icon, pointing to a small Matchfile thumbnail labeled ‘app_identifier: […main…]’ — a thread connecting the missing badge to the Matchfile, with a magnifying glass over the […] showing the absent bundle. Editorial illustration style, muted colors.”

Tags: fastlane, ios, ci, widget, gotcha

Internal cross-links:

  • Previous article: dev.to 96 “Swift truncatingRemainder Trap”
  • Series: “Indie iOS Lessons from 4 Apps in 75 Days”
  • Repo: github.com/jiejuefuyou/autoapp

Building iOS CI on Windows? The autoapp-toolkit has a 32-check pre-submit verifier: github.com/jiejuefuyou/autoapp-toolkit.

Need help with an Apple rejection? Book a 15-min audit — $249, 14-day refund.