# Engineering Manager, Tech Advisor & Author > Engineering manager and technical advisor to startups and Fortune 500 companies. Forbes Technology Council member. Featured in the Financial Times. Public Ghost content for AI and LLM tooling. This file includes a bounded export of public pages first, then recent public posts. Append `.md` to any post or page URL to get the content in Markdown (for example, `/example-post.md`). ## Pages _No public content available._ ## Posts ### Hampton Review: Is It Worth It When Your City Doesn't Even Have a Chapter? URL: https://vorobyov.me/hampton-review/ Last updated: 2026-08-10T23:17:13.000Z If you googled "Hampton review" and landed here, quick orientation first. [Hampton](https://joinhampton.com/?ref=vorobyov.me) is a private, vetted community for founders and CEOs, started by [Sam Parr](https://x.com/thesamparr?ref=vorobyov.me) (founder of [The Hustle](https://thehustle.co/?ref=vorobyov.me), host of [My First Million](https://www.mfmpod.com/?ref=vorobyov.me)) and [Joe Speiser](https://x.com/jspeiser?ref=vorobyov.me). To get in you need to run a successful business, pass an interview, and go through a vetting process. Once in, you're placed into a Core group of about eight founders that meets monthly with a trained moderator. Around that sits a Slack community of 1,000+ vetted CEOs, one in a lifetime events, and top tier retreats. Membership runs $15K+ a year. That's the quick overview, and below is a rundown of my own experience, including the part most reviews are too shy to talk about: what Hampton is like, when your city doesn't have an IRL chapter. ## Why I joined I joined Hampton in early 2024, nine years into building my company, [EltexSoft](https://eltexsoft.com/?ref=vorobyov.me). The timing wasn't random – the SVB collapse had rippled through our client base for a year+, startups froze their engineering budgets, projects got cut, and for the first time in a very long while I was staring at a real downturn, with nobody to talk to about it. It couldn't be my team, who needed me confident; or the clients, obviously. What I wanted was a level 1 network – people I could be fully transparent with, people who had been through the same fire and wouldn't flinch. ## Who I am I run EltexSoft, a senior-only software engineering studio. We're around 40 engineers, spread between US and EU. Eleven years in, doing $3-5M a year. We build and rescue products for series A-D startups and established companies. Fortune 500 enterprises, and a couple of Hampton member companies work with our engineers today. Small world :) Agency life is very much different from the product company life, and that matters for this review. Most founder communities are built for SaaS founders with boards and investors. Agency founders have neither – they mostly come bootstrapped, and run on their own capital. When things get hard, there is no board meeting to bring it to – there's just you, solo. ## What Hampton actually is The simplest way I explain it: a room of founders where nobody is selling anything and nobody is impressed by anyone. Before joining, I assumed the Core group would be the nice-to-have and the network would be the real product. In fact, both turned out to be the complementing parts of the same product. Confidentiality is the whole reason the Core works, so I won't say who is in mine or what anyone said. But I can describe the shape of the room. It's a handful of founders running very different businesses – different industries, different time zones, similar scale, and eerily similar problems. Nobody is there to network. Everyone is there because running a company at this level gets lonely in a very specific way, and the only people who truly get it are the ones living it. Hampton overall is founders, CEOs and owners of mostly digital businesses – Hampton says the average member does around $20M in revenue. The range is wide, and nobody cares where you sit in it. That's the refreshing part: it's the first room I've been in where revenue is a data point, not a status symbol. My core group meets monthly. We share the things founders never say out loud – cashflow disruptions and how to actually get out of them, hiring calls we got wrong, founder wear-out which nobody warns you about when you start a mint fresh, new business. There are things I've said in that room I haven't said anywhere else, including at home. And that's the point, and the value of the room. ## Where Hampton paid for itself Two stories, both are true and mine to tell. First, we had a client who stopped paying. Long engagement, relationship soured, invoices went silent. I brought it to the community and got been-there advice from founders who had lived the exact same playbook. The single best line of the whole thread: you don't need BigLaw for this, you need a scrappy local attorney who files these in their sleep. We handled it clean. Second, a past client tried to come at us in a way that was flatly unlawful. I posted, and had a trusted lawyer referral in front of me almost immediately. In a situation where speed and trust are everything, I had both within hours. Neither story shows up on a membership brochure, while each one individually was worth more than the yearly membership fee. ## The IRL part Hampton doesn't have a chapter in Lisbon, where I spend a lot of my time. My membership is digital-first: remote Core, Slack, events when I travel. If you're picturing monthly dinners in a private room in Manhattan, that's not my experience. Full transparency: I'm grandfathered into that setup – these days new members join Core groups in person only. So if your city doesn't have a chapter yet, check with the Hampton team on what's available before you apply. So is Hampton worth it without a local chapter? I've asked myself that hard, because $15K+ a year is a decent sum of money for an agency owner. My answer after two-plus years: yes, because the Core and the network carry the value even remotely. And the local side turned out livelier than expected. In our case, members self-organize – we've had Lisbon meetups, and one Hampton founder put together a full conference where another member and I both spoke. That wasn't Hampton HQ doing it for us – it's the kind of people Hampton lets in, and who you will be in a same Slack team with, one message away. ![](https://vorobyov.me/content/images/2026/08/2026-08-10-15.50.54.jpg) ![](https://vorobyov.me/content/images/2026/08/2026-08-10-15.51.01.jpg) But I'll be straight, if your city has an IRL chapter, you're getting more for the same money. If it doesn't, you need to be the kind of member who shows up digitally and travels occasionally. I am dynamic, responsive and nomadic – not everyone is. ## Who shouldn't join Anyone looking for leads. The solicitation rules are strict and the community smells a pitch instantly, sharper than Reddit. Also anyone who won't commit to the Core cadence, or an approach of giving before asking. Hampton runs on three rules: commitment, confidentiality, candor. If you skip the Hampton meetings when a regular client call comes up, save your money. ## My personal resume I joined during the hardest stretch of my company's life, looking for people I could be honest with, and found them. The advice has been concrete and occasionally company-saving. The bigger thing is quieter: I'm no longer making multi-million-dollar decisions alone. If you're a founder on the fence, especially an agency founder who assumes these communities are all SaaS people talking about ARR: it's not that, and it's worth the application. *Dennis Vorobyov is the founder and CEO of* [*EltexSoft*](https://eltexsoft.com/?ref=vorobyov.me)*, a senior-only software engineering studio trusted by startups and Fortune 500 teams since 2015\. Hampton details at* [*joinhampton.com*](https://joinhampton.com/?ref=vorobyov.me)*.* ### MyFlyRight: 1M Flight Claims on a Rebuilt Platform URL: https://vorobyov.me/myflyright-1m-flight-claims-on-a-rebuilt-platform/ Last updated: 2026-07-28T15:42:47.000Z Under EU Regulation 261, passengers on delayed or cancelled flights are owed up to €600 in compensation — and airlines are structurally uninterested in volunteering it. MyFlyRight built the machine that claims it for them. When we joined in 2016, that machine was a jQuery prototype: a proof that the idea worked, and proof of nothing else. ## The role I led the engagement as technical program lead over a long arc — the prototype-to-platform rebuild and the years of scale that followed. The pattern was the one I've run everywhere: I curated the program, owned what got built and in what order, coached the leads, and kept the founders' legal-market strategy and the engineering plan pointed at the same target. PMs carried the routine; the decisions that would still matter in year five stayed with me. Legal tech sharpens that role, because the product decisions *are* legal decisions. Every jurisdiction and airline pairing behaves differently, and encoding that reality is exactly the kind of work that goes wrong when engineers guess at it alone. ## What we built We didn't renovate the jQuery prototype — we retired it and built the platform from scratch on React and Laravel. The core is the claims wizard: a passenger enters their flight, and an algorithm-driven eligibility assessment decides in real time whether they have a case worth pursuing. Cases that qualify route directly to lawyers. That triage engine is the whole business. Send lawyers junk and the unit economics die; reject valid claims and you're leaving the product's reason to exist on the table. Getting the assessment right — across all EU airlines, all jurisdictions, and the regulation's genuinely messy case law — was the program's center of gravity for years. It's also a perfect example of why the curation role exists: no single sprint ever makes eligibility logic better, but five years of consistent attention does. ## The numbers The platform has processed over one million claims across all EU airlines and jurisdictions, and recovered more than €100 million for passengers. MyFlyRight became one of the top 10 Hamburg-based startups. One million claims is the number I'd put on the engagement's tombstone. A prototype handles a demo; a platform handles the millionth claim the same way it handled the first, under a regulation that airlines have every incentive to contest. The distance between those two states is what the 2016 rebuild decision — scrap, don't renovate — bought. ## What this one taught me Know when a prototype has finished its job. The jQuery version wasn't a mistake; it validated the market for the price of a prototype. The mistake would have been asking it to become the platform. Founders often feel the sunk cost — we made the call early that validation code and production code are different artifacts with different jobs, and the second one deserves its own foundation. And in any domain with an adversary — and an airline avoiding payouts is an adversary — precision is the product. Features are negotiable. The eligibility engine being right was not. --- *Engagement: 2016 onward · jQuery prototype rebuilt from scratch on React + Laravel · algorithm-driven claims eligibility, direct routing to lawyers · 1M+ claims across all EU airlines and jurisdictions · €100M+ recovered · top 10 Hamburg startup.* ### Greek House: From Stalled Releases to a 2024 Exit URL: https://vorobyov.me/greek-house-from-stalled-releases-to-a-2024-exit/ Last updated: 2026-07-28T15:40:09.000Z Greek House prints custom apparel for fraternities and sororities — a business where the entire year compresses into recruitment season, and a release that slips a week might as well slip a semester. When the founder came to us, his previous team had slowed releases to once every few months. The product wasn't dying; it was calcifying, which is worse, because everyone can see the roadmap and nobody can ship it. ## The role My part was advisory and program leadership, and it started the way these engagements should: not with a proposal, but with the codebase. We reviewed what was actually there before promising anything. The verdict mattered — the code was workable; the delivery machine around it was broken. No pipeline worth the name, no path from a merged change to a live release that didn't involve ceremony and fear. So the first thing we built wasn't a feature. We set up CI/CD and started shipping the same week. That week matters more than any architecture decision we made in the four years after it, because it reset the founder's relationship with his own product: changes went live when they were ready, not when the stars aligned. From there I ran the engagement the way I run all of them — curating the program, coaching the team, keeping the founder's priorities and the engineering reality connected, with PMs on the routine and me on everything that couldn't afford to be routine. ## What four years of shipping looks like The cadence became the product's advantage. During rush season — the weeks when the entire customer base orders at once — we held same-day release capability. In a seasonal business, being able to fix or launch anything *today* during the weeks that decide the year is not an engineering vanity metric. It's revenue. The company made the Inc. 5000 multiple years running. It secured official licensing with US colleges — a real moat in collegiate apparel, where the licensed and unlicensed worlds don't compete in the same market. And in 2024, Greek House was acquired. ## The real outcome The acquisition is the headline. It's not the result I care most about. After the exit, the founder started his next company — and came back to build it with us. That's the entire consulting business model compressed into one sentence. A founder who has been through a full arc with you, from a stalled codebase to an acquisition, has seen everything: how you handle the bad weeks, what you're like when the pipeline breaks during rush, whether the invoices match the value. His next company is the only review that can't be faked. ## What I'd tell anyone inheriting a "slow" team Diagnose the delivery system before the code. Codebases get blamed for what pipelines did. A team shipping quarterly almost never has a code problem first — it has a fear problem, and fear grows in the gap between merging and releasing. Close the gap, ship the same week, and let the codebase improve while it's moving. Rewrites are what you do when you can't ship; shipping is what makes rewrites unnecessary. --- *Engagement: four years, advisory and technical program leadership · CI/CD from week one, same-day releases through rush season · Inc. 5000 multiple years · official US college licensing · acquired 2024 · founder now building his next company with us.* ### Nautical Commerce: Embedded Team to SOC 2 and Scale URL: https://vorobyov.me/nautical-commerce-embedded-team-to-soc-2-and-scale/ Last updated: 2026-07-28T15:32:45.000Z Nautical Commerce raised $30M from Drive Capital to build marketplace-as-a-service — the infrastructure layer for companies launching multi-vendor marketplaces without building payments, catalogs, and vendor management from zero. When we joined, the codebase was a year old, written fast the way funded codebases are, and the company needed it to become something enterprises would sign for. ## The role I ran our side of the engagement as its technical program lead — a team of seven engineers embedded into Nautical's organization, with me curating the whole program: what we took on, who did it, how it was reviewed, and what the client heard from us and when. Project managers ran the day-to-day mechanics; I coached the leads, held the quality bar, and stayed the escalation path in both directions — when Nautical needed something to move, and when my engineers needed something unblocked. This is a different shape of work from owning a product outright. Embedded in someone else's engineering org, half the job is technical and half is organizational: making seven external engineers feel — to the client's own team — like colleagues rather than a vendor. The measure of whether that worked isn't mine to give, but Nautical's head of engineering later left a public review calling us the best experience he'd had with an offshore team, and I'll take that verdict over any I could write. ## What we did The inherited frontend wasn't going to carry an enterprise product, so we rebuilt it. Then the work that turns a marketplace demo into marketplace infrastructure: payment processing, multi-currency, multi-language, and tax logic — the four features every founder underestimates because each one is a lattice of edge cases wearing a one-line label. Tax logic across jurisdictions alone is a program of work, not a ticket. The second arc was hardening: taking the platform enterprise-grade and SOC 2 ready. SOC 2 readiness is mostly invisible engineering — access controls, audit trails, process discipline — and it's exactly the kind of work that dies without program-level ownership, because no individual sprint ever feels like the one where compliance matters. Keeping that arc moving alongside feature delivery was, more than anything else, what my role was for. At scale, the platform was processing over 200,000 transactions a month. Nautical's assets were later acquired by Traide. ## What this engagement taught me Rescue framing undersells what this work actually is. The one-year-old codebase wasn't broken — it was appropriate for its age, and the mistake most teams make is declaring bankruptcy on code that simply hasn't been asked to be enterprise software yet. We rebuilt what genuinely couldn't carry the load (the frontend) and evolved everything else in place. Knowing which is which is the judgment call the whole engagement rests on, and it's a program decision, not a pull request. And on embedded teams: the vendor-client boundary is exactly as visible as you allow it to be. Every process we ran — reviews, standups, escalations — was designed so the boundary disappeared. When it works, the client's head of engineering writes the review ours got. --- *Engagement: 7 embedded engineers · Frontend rebuild, payments, multi-currency, multi-language, tax logic · Enterprise-grade, SOC 2 ready · 200K+ monthly transactions · Client raised $30M from Drive Capital; assets later acquired by Traide.* ### Ripe: from Rebuild to Exit URL: https://vorobyov.me/ripe-from-rebuild-to-exit/ Last updated: 2026-07-28T08:14:36.000Z Ripe was a real business before we wrote a line of code. A functioning catering operation in Manhattan with a kitchen, drivers, and a customer list that included DigitalOcean, MongoDB, and Kickstarter — companies that paid every week for lunch, not for software. In 2016 the founders decided the operation needed a platform, and that's where I came in. ## The role I wasn't the one writing the code, and this post won't pretend otherwise. My job was everything around the code: I ran the engagement as its technical program lead — scoping what would get built and in what order, assembling and coaching the team, owning the client relationship, and making the calls when something had to give. Project managers handled the routine ceremonies. I made sure nothing important ever became routine. That distinction matters for how a project like this survives five years. Engineers rotate, priorities shift, a catering company's Tuesday crisis is never in the sprint plan. The constant was the program: one person accountable for the whole arc, from the first architecture conversation to the diligence data room. ## What we built The platform covered the entire operation, because a catering business is an operation first: ordering for the corporate customers, kitchen workflows so the food actually got made in the right sequence, delivery tracking so it arrived, and billing so Ripe got paid. Four systems that each look simple on a whiteboard and none of which are simple when the kitchen is live at 10am and the client's lunch is at noon. We built it from scratch in 2016 and then did the less glamorous thing: supported it for five years. Real support — the product evolving with the business, not a maintenance retainer. At steady state the platform was processing over a thousand orders a week. ## The exit In 2020, Hungry — a larger food-tech marketplace — moved to acquire Ripe. Acquisitions of software-enabled businesses come with technical due diligence, which is the one moment an outside team reads your codebase with money on the line and no reason to be polite. Hungry's team went through the platform. They liked what they found. The acquisition closed in September 2020. I think about that as the real deliverable of the engagement. Not the launch — launches are easy to celebrate and forget. Five years later, a skeptical third party audited the work product and it held up well enough to close a deal on. That's the standard I run programs to: build as if someone will eventually read this with a checkbook in hand, because someone eventually did. ## What I'd tell the 2016 version of this engagement Treat the kitchen as the primary user. The corporate customers had the money, but the operation lived or died on whether the people making and moving food trusted the system. Every hour spent watching the actual workflow paid back tenfold in what we didn't build. And keep the program continuous. The projects that decay are the ones where leadership attention arrives in bursts — at kickoff and at crisis. Ripe had neither burst, which is why it had a quiet, clean exit instead. --- *Engagement: 2016–2021 · Platform: ordering, kitchen operations, delivery tracking, billing · 1,000+ weekly orders · Acquired by Hungry, September 2020, following technical due diligence.* ### Netdata on real hardware: registration, sensors, and alert templates that don't lie URL: https://vorobyov.me/netdata-on-real-hardware-registration-sensors-and-alert-templates-that-dont-lie/ Last updated: 2026-07-28T07:57:29.000Z I run Netdata on the machines I care about, including a Supermicro server board whose BMC knows more about the hardware than the OS does. This is the setup guide I wanted when I started: how to install and register a node properly, how to get hardware sensors flowing — both lm-sensors and IPMI — and how alert templates actually work, including the syntax everyone copies without understanding. Everything below was run on a live system. Where a step has a failure mode, I hit it, and I'll tell you where. ## Install and register in one command Netdata's kickstart script installs the agent and can claim it to Netdata Cloud in the same run. Grab the claim token from your Space (Add Nodes shows the full command), then: ```bash wget -O /tmp/netdata-kickstart.sh https://get.netdata.cloud/kickstart.sh sh /tmp/netdata-kickstart.sh \ --claim-token YOUR_TOKEN \ --claim-rooms YOUR_ROOM_ID \ --claim-url https://app.netdata.cloud ``` Three things worth knowing that the happy path doesn't tell you: **If Netdata is already installed from your distro's packages**, add `--claim-only`. Without it the script may decide to manage the install itself, and now you have two update mechanisms fighting over one agent. **The config-file alternative** is cleaner for anything automated. Instead of flags, drop a `claim.conf` next to `netdata.conf`: ```ini [global] url = https://app.netdata.cloud token = YOUR_TOKEN rooms = ROOM_ID ``` Restart the agent and it claims itself. This is the right shape for Ansible, cloud-init, or a Coolify service definition — the token lives in config management, not in shell history. **Re-claiming a node** (moved Spaces, cloned a VM, rebuilt a machine) requires removing `/var/lib/netdata/cloud.d/` and restarting the agent first. A cloned VM that kept that directory will fight the original for the same node identity, and the symptom — the node flickering online and offline in Cloud — looks nothing like the cause. Registration is optional, to be clear. The agent serves its own full dashboard at `http://localhost:19999` with per-second granularity and no account. Cloud gives you multi-node views, alert routing, and access without a VPN. I use both: local for debugging, Cloud for noticing. ## Sensors, part one: lm-sensors For desktop and workstation boards, hardware monitoring comes from the kernel's hwmon interface, surfaced by lm-sensors: ```bash sudo apt install lm-sensors sudo sensors-detect # answer yes to the safe defaults sensors # verify readings appear ``` Netdata picks this up through the **go.d sensors module** — the old charts.d collector is history. If `sensors` prints readings but Netdata shows no charts, the usual cause is the binary not being on the netdata user's PATH; point at it explicitly: ```bash cd /etc/netdata sudo ./edit-config go.d/sensors.conf ``` ```yaml jobs: - name: sensors binary_path: /usr/bin/sensors ``` Always use `edit-config` rather than editing files under `/usr/lib/netdata/conf.d/` — it copies the stock file into `/etc/netdata/` and your changes survive upgrades. ## Sensors, part two: IPMI, where the real data is On server boards the interesting numbers — every voltage rail, every fan, chassis intrusion, PSU state — live in the BMC, and the OS-level sensors see a fraction of them. Netdata reads the BMC through the **freeipmi plugin**: ```bash sudo apt install freeipmi netdata-plugin-freeipmi sudo ipmimonitoring | head -5 # test the BMC directly, before blaming Netdata ``` ``` ID | Name | Type | State | Reading | Units | Event 4 | CPU Temp | Temperature | Nominal | 49.00 | C | 'OK' 71 | System Temp | Temperature | Nominal | 38.00 | C | 'OK' ``` If `ipmimonitoring` works, the plugin will work — it's the same library. You can also run the plugin standalone to watch it emit chart definitions: ```bash sudo /usr/libexec/netdata/plugins.d/freeipmi.plugin 5 ``` Note the plugin binary is setuid root with group `netdata` — IPMI device access needs privilege, and that permission set is intentional, not a packaging accident. **The gotcha that cost me a debugging session:** on some boards the BMC is not ready to answer IPMI requests at the moment Netdata starts, the plugin fails its first attempt **silently — nothing in the journal — and never retries**. Every reboot: all charts fine, IPMI absent, and a manual `systemctl restart netdata` fixes it. I confirmed the race by comparing `systemctl show netdata --property=ActiveEnterTimestamp` against `uptime` — Netdata had entered active state in the same minute the machine booted. Don't fix it with `ExecStartPre=/bin/sleep`. Delaying the whole agent blanks every dashboard to buy one collector, and 30 seconds wasn't enough on my board anyway. Fix it with a one-shot systemd timer that restarts Netdata once the BMC has settled: ```bash sudo tee /etc/systemd/system/netdata-ipmi-fix.service > /dev/null << 'EOF' [Unit] Description=Restart Netdata for IPMI [Service] Type=oneshot ExecStart=/bin/systemctl restart netdata EOF sudo tee /etc/systemd/system/netdata-ipmi-fix.timer > /dev/null << 'EOF' [Unit] Description=Restart Netdata 2 minutes after boot for IPMI [Timer] OnBootSec=120 Unit=netdata-ipmi-fix.service [Install] WantedBy=timers.target EOF sudo systemctl daemon-reload sudo systemctl enable netdata-ipmi-fix.timer ``` Netdata is up and charting from second zero; the collector that depends on the BMC gets a clean second chance at boot+120. ## Writing alert templates This is where most people stop at copy-paste, and it's a shame, because the health engine is small enough to actually understand. Health entities live in `.conf` files under `health.d/`, edited the same way as everything else: ```bash cd /etc/netdata sudo ./edit-config health.d/cpu-temp.conf ``` An entity starts with one of two words, and the difference is the single most important thing on this page: - **`template:`** attaches to a *context* — a chart type — and applies to **every chart of that type**, current and future. `on: disk.space` covers every disk, including the one you plug in next month. - **`alarm:`** attaches to **one specific chart ID**. `on: disk_space._mnt_data` covers exactly that mount. Write templates by default. Reach for `alarm:` only when one instance genuinely needs different treatment. Here's a complete, working entity for CPU temperature from the IPMI data above: ``` template: cpu_temp_high on: ipmi.sensor_temperature_c lookup: average -1m unaligned units: Celsius every: 10s warn: $this > (($status >= $WARNING) ? (75) : (80)) crit: $this > (($status == $CRITICAL) ? (85) : (90)) delay: down 5m multiplier 1.5 max 1h summary: CPU temperature info: Average CPU temperature over the last minute to: sysadmin ``` Line by line, the parts that matter: `**lookup**` is a small query language: `average -1m unaligned` means "average of the last minute." You can take `min`, `max`, `sum`, restrict to specific dimensions (`of user,system`), or use percentages. The result lands in `$this`. If you need arithmetic instead of a query, `calc:` takes an expression. An entity needs at least one of `lookup`, `calc`, `warn`, or `crit`. **The warn/crit conditionals are hysteresis, not decoration.** Read `$this > (($status >= $WARNING) ? (75) : (80))` as: the alarm *trips* at 80 but only *clears* below 75\. Without that band, a sensor hovering at the threshold flaps between states and pages you every ten seconds. Everyone copies this pattern from the stock configs; almost nobody can say what it does. Now you can. **`delay: down 5m multiplier 1.5 max 1h`** debounces the notifications on recovery so a bouncing alert escalates its own silence instead of spamming you. `**to:**` routes to a recipient role defined in `health_alarm_notify.conf` — and `to: silent` is the sanctioned way to keep an alert visible on the dashboard while sending nothing. Far better than deleting entities you might want back. Two operational rules that bite people: **Overriding a stock alert means owning the whole file.** If you create `health.d/cpu.conf` in `/etc/netdata`, the stock file of the same name is ignored *entirely* — your copy must contain every entity you still want from it, not just the one you changed. Give overrides their own filename (`my-overrides.conf`) unless you mean to replace the set. **Reload health without restarting the agent:** ```bash sudo netdatacli reload-health ``` Then verify the entity actually loaded, with your thresholds, straight from the API: ```bash curl -s "http://localhost:19999/api/v1/alarms?all" | python3 -m json.tool | grep -A4 cpu_temp_high ``` If it's not in that output, the file didn't parse — and the health log will say why, which is more than the freeipmi plugin ever did. ## What I'd do differently **Test the data source before the collector, every time.** `sensors` before go.d, `ipmimonitoring` before the plugin. Half of all "Netdata doesn't show X" is the layer underneath not answering. **Alert on collector presence, not just values.** My IPMI plugin failed silently for weeks of reboots while every dashboard stayed green. A monitoring system that loses a collector without telling you has failed at its one job — a template on the chart count, or an external check that the `ipmi.*` contexts exist, closes that hole. **Write the hysteresis version from the first alert.** The plain `warn: $this > 80` you write on day one becomes the flapping pager you debug on day thirty. --- *Netdata 2.10.3 on Ubuntu · netdata-plugin-freeipmi against a Supermicro BMC · go.d sensors with lm-sensors · claimed to Netdata Cloud via kickstart · health entities in `/etc/netdata/health.d/`, reloaded with `netdatacli reload-health`.* ### What It Took to Write and Publish 42 URL: https://vorobyov.me/what-it-took-to-write-and-publish-42/ Last updated: 2026-07-28T06:33:49.000Z # How I Wrote 42 The first thing I ever built with AI was a Slack bot that answered questions about our own company documentation. It took an afternoon. It was ugly. It hallucinated about our vacation policy. Within a month it was answering around two hundred questions a week, and people at EltexSoft stopped asking me things they could ask it instead. That bot is why there's a book. Not directly. What happened after is that I started writing things down. Which tool, which configuration, which prompt, which failure. It was internal documentation, the kind nobody reads. Then it became articles on our site. Then someone asked whether there was a full version, and the honest answer was no, because the full version lived in fragments across a workspace and my head. *42: The AI Builder's Stack* publishes on August 15\. Twenty-two chapters, 396 pages, just under a hundred thousand words, written while running a forty-person studio. This is how it got made. ## Twelve chapters became twenty-two The first outline was a toolbelt. Terminal, editor, version control, deployment. Twelve chapters, each one a tool. It fell apart around chapter six. Every chapter kept pointing at a chapter that didn't exist. You can't explain Claude Code without explaining what a good instruction file looks like, which means explaining how to write a spec, which means a chapter on PRDs. You can't explain MCP without first explaining why an API isn't enough. You can't recommend AI code review without confronting how much more often AI-written code fails a security pass, which is its own chapter. Twelve became twenty-two, in six parts, arranged so a reader who has never opened a terminal can start at chapter one and a reader who ships daily can start at chapter twelve. The last two written were Chapter 7 on AI-powered IDEs and Chapter 21 on transforming a small team. Twenty-one is the one people will still care about in five years, when every tool in the other chapters has been acquired or deprecated. Letting the scope grow cost me about a year. A half-book on a moving target is worth nothing, so it was the right trade, but it's the reason this took almost two years instead of one. ## Every chapter has the same skeleton I didn't figure this out until chapter four, and then I went back and rebuilt the first three. Open with something that happened. A client problem, a mistake, a workflow that changed. Then explain the thing from absolute zero, assuming the reader has never heard of it, because the promise of the book is zero to deep and skipping the zero part is how technical writing loses people in the first page. Then go deep: architecture, configuration, the features that matter against the ones you can ignore, benchmark numbers with the source named. Then show what I do, with the config files and code that make it reproducible. Then compare it honestly to the alternatives, including where the alternative wins. Then close with what to do first. Having a fixed skeleton meant I could write a chapter in pieces across a week of interruptions and it would still hold together. That mattered more than any tool. I was not writing in retreats. I was writing between calls. The research came first and separately. For each chapter I'd build a research artifact of five to twenty thousand words before writing a line: official docs read directly rather than summarized, independent benchmarks alongside vendor claims, pricing confirmed on the pricing page with the month noted. Then the chapter got written from that. Keeping research and writing in different passes is the single biggest quality decision I made, because when they're mixed you end up writing toward whatever you found first. ## The editing pass was quantitative At some point I stopped reading my own prose usefully. Twenty-two chapters in, I could not tell my voice from my tics. So I ran the manuscript as data. A cross-chapter scan found that "actually" appeared 73 times, "genuinely" 22 times. Terminology had drifted: open-source and open source, real-time and realtime, sometimes inside the same chapter. Chapters 8 and 21 had slid into a different register than the rest of the book. None of that shows up when you read. All of it shows up when you count. I thinned the filler words except where they were doing contrast work, standardized the terminology, and pulled 8 and 21 back into voice. I also paid for ProWritingAid and turned most of it off. Its readability, pacing, and vivid-verb rules fight technical writing constantly, because a sentence explaining row-level security is supposed to be flat. What earned its keep was consistency checking, grammar, and a custom banned-words list. Everything else was arguing with me about a job it didn't understand. Two things came out of that pass that had nothing to do with grammar. First, every client name got anonymized: a specific real estate platform became "a major real estate platform client," and so on down the list. Second, I went back and added humor deliberately, and then had to vary it, because my default joke shape is "while doing X" and twenty-two chapters of one joke shape is worse than no jokes. ## Writing about the AI stack, using the AI stack Where it helped without argument was production. I started formatting in Atticus, the standard self-publishing tool, and killed it in a day: no code-block element, strips indentation, flattens the PDF text layer. For a book with a hundred-plus code samples and YAML configs that's disqualifying, and I found out by running a deliberately hostile test chapter through it rather than importing the whole manuscript and discovering it at page 200. I rebuilt the interior on Pandoc and XeLaTeX, driven by Claude Code, so one command produces the print PDF and the EPUB from the same source files. The first clean build took days. Every rebuild after that took seconds, which is the only reason I could keep editing this close to the deadline. That build also caught 158 failures in a single pass, all of them the same mistake I'd been making for two years: markdown links written as `[supabase.com](supabase.com)`, with no scheme. EPUBCheck rejects every one. A twelve-line script fixed all 158 in a second. By hand it would have taken a weekend and I'd have missed some. Now the part I think about most. Early on, a placeholder advance-praise page got created with seven endorsements on it. Names, titles, companies, quotes. All invented, all labeled as a placeholder in the moment. Then it sat in the manuscript folder for months while the file list grew past forty and I stopped opening the files I thought were finished. It surfaced in July, during a verification pass on the compiled interior, and only because that check ran against the built PDF instead of against my memory of what was in it. Seven fabricated endorsements from seven people who don't exist, one recompile away from shipping inside a book whose entire argument is that you verify claims instead of trusting them. That was my failure, not the tool's, and the fix is a rule I now apply to everything: open the artifact and look at it. Not the summary of the artifact, not the report about it, not what you remember putting there. The file. The praise page that ships has three names on it. Riley Davidson, an engineering manager at Instacart. Richard Cave at VSCO, formerly an engineering manager at Apple. Ivan Bercovich, an operating partner at ScOp Venture Capital. All three read the manuscript. A fourth one came later, with quite a few more still pending. This taught me the useful thing about blurbs: assume a solid portion of your asks evaporate, and never design a back cover around a quote you don't have in writing. The thing I didn't expect from any of this is what the epilogue ended up admitting. I started the project thinking I knew my stack well. Researching twenty-two chapters showed me how much of it I'd been running on autopilot. The chapters on MCP, Skills, and agentic workflows forced me to formalize things that had been intuition, and the chapter on Chinese models forced me to check assumptions I'd been making about tools I depend on daily. The book changed how I work more than it documented it. ## The cover took seven tries I hired Mike Trent through Reedsy. He spent two decades at Wiley art-directing their technical and professional lines, which is exactly the shelf this book sits on, and the benchmark I gave him was Stripe Press and Holloway. Restraint, typography, no robots, no circuit boards, no blue glow. He came back with seven concepts. We landed on deep forest green with a vertical stack of diamonds and a large serif 42, which turned out to be his own first instinct as well as mine. The one I still think about is the one we killed: a navy cover built from forty-two rows of layers with the gaps forming decision paths through them. It was the smartest thinking in the set and it was too clever for a thumbnail, which is the whole problem with book covers. That's the argument for hiring someone. A generated cover would have saved four figures and cost the book its credibility in the half-second before anyone reads a word. ## The free chapters are strategy, not generosity Trimmed versions of all twenty-two chapters are free at eltexsoft.com/course. Web chapters run two to three thousand words. Book chapters run four to six. The rule I wrote for myself when cutting them: teach the what and the why, sell the how. The opening story stays uncut. The zero-to-understanding section stays uncut. The closer stays uncut. What comes out is the configuration walkthrough, the working code, the config files, the full comparison tables, and the sections where I go step by step through my own workflow. A web reader finishes able to explain the topic at a standup. A book reader finishes able to run it. People told me I was giving away the product. *Eloquent JavaScript* is free online and the print edition sits at the top of its Amazon category. *Pro Git* is free under Creative Commons and the paperback has been in print over a decade. Buyers self-select. The free version builds the audience, the paid version captures the people who want the uncut thing on a shelf. Pricing landed at $9.99 for Kindle, $24.99 paperback, $39.99 hardcover, after a pre-order at $6.99\. The Kindle number isn't taste. KDP's 70% royalty band stops at $9.99 and drops to 35% above it, so you'd have to charge twenty dollars to earn the same amount. The hardcover exists because hardcovers are rare in self-published technical books, which is exactly why it's worth having one. No sponsors, no affiliate links, nobody paid to be mentioned. That's not a moral position, it's a functional one. The book tells you which tools are marketing ahead of reality. That sentence is worth nothing if a vendor bought a page. ## The unglamorous half Somewhere around month eighteen I learned that publishing a book properly means running a second project alongside the first one. There's an imprint, Sub-Etha Press, which is a trade name over a small Wyoming company rather than a publishing house. There are three ISBNs from Bowker, so the imprint owns its own identifiers instead of borrowing Amazon's. There's a registered copyright, a library catalog number, and an insurance policy. The name is a nod to Douglas Adams, since the Sub-Etha is the network in *The Hitchhiker's Guide to the Galaxy* and the title is 42. None of it makes the book better. It's what lets a book that names thirty-one people and rates twenty-five commercial products say what it says. If you're writing something opinionated, start it early, because all of it has dependencies on your publication date and none of it moves fast. ## What I'd do differently **I'd fix the chapter skeleton before writing chapter one.** Rebuilding the first three chapters to match the pattern I found in chapter four cost a week I didn't have. **I'd pick the production toolchain before writing a word.** Two years of markdown written without a target format is two years of small inconsistencies that all arrive at once during the first build. **I'd never create a placeholder that looks finished.** If a page isn't ready it should be empty, or it should say TODO in forty-point type. A well-formed fake is the most dangerous thing in a project, because it passes every glance. **I'd freeze the tool chapters six months out and let them age.** I rewrote pricing and feature sections repeatedly chasing accuracy, and some of it changed again anyway. My own epilogue says the book has a half-life measured in months. I should have believed it sooner and spent that time on the chapters that don't decay. **I'd ask twice as many people for blurbs, twice as early.** The free-chapter decision is the one call I've had no second thoughts about at all. --- *42: The AI Builder's Stack publishes August 15, 2026 from* [*Sub-Etha Press*](https://subethapress.com/?ref=vorobyov.me)*. It's* [*on Amazon*](https://www.amazon.com/dp/B0H8WQZ7B8?ref=vorobyov.me) *in Kindle, paperback, and hardcover. Trimmed versions of all twenty-two chapters are free at* [*eltexsoft.com/course*](https://eltexsoft.com/course?ref=vorobyov.me)*.* ### What my roof antenna actually hears: three months of ADS-B on the Portuguese coast URL: https://vorobyov.me/what-my-roof-antenna-actually-hears-three-months-of-ads-b-on-the-portuguese-coast/ Last updated: 2026-07-28T05:18:45.000Z Every airliner overhead is shouting its position into the void, unencrypted, about twice a second, and anyone with €30 of radio hardware can listen. I've been running a receiver on the coast near Torres Vedras since May. This post is about what those signals actually are, what the receive chain does to them, why my station's maximum range is a weather report rather than an antenna spec, and what the Flightradar24 numbers genuinely measure — because most feeder write-ups, including the first draft of this one, get that part wrong. ## What is actually in the air Transport aircraft carry a Mode S transponder. On top of the interrogation-reply traffic it exchanges with radar, the transponder spontaneously broadcasts **1090 MHz Extended Squitter** messages — this is ADS-B: Automatic (nobody asks), Dependent (position comes from the aircraft's own GNSS, not from radar), Surveillance, Broadcast. Each message is 112 bits, pulse-position modulated at 1 Mbps, so a complete transmission lasts 120 microseconds including the preamble. Inside: a 24-bit ICAO address unique to the airframe, a type code, and the payload — airborne position, velocity, callsign, altitude — protected by a 24-bit CRC. An aircraft in cruise emits position messages roughly twice a second, velocity about as often, identity every five seconds or so. Nothing is encrypted and nothing is authenticated. That's not an oversight; the system is designed so that any aircraft or ground station can use any other aircraft's broadcasts for situational awareness. It's also why a hobbyist receiver is a first-class citizen: I'm decoding exactly the same bits an ATC ground station decodes. Two consequences matter for everything below. First, **one message is not one aircraft and not one position** — the message stream is a mix of types, and the ratios between them show up directly in feeder statistics. Second, position isn't sent as raw latitude/longitude. It's **Compact Position Reporting**: coordinates compressed into 17 bits each by dividing the earth into zones, with alternating "even" and "odd" frame formats. A decoder needs one of each within about ten seconds to fix a position unambiguously. A single frame narrows the aircraft to one spot per zone — ambiguous globally, resolvable locally only once the decoder already has a reference. This is why a station can *hear* an aircraft long before it *plots* it, and why weak, fragmentary reception at the edge of range produces hits but few positions. ## The receive chain, and why each part is there My chain is deliberately short: antenna on the balcony, \~3m of coax, a FlightAware Pro Stick Plus, a Raspberry Pi on the porch. The Pro Stick Plus is an RTL-SDR — a DVB-T tuner chip repurposed as a general receiver — with two additions that matter at 1090 MHz. An LNA in front of the tuner, and a **SAW filter** passing only a narrow band around 1090\. The filter is the important one. The RTL chip digitizes with an **8-bit ADC**, which gives you about 48 dB of usable dynamic range to fit the entire RF environment into. A broadcast FM transmitter or a GSM base station in the neighbourhood doesn't need to be *on* 1090 MHz to ruin reception — it just needs to be strong enough that the ADC spends its 8 bits representing the interferer, quantizing your microvolt aircraft signals into the noise. The filter throws the interferers away before the ADC ever sees them. The same 8-bit budget is why **gain tuning is a real step, not a formality**. Max gain lets you decode weak frames from 300 nm out — and clips the message from the A320 passing overhead at FL100, whose signal is a million times stronger. Every clipped frame fails CRC and is discarded. The right setting is the one that maximizes *decoded messages*, not signal strength, and `dump1090-fa` will happily show you the percentage of strong messages so you can back the gain off until overhead traffic stops saturating. Why no external LNA in front of all this: amplification helps only when cable loss between antenna and receiver is eating your signal-to-noise. Over 3 metres of decent coax it isn't, the dongle already has an LNA behind its filter, and a second unfiltered gain stage in front would amplify exactly the out-of-band garbage the SAW filter exists to remove — spending ADC range on it in the process. On a 30-metre mast run, different answer. Here, it's a downgrade sold as an upgrade. Software: `dump1090-fa` does the actual work — detecting preambles in the sample stream, demodulating, CRC-checking (and single-bit error correction using the CRC), pairing CPR frames into positions. `tar1090` is the map, `graphs1090` the history, and the FR24 feeder ships the decoded stream out. The FR24 stats page lists my station's local IP as 172.19.0.4 with no MAC address — a Docker bridge subnet, because the feeder runs containerized and the stats page is telling you about the container, not the Pi. ## Range is geometry first, weather second 1090 MHz is line-of-sight. The governing equation is the radio horizon: distance in nautical miles ≈ **1.23 × √height-in-feet**, the 1.23 already accounting for standard atmospheric refraction bending the path slightly around the curve of the earth. My antenna sits maybe 8 metres up — call it 26 feet — worth about 6 nm. The aircraft contributes the rest: at FL350, 1.23 × √35,000 ≈ **230 nm**; at FL400, about 246\. Add my 6 and the physics says this station should top out around **236–252 nm** for aircraft at cruise, and far less for anything low. Now look at the FR24 stats: maximum range **349 nm**. That's not the antenna being heroic. Reception meaningfully beyond the standard radio horizon happens through **tropospheric ducting** — temperature inversion layers, common over ocean and coast, that trap the signal and bend it along the surface far past the geometric limit. A coastal Atlantic site gets these conditions regularly. The 349 came from a handful of frames on the right evening. **Maximum range is a weather statistic.** It tells you what the atmosphere did once, not what the station does daily. And there's a second, giveaway detail. The global ranking around my position reads: 349, 349, 349, 347, 349\. My Portuguese neighbours: 349, 349, 349, 349, 349\. Hundreds of stations do not independently converge on the same number. **FR24 discards position reports beyond \~350 nm as implausible** — a sanity filter against bad decodes and CPR ambiguity errors, which genuinely do produce aircraft teleporting across hemispheres. So every reasonably good station's "max range" pins at the cap, and the column is a checkbox, not a differentiator. Any comparison of feeders by maximum range is comparing who has hit the filter. The number that describes the station is **average range: 231 nm** — sitting almost exactly on the \~236 nm ceiling the horizon math predicts for cruise traffic from this elevation. That's the satisfying result of the whole project: the station sees essentially to the physical horizon, and the remaining gap is geometry no hardware purchase can move. The range histogram tells the same story from the other side. On a representative day: 2,030 position reports under 50 nm, 3,205 at 50–100, 2,016 at 100–150, 1,658 at 150–200, 626 beyond 200\. The peak at 50–100 nm is where two curves cross — the area of each range ring grows with distance (more sky, more aircraft), while the minimum altitude an aircraft needs to clear my horizon also grows with distance (at 200 nm, only cruise traffic is visible at all; at 30 nm I can see an ATR climbing out). Beyond 200 the report count collapses not because aircraft thin out but because the geometry only admits the top of the altitude band — and each of those distant aircraft yields fewer decodable frames per minute anyway, weak fragmentary reception being exactly the CPR-pairing problem from earlier. The polar plot: clean past 200 nm to the west, over open Atlantic, and sharply truncated to the east, where the terrain rises inland. A hill in the first Fresnel zone is a wall at this frequency. Gain does not fix it, height barely fixes it, and any purchase justified by "improving eastern coverage" is decoration. On gain, one more piece of physics the antenna market obscures: a passive antenna creates gain only by **reshaping** the pattern. A 9 dBi collinear takes energy from high elevation angles and flattens the donut toward the horizon. That's the right trade for a low station chasing distant cruise traffic — but it means *worse* reception of aircraft overhead, and on a rolling deck or a windy mast a very flat pattern can literally wobble off the horizon. The DPD Productions 9 dBi I've specced (€157.95, with 16ft of RG8X at \~€40) is a considered pattern trade against my current 5.5 dBi, not "a better antenna." I expect it to move the *position count* in the 150–200+ bands and to move average range a little; it cannot move the horizon, and I've written this paragraph before installing it precisely so the before/after is on record. ## What the FR24 numbers actually count Feeder stats use three words that get conflated constantly, including by me in an earlier draft: - **Hits** — messages received and forwarded. All types: position, velocity, ident. - **Positions** — decoded position fixes, i.e. successfully paired CPR frames. - **Aircraft seen** — unique ICAO 24-bit addresses. My stats snapshot, taken at 04:30 UTC — worth stating, because these counters are *today so far*, four and a half quiet overnight hours, not daily totals: 21,910 hits, 8,656 positions, 76 aircraft. The hits-to-positions ratio (\~2.5:1) is roughly what the message mix predicts — position frames are only a fraction of the squitter stream, and edge-of-range aircraft contribute hits without positions. The full-day aircraft counts over the last week: 940, 967, 887, 1,053, 967, 973 — a steady \~950 with the Saturday peak. Uptime since first coming online on 4 May: **100%**, which says less about my engineering than about how little a Pi decoding a 1 Mbps bitstream is actually being asked to do. Beyond raw ADS-B, a networked feeder does one more job: **multilateration**. Older aircraft and most military traffic carry Mode S without ADS-B Out — they transmit identity and altitude but never position. FR24 solves for their position by comparing the arrival timestamps of the same frame at four or more receivers, which means every feeder is also a node in a distributed passive radar. Your station contributes to tracks it could never produce alone. Which is also the honest answer to "why does a network with tens of thousands of receivers want mine." Feeder coverage maps onto population; over the Atlantic it thins to nothing, and satellite ADS-B fills that gap at much lower update rates. A station on the Portuguese coast with an unobstructed western horizon is watching the approaches to the continent — traffic funnelling to and from the North Atlantic tracks, Madeira, the Azores, the Canaries — from one of the last patches of ground that can. The ranking (20th in Portugal, 732nd globally, score 10,797) rewards exactly that: FR24's score weighs coverage contribution, which is why a modest station in the right place outranks a better one in a saturated city. In exchange, all three networks I feed — FlightAware, FR24, ADS-B Exchange — comp their premium tiers, which on the first two is a genuinely good trade for a €250 build. ## Three corrections to the standard advice **"More gain" is a pattern decision, not a volume knob** — both at the antenna (elevation trade) and at the SDR (dynamic-range trade). The metric to optimize is decoded messages per second, visible right in graphs1090, and it is routinely maximized by *reducing* gain. **Judge a station by average range and message rate, never maximum range.** The max is capped by the network, reached via ducting, and identical for everyone on your ranking page. Average range against your computed horizon tells you whether the install is done or whether there's SNR still on the table. **Height beats everything else you can buy, and terrain beats height.** The 1.23√h relation is brutally sublinear — doubling antenna height on a low install buys a couple of nautical miles of horizon. The site is the decision; the hardware is details. ### The Claude Code setup I actually run URL: https://vorobyov.me/the-claude-code-setup-i-actually-run/ Last updated: 2026-07-28T05:23:06.000Z Most people publish a single `CLAUDE.md` and call it a setup. Mine started that way too. The Tawen repo had one file at 48,890 characters — every convention, every command, every compliance note, loaded into context at the start of every session whether it was relevant or not. It got worse results than the 5,648-character file that replaced it. That is the whole lesson, and Anthropic's own documentation now says it plainly: files over 200 lines consume more context and reduce adherence. Instructions are delivered as context, not as enforced configuration. Claude reads them and tries to follow them. There is no guarantee. So the question stopped being "what should I write in CLAUDE.md" and became "what belongs in context, what belongs in a conditional file, and what shouldn't be left to the model at all." Everything below is in a public repo: [github.com/roadhero/claude-code-setup](https://github.com/roadhero/claude-code-setup?ref=vorobyov.me). MIT, no attribution required. Take what's useful. ## Two tiers There is exactly one global file and one project file. That's the architecture. **The global tier** lives in `~/.claude/` and holds everything true regardless of what I'm building: - `CLAUDE.md` — sections 1 through 18\. Workflow, git rules, coding guidelines, secrets handling, anti-patterns. Stack-agnostic. - `rules/` — platform packs that load conditionally. - `agents/` — the default subagent roster. - `hooks/` — the enforcement layer. - `skills/new-repo/` — a scaffolder. **The project tier** is a short `CLAUDE.md` at the repo root holding only section 19: stack, quality gate, release pointers, compliance scope. Nothing else. The split matters for a mechanical reason. Claude Code walks up the directory tree and concatenates every `CLAUDE.md` it finds, ordered root-down, so the file closest to where you launched is read last. The global spine is identical across every session on the machine, which makes it a good prompt-cache citizen. The project file is the only thing that changes per repo, and it's the last thing read. Configure once, then mostly leave it alone. ## What section 19 looks like filled in Abstract advice about "project context" is worthless. Here is the shape, from Tawen's actual file: - **One-paragraph description.** What it is, in prose, including what it deliberately does not do. - **Stack.** Kotlin 2.3 / K2, JDK 17, `minSdk 28`, `targetSdk 36`, the framework list with pin policy, storage schema version, build tooling, test runner, CI. - **Local quality gate.** The exact command, runnable from a fresh clone, with `--no-daemon` to match CI: ```bash ./gradlew spotlessCheck detekt :app:lintDebug \ :app:testDebugUnitTest :app:verifyRoborazziDebug \ :app:compileReleaseKotlin --no-daemon ``` - **Release pointers.** Live version and versionCode, what's in flight. - **Compliance scope.** For Tawen: US and Canada only via Play Console geo-restriction, analytics consent-gated and off by default, no health values in telemetry. For HashKeep, an encrypted SMS backup app: the Play SMS policy backup-lane exception, no `SEND_SMS` permission ever, and a Detekt rule enforcing zero-content telemetry. Those are written as hard overrides, not suggestions. None of that is derivable from the codebase, which is exactly the test for whether it belongs in the file. Directory layouts and dependency lists don't go in. Claude can read those. ## Rules that load only when they apply `rules/{web,android,ios,compute}.md` are platform packs. The Android pack has no business being in context when I'm working on a C++/CUDA repo. Claude Code supports this natively now through `paths` frontmatter in `.claude/rules/`: ```markdown --- paths: - "**/*.{kt,kts}" --- # Android rules ``` Rules with a `paths` field only enter context when Claude actually reads a matching file. Rules without one load unconditionally at the same priority as the project `CLAUDE.md`. User-level rules in `~/.claude/rules/` apply to every project and load before project rules. This is the single highest-leverage change available to anyone with a bloated config, and it's the thing I'd tell past-me to do first. ## Fifty agents, four stacks The roster is 50 subagents split across four sets: - `agents/` (15) — the default. A four-hat chain (architect → senior-swe → code-reviewer → qa), specialists (security, performance, db-migration, debugger, devops, docs, design), and a delivery layer (TPM, scrum-master). - `agents-android/` (7), `agents-ios/` (7), `agents-compute/` (21) — per-stack overrides. The override mechanism is the interesting part. A per-repo `.claude/agents/` file shadows a user-scope `~/.claude/agents/` file with the same canonical `name:` field. So dropping the Android set into a repo replaces the generic `code-reviewer` with a platform-brained one, silently, with no routing configuration anywhere. Same name, different body. The compute set is the largest because C++/CUDA/Python systems work has the most ways to go quietly wrong. It's also overkill if you only ship web apps. Strip it. Subagents each get their own context window, which is the real reason to use them. The architect doesn't need the QA agent's context and vice versa. ## Hooks are the only enforcement This is the section that earns the repo. The documentation is explicit that CLAUDE.md shapes behavior but is not a hard enforcement layer, and that if something must run at a specific point, it should be a hook. Hooks are shell commands at fixed lifecycle events. They run regardless of what the model decides. `hooks/guard-commit.sh` blocks four things: 1. Force-pushes to protected branches 2. Non-human committers 3. AI attribution lines in commit messages 4. Obviously-staged secrets Every one of those was previously a line in `CLAUDE.md` asking nicely. Asking nicely worked most of the time, which is worse than not working, because you stop checking. `hooks/format.sh` auto-formats edited files by extension across every stack. Design decision worth stealing: a missing formatter is a silent no-op, never an error. A hook that fails loudly on a machine that doesn't have `ktlint` installed is a hook people disable. The general principle, stated in Anthropic's own settings-versus-memory table: use settings and hooks for technical enforcement, use CLAUDE.md for behavioral guidance. Anything you would be upset about if it slipped through belongs in the first category. ## The two settings files `settings.json` covers web, Android, and iOS. `settings2.json` covers C++/CUDA/Python, with compute toolchain permissions and a subagent model pin. Claude Code reads only `settings.json`. `settings2.json` requires a manual swap or a merge. This is the sharpest edge in the whole repo and I'd rather say so than have someone discover it at an inconvenient moment. ## Three things that broke **The fork-bomb deny rule.** I had `Bash(:(){:|:&};:)` in the deny list. It caused repeated JSON parse errors. Removed permanently. The characters that make a fork bomb dangerous also make it terrible JSON. **Hook paths on Linux.** They must be literal. `$HOME` does not expand in that position and the hook silently fails to fire. If you're moving a config from macOS to Linux, this is the first thing to check. **Assuming an override took effect.** Run `/context` and read the list under **Memory files** to see what actually loaded. There is also an `InstructionsLoaded` hook that logs exactly which instruction files loaded, when, and why — useful for debugging path-scoped rules that appear to do nothing. ## Where it runs The Mac bundle and the Linux bundle share the universal spine. Nexus, my Threadripper workstation, got the compute-focused variant: 21 agents installed system-wide, `compute.md` rules, validated `settings.json`, live hooks. `claude doctor` clean, no errors. One outstanding issue: a `kotlin-lsp` plugin error on the Mac, pending a Command Line Tools update or disabling the plugin. ## Honest caveats This is opinionated and built for how my team works. The git rules assume PR-based flow with protected branches. The hooks assume formatters are installed, though they no-op cleanly if not. The compute stack is systems work and is dead weight for web-only shops. The point isn't to adopt my exact setup. It's to see a real working one and build yours. Most published configs are aspirational. This one runs every day at a 40-person studio. ## The longer version The chapter on Claude Code in **42: The AI Builder's Stack** goes deeper on the reasoning behind all of this — why the two-tier split, why hooks over instructions, what I got wrong on the way there. The book covers the rest of the stack too: terminal setup, database architecture, deployment pipelines, agentic workflows, voice AI, code review automation. 22 chapters. No sponsors, no affiliate links, nobody paid to be mentioned. - **The book:** [subethapress.com](https://subethapress.com/?ref=vorobyov.me) - **Buy it:** [Amazon](https://www.amazon.com/dp/B0H8WQZ7B8?ref=vorobyov.me) - **The config:** [github.com/roadhero/claude-code-setup](https://github.com/roadhero/claude-code-setup?ref=vorobyov.me) Free chapter excerpts are at [eltexsoft.com/course/](https://eltexsoft.com/course/?ref=vorobyov.me) if you want to read before you buy. ### Snapwire: Engineering Lead URL: https://vorobyov.me/snapwire-engineering-lead/ Last updated: 2026-05-28T05:45:36.000Z Snapwire was a marketplace connecting brands — including names like Dell and Starbucks — with on-demand photographers. For two and a half years I was the embedded engineering manager and technical product lead on it, working as part of the client's own team rather than a vendor at arm's length. That distinction matters, so let me be specific about what "embedded" meant here. ## Managing across two companies and three timezones The team I ran was split across organizations. I managed a group of about ten engineers on our side, based in Europe, alongside Snapwire's own engineers in California and Toronto. Two different companies, three timezones, one standup. I was in that standup every day, working directly with Snapwire's CTO and their scrum master — a fifteen-year ex-Apple engineer — as one integrated team. Not "here's the spec, ping us when it's broken." Daily ownership of the technical reality of the product. Day to day I monitored every issue, wrote the technical specifications, tightened QA and automated testing, and personally executed the hardest parts of the system. I vetted hires, took part in re-architecting the platform when the product pivoted, and helped set technical direction alongside the client's leadership. ## What I built **The V2 platform.** I rebuilt the product's user experience with our designer, and a big part of that job was unglamorous reconciliation: keeping the new designs aligned with the actual state of the API and the existing frontend, so what got designed was what could actually ship. A redesign that ignores the live system is just a mockup. I made sure V2 matched reality and improved on it. **Payments.** I implemented Stripe Connect — marketplace payments, with all the splitting, payout, and edge-case handling that two-sided money movement demands. **The admin backends.** The internal tooling Snapwire's team used to run the marketplace. **A real performance win on the frontend.** The React bundle had grown to tens of megabytes. Working with the engineering team, we cut it to around three — roughly a ninety percent reduction — which, combined with the V2 redesign, transformed load time and the overall feel of the product. ## The image pipeline, and the part I'm proudest of Snapwire moved millions of images and videos, so I researched and implemented the CDN pipeline on AWS to serve them at scale. Tagging and quality scoring ran through AWS Rekognition. Here's the honest version: Rekognition out of the box landed around 60–70% accuracy on our content. Good enough for a demo, not good enough for a product brands were paying for. So we didn't stop there. We wrote custom AI/ML code to post-filter Rekognition's output, and that took tagging accuracy past 95%. The off-the-shelf service was the starting point, not the answer — the accuracy that actually mattered came from the layer we built on top of it. ## The handoff Snapwire was acquired by StudioNow. We supported the technical due diligence, helped transition the platform, and — the part I care about — kept shipping features and polish through and after the acquisition, staying with the product until we genuinely weren't needed anymore. A smooth handoff isn't an accident. It's what happens when the people who built the system are still around, the code is understood, and nobody's scrambling to reverse-engineer their own product during diligence. ## What this kind of work is Snapwire is the clearest example of a mode I work in often: not a vendor you brief and wait on, but an engineering manager and technical product lead who embeds in your team, owns the hardest parts of the build, and runs people across companies and timezones as if it were one org. The output isn't a deliverable handed over a wall. It's a product I helped run, day by day, until it had a good home. ### Building HeyTutor: Seed to Series URL: https://vorobyov.me/building-heytutor/ Last updated: 2026-07-28T05:04:19.000Z In 2016, [two founders](https://www.forbes.com/profile/heytutor/?ref=vorobyov.me) showed up with a product spec written on a single sheet of A4\. They were non-technical, sharp, and they wanted to build a two-sided tutoring marketplace across the United States. They didn't have a CTO. For the next nine years, I was it — without the title. That's the role I want to talk about, because it's the one most companies don't know how to ask for. ## The CTO nobody hired HeyTutor never had a CTO on the org chart until 2023\. They didn't need one, because they had me. I owned the architecture, the infrastructure, the CI/CD, the database, the server management, the API selection, the hiring, and the technical roadmap. Operational ownership, not advice from the sidelines. People hear "fractional CTO" or "technical advisor" and picture someone who drops in for a strategy call and disappears. That's not what this was. I was scaling load balancers late at night when traffic spiked. I was in the schema design, the query optimization, the migration planning. The database decisions I made in year one were still holding up the platform in year nine — which is the only real test of whether you got them right. ## What we actually built We turned that one-page spec into forty pages, brought in a senior designer for a full InVision prototype, and built V1 from zero. The stack was Vue on the front end — an early bet in 2016, when Vue was still proving itself — and Laravel with PostgreSQL behind a RESTful API. Stripe for payments. Real-time messaging. CI/CD with proper QA and staging environments from day one. Marketplaces are one of the hardest things to build well, because every page has to serve two sides at once. The piece I'm proudest of is the matching engine. Over a hundred signals fed into pairing a student with a tutor: subject and grade level, teaching style, availability windows, timezone compatibility, location proximity, price range, ratings, background-check status, response-time patterns. Get the weighting wrong and the marketplace feels broken even when every individual feature works. Timezone-aware scheduling was its own quiet monster. A student in New York and a tutor in Los Angeles need a slot that's correct in both their calendars, with recurring sessions, cancellations, and two people occasionally trying to book the same slot at the same instant. None of that is glamorous. All of it has to be exactly right. We also generated thousands of local SEO landing pages — "math tutors in Los Angeles," "SAT prep near me" — programmatically, each with location-specific content. They became one of the platform's primary organic acquisition channels. ## The pivot that changed the company HeyTutor started B2C. Then school districts came calling, and B2B turned out to be a different animal entirely. Districts don't browse a marketplace and pick tutors one by one — they bulk-hire and assign through a CRM-driven workflow, and they need to plug into the e-learning infrastructure they already run. So we built a separate B2B layer on top of the existing platform: Google Classroom integration, district-level dashboards, bulk assignment workflows, and the compliance and reporting that government contracts demand. That layer is what let HeyTutor win LAUSD — the largest school district in the United States. Contracts at that scale don't tolerate a platform that's flaky or undocumented. The engineering has to be right, and it has to be provably right. ## Onsite, because some things don't happen over Slack Roadmaps that actually stick get built in a room. I flew our technical lead and PMs out to California to sit with the HeyTutor team in person, set the direction, and decide together what to build, what to defer, and what to kill. You can run a nine-year engagement remotely, but the inflection points — the moments where the product's direction changes — are worth being in the same room for. ## How I measure a good engagement: the handoff In 2023, HeyTutor hired their first in-house CTO, and I handed the technical leadership over to their internal team. That's the outcome a fractional engagement should produce. The company grows until it needs and can afford a full-time technical leader, and the handoff is clean because the codebase, the infrastructure, and the processes are all documented and well-maintained. A messy handoff means you built a dependency, not a system. A clean one means you did the job. By the end: 10,000+ tutors, 10,000+ students, tens of thousands of sessions a month, continental US coverage, multiple funding rounds, and both founders on the Forbes 30 Under 30 list. Nine years — my longest engagement, and the one I'd take again without hesitating. ## What this kind of work is If you're a non-technical founder with a hard product and no senior engineering leadership, this is the gap I fill: someone who owns the technical reality of the company end to end, builds the team, makes the architecture decisions you'll be living with for years, and hands it off clean when you've outgrown the arrangement. Not a deck. The actual thing. ### Sanity CMS in 2026: The Headless CMS That Actually Respects Your Time URL: https://vorobyov.me/sanity-cms-in-2026-the-headless-cms-that-actually-respects-your-time/ Last updated: 2026-07-28T05:23:25.000Z I used Contentful for years. It was fine. The content model was decent, the API was fast, the editorial experience was good enough. Then on April 30, 2025, Contentful decided that "free" meant 25 content types, 100K API calls, and a $300/month cliff if you go over. We had 30+ content types. The math was simple. We left. ## Why Sanity I evaluated Strapi, Payload, Storyblok, and Sanity. Here's the honest version of that evaluation: **Strapi** is open source and self-hosted. Great for data sovereignty. Terrible if you don't want to maintain a Node.js server, a Postgres database, and all the DevOps that comes with it. I run a software studio. I have engineers. I still didn't want to babysit a CMS server. **Payload** is fascinating — a CMS that lives inside your Next.js app. One deployment unit. TypeScript-native. 42K GitHub stars. If we were on Next.js, I'd seriously consider it. We're on Astro. Moving on. **Storyblok** has the best visual editing for marketers. Drag-and-drop, WYSIWYG, the whole thing. But our marketers are me, and I type Markdown in a text editor. Visual page builders solve a problem I don't have. **Sanity** gave me 20 seats, 10,000 documents, 1 million CDN requests, and 100GB bandwidth. Free. The Studio is React-based — same stack our team uses for client work. The content model is defined in code, not clicked together in a UI. And the query language (GROQ) is genuinely better than GraphQL for content. That's it. That's the evaluation. Sometimes the right answer is the one that costs nothing and works immediately. ## What Sanity actually is Sanity is a headless CMS with a twist: the editing interface (Sanity Studio) is a React app that you own, customize, and deploy. The content lives in Sanity's cloud (the "Content Lake"). You query it with GROQ or GraphQL. You render it with whatever frontend you want. The Studio runs at `your-project.sanity.studio` or on your own domain. You define schemas in TypeScript. You build custom input components in React. You can replace literally any part of the editing interface with your own code. Most CMSes give you a settings panel. Sanity gives you a React app. This means the setup cost is higher than WordPress. You'll spend developer time configuring the Studio. But the ceiling is also higher — dramatically higher. Our Studio has custom preview components for blog posts, structured case study schemas with stack arrays and metric objects, and an author reference system. Took a few hours to set up. Works exactly how we need it to. ## GROQ is weird. GROQ is also better. GROQ (Graph-Relational Object Queries) is Sanity's query language. It looks like nothing you've seen before. The first time you see `*[_type == "post"]{title, "author": author->name}` you think "what is this syntax." The second time you think "wait, that just resolved a reference in one character." The `->` operator dereferences a reference. In GraphQL, you'd need a resolver, a type definition, and a separate query. In GROQ, you add two characters. Where GROQ wins over GraphQL: server-side projections (you shape the exact JSON your component needs in the query), no deployment step (schema changes are queryable instantly), real-time subscriptions work natively, and reference following is trivial. Where GraphQL still wins: schema introspection, cross-source federation, and the fact that every developer on earth already knows it. GROQ has a learning curve. Not a steep one — maybe two hours to be productive, a week to be fluent — but it's one more thing. Sanity supports both. If your team has strong GraphQL opinions, use GraphQL. You'll give up real-time subscriptions and mutations (Sanity's GraphQL layer is read-only), but you'll get the tooling you already know. ## Portable Text: why it matters more than you think Most CMSes store rich text as HTML. Sanity stores it as a JSON block array called Portable Text. "Who cares?" You will, the first time you need to render the same content in a React web app, a React Native mobile app, an email template, and an LLM context window. HTML is designed for browsers. Portable Text is designed for anywhere. Every rendering layer has a Portable Text renderer: React, Vue, Svelte, native iOS, native Android. You write custom serializers for your own block types. An inline "callout" block in the editor becomes a styled callout in the web app, a plain text block in the email, and structured metadata for the AI agent. Same source content, four different outputs, zero HTML parsing. The trade-off: pasting from Google Docs into the editor doesn't always do what you expect. Block-level formatting survives. Inline formatting sometimes doesn't. Your editors need 15 minutes of training. After that, it's fine. A bigger trade-off we learned the hard way: pasting rich text from chat interfaces (like Claude's web UI) can silently rewrite relative URLs to absolute ones with the wrong domain. We discovered 146 links pointing to `claude.ai` instead of `eltexsoft.com` because Sanity Studio's rich text editor resolved `/blog/...` against the page URL. We wrote a script to fix them. It took an hour. But it shouldn't have happened, and it wouldn't have if we'd pasted Markdown instead. ## The MCP thing is actually a big deal Sanity has a Model Context Protocol server. MCP is the protocol that lets AI agents (Claude, Cursor, VS Code agents, etc.) interact with external tools. Sanity's MCP server lets an AI agent query your content, create documents, patch fields, manage releases, and run semantic search — all through natural language. I use this daily. "Query all posts where the slug contains 'outsourcing'." "Patch the seoTitle on document ID 93299147 to this value." "Publish these 13 documents." Claude does it. I verify. Done. We created blog posts, patched SEO fields across 31 pages, fixed 146 broken links, and published 36 articles through a combination of MCP operations and Studio editing. The MCP connection turns content management from a point-and-click operation into a programmable one. No other headless CMS has anything close to this in May 2026\. Contentful doesn't. Strapi doesn't. Payload doesn't. Sanity's MCP server is the single biggest competitive advantage in the headless CMS market right now, and almost nobody is talking about it because "CMS has an AI integration" sounds like marketing fluff. It's not fluff. It's the difference between manually editing 31 meta descriptions and telling Claude to do it in one prompt. ## Real-time collaboration Multiple editors on the same document. Google-Docs-style cursors. Keystroke-level presence. Conflict-free because the Content Lake is patch-based — every change is a discrete operation, not a full document overwrite. Most headless CMSes have "collaboration" that means "optimistic locking" — if two people edit the same document, one of them loses. Sanity's collaboration means two people type in the same field at the same time and both changes survive. Contentful would need a near-total architectural rewrite to match this. I'm the only editor on our site, so I don't use this feature much. But for agencies managing content for clients, or companies with editorial teams, this is the difference between "don't touch that document, I'm editing it" and "go ahead, we're both editing it." ## The money **Sanity Free:** 20 seats, 10,000 documents, 1M CDN requests, 250K API requests, 100GB bandwidth, 100GB assets. Real-time collaboration, Media Library, GROQ + GraphQL. **Sanity Growth:** $15/seat/month (viewers free). 25,000 documents, 5 roles, AI Assist, Comments, Tasks, Scheduled Drafts, Content Releases. **Contentful Free (post-April 2025):** 25 content types, 100K API calls, 50GB bandwidth. No grandfathering. Exceeding limits can result in suspension. **Contentful Paid:** Somewhere between $300 and $850/month depending on which source you trust (PricingSaaS, Vendr, and CostBench all report different numbers). Get a sales quote. Don't trust the internet, including me. For a studio our size, Sanity's free tier covers everything. 36 blog posts, 13 case studies, 10 industry pages, 9 tech pages, 22 course chapters, authors, metadata — all well under 10,000 documents. We'll hit Growth when we need Content Releases or Tasks. That's $15/month for one editor seat. Not $300. ## Where Sanity falls apart **The Studio needs configuration.** A fresh install is functional but bare. You'll spend developer days making it feel good for editors. Schema definitions, custom previews, desk structure, validation rules — it's all code you write. This is simultaneously Sanity's greatest strength and its highest barrier to entry. **Document limits are real.** 10K on Free, 25K on Growth. Reference-heavy schemas inflate document counts because references create separate documents. If you have 5,000 blog posts with 3 author references each, those references count. The Increased Quota add-on is $299/month. That's a cliff. **API vs CDN billing will bite you.** Direct API requests to `api.sanity.io` cost 10x more per unit than CDN requests to `apicdn.sanity.io`. A misconfigured `@sanity/client` with `useCdn: false` will inflate your bill for no reason. Always set `useCdn: true` for read operations. **No self-hosting.** The Content Lake is Sanity's cloud. Period. You can export everything (GROQ + export CLI), so migration is feasible, but you're a SaaS customer. If "I own the server" is a hard requirement, use Strapi or Payload. **No native visual page builder.** If your primary editor wants to drag-and-drop sections like Squarespace, Sanity is the wrong tool. It has Visual Editing (click-to-edit overlays on your frontend) and the Presentation tool, but it's not a page builder. It's a structured content editor. Different animal. **GROQ is one more thing.** Your team learns React. TypeScript. Your framework. Your deployment platform. GROQ. That's a real cost. It pays off, but don't pretend there's no ramp. ## The bottom line Sanity is a CMS for people who think about content as structured data, not as pages. If that sentence resonates with you, Sanity is probably the right choice. If that sentence sounds like unnecessary complexity, Storyblok or WordPress might serve you better. No judgment. We migrated from Contentful using the official `contentful-to-sanity` CLI. One command. The content came over clean. The Studio took a day to configure. The MCP integration took 10 minutes. The blog has been running for two weeks with zero issues. 30,000+ organizations use Sanity. Nike, Sonos, Figma, Linear, National Geographic, Shopify, Spotify. $85M Series C. $40M+ ARR. This isn't a bet on an underdog. This is a bet on the CMS that quietly became the default while everyone was arguing about WordPress vs Contentful. The free tier is generous. The content model is flexible. The AI integration is years ahead. And the query language is weird in the way that good tools are always weird at first — it feels wrong until you realize it's right, and then you can't go back. --- *We rebuilt* [*eltexsoft.com*](https://eltexsoft.com/?ref=vorobyov.me) *on Astro + Sanity. The full teardown:* [*Rebuilding Our Agency Website in 2026*](https://vorobyov.me/rebuilding-agency-website-in-2026-lessons-learned/)*. The Astro half of the story:* [*Astro in 2026*](https://vorobyov.me/astro-in-2026-the-framework-that-wins-by-doing-less/)*.* ### Astro in 2026: The Framework That Wins by Doing Less URL: https://vorobyov.me/astro-in-2026-the-framework-that-wins-by-doing-less/ Last updated: 2026-07-28T05:23:39.000Z I spent 10 years building websites with frameworks that ship 200KB of JavaScript to render a paragraph of text. Vue, Nuxt, React, Next — all brilliant tools for building apps. All absurdly overpowered for a marketing site. Then I found Astro. And I felt like an idiot for not finding it sooner. ## What Astro actually is Astro is a web framework that ships zero JavaScript by default. You write components — in Astro's own syntax, or in React, Vue, Svelte, literally whatever you want — and the build step renders them to plain HTML. The JavaScript disappears. What lands in the browser is HTML and CSS. That's it. A typical page on our site is 5-15KB. Not 5-15KB gzipped. 5-15KB total. The Lighthouse score is 95+ on mobile without trying. Not because we optimized aggressively — because there's nothing to optimize. You can't have a slow JavaScript bundle if there is no JavaScript bundle. "But what about interactivity?" Fine. You mark a component with `client:load` or `client:visible` and it hydrates independently. One interactive widget on a page of 20 components means one component ships JS. The other 19 are static HTML. Astro calls this "Islands Architecture." I call it "not shipping code nobody asked for." ## The numbers that made me switch State of JavaScript 2025 surveyed thousands of developers. Astro hit 94% positive sentiment. Number one. SvelteKit came in at 88%. Nuxt at 80%. Next.js? 55%. Down from 68% the year before. The survey editors didn't sugarcoat it: Next.js keeps gaining market share while losing satisfaction at the same time, and now has a 39-point gap with Astro. That's a framework running on inertia, not love. I don't make technology decisions based on popularity contests. But when 94% of people who use a tool like it, and 45% of people who use the alternative don't, the signal is hard to ignore. ## Cloudflare bought them January 16, 2026\. Cloudflare acquired The Astro Technology Company. All Astro employees became Cloudflare employees. Framework stays MIT-licensed. The strategic logic is obvious. Vercel has Next.js. Cloudflare needed a framework story. Astro's static-first, edge-friendly architecture is a natural fit for Workers, R2, and D1\. Expect Cloudflare-specific features to ship faster than competing platform adapters. Should you worry? A little. "Stays open source" is a stated commitment, not a contractual guarantee. Governance changes typically lag acquisitions by 12-24 months. But Astro deploys anywhere — Vercel, Netlify, Node, Deno, Hetzner with nginx. We deploy on Hetzner. Cloudflare has zero leverage over our hosting choice. That's the beauty of static HTML. ## Astro 6.0 Shipped March 10, 2026\. The highlights that actually matter: **Rebuilt dev server on Vite's Environment API.** Faster HMR. You won't notice unless your project is large, then you'll notice a lot. **First-class Cloudflare Workers runtime.** Unsurprising given the acquisition. If you deploy to Workers, this removes a layer of adapter friction. **Stable Content Security Policy API.** Astro is the first JavaScript meta-framework with built-in CSP for both static and dynamic pages. If you've ever hand-rolled CSP headers in nginx and wanted to cry, this is for you. (I have. I did.) **Experimental Rust compiler.** Not default yet. Will be. The team is rewriting the compiler in Rust for speed. Early benchmarks show 2x faster rendering. Treat that number as directional — vendor benchmarks always are. **Built-in Fonts API.** Sounds minor. Isn't. Google Fonts without the privacy headache and layout shift. Self-hosted, optimized, one config line. If you're not ready for v6, run 5.17.x. It's stable, battle-tested, and what we're on right now. Upgrade after 6.1 or 6.2 when the integration ecosystem catches up. ## Content Collections are the killer feature nobody talks about This is the feature that sold me. Not the performance. Not the zero JS. The content model. You define a schema in Zod. You put Markdown files in a folder. Astro validates every file against the schema at build time. Type errors, missing fields, wrong formats — all caught before deploy, not in production at 2 AM. We have 116 pages: services, industries, tech stacks, case studies, course chapters. Every content type has a Zod schema. Every markdown file is validated. If an engineer adds a case study with a missing `stack` field, the build fails. Not "renders wrong." Fails. Loudly. The Content Layer API (stable in 5.0) generalized this further. You can write loaders that pull from anywhere — local files, APIs, headless CMS, databases — into the same type-safe pipeline. Our blog posts live in Sanity. Our marketing pages live in Markdown. Same build, same types, same validation. Both sources. ## The AI crawlability angle nobody's writing about Here's the thing that matters in 2026 and almost nobody is talking about. GPTBot, ClaudeBot, PerplexityBot, Google's AI Overviews — they fetch your HTML. They don't execute your JavaScript. If your content lives inside a React component that renders client-side, it doesn't exist to them. Our old Nuxt site was invisible. Zero organic traffic for years. Not because the content was bad. Because the content wasn't in the HTML. Astro's output is pure HTML. Every word on every page is in the source. AI bots read it. Search engines index it. LLMs can summarize it. We went from zero to 116 fully indexable pages in two weeks. The SEO value of "your content is actually in the HTML" is impossible to overstate in a world where half the web hides behind client-side rendering. ## Who else is using it IKEA, Unilever, Visa, NBC News, OpenAI, Cloudflare's own developer docs, Microsoft, Adobe, Porsche, The Guardian, Bloomberg, Deloitte, LEGO. TechnologyChecker tracks 49,000+ active domains on Astro. Migration data shows a 12:1 ratio of companies moving from Gatsby to Astro versus the other direction. This isn't an indie framework anymore. ## Where Astro falls apart I'll be honest because nobody else is. **Complex stateful apps.** If you're building a dashboard, a Figma clone, a real-time multiplayer game — Astro is the wrong tool. It's content-first. Everything else is a workaround. Use Next.js, SvelteKit, or whatever your team knows. **The ecosystem is younger.** Auth, advanced i18n, complex form state — you'll write more glue code than in Next.js. The integrations exist but they're not as mature. **Astro DB was a miss.** The hosted database service (Astro Studio) shut down in early 2025\. Fred Schott was unusually candid about it: they never found product-market fit. Astro DB still works as a wrapper around libSQL/Turso, but don't pick Astro for the database story. Pick it for the rendering story. **Cloudflare alignment.** The framework deploys everywhere today. But Cloudflare-specific features will get priority. If that bothers you philosophically, factor it in. ## The bottom line I run a software engineering studio. We build apps for Fortune 500s. Our own website runs on Astro on an $8.60/month Hetzner VPS with nginx. It builds 116 pages in 2.66 seconds. Lighthouse scores are 95+. Ahrefs health score is 99/100\. Every AI bot on the internet can read every word. The framework won by doing less. Less JavaScript, less complexity, less cleverness. More HTML. More speed. More content that actually shows up when someone (or something) looks for it. 94% satisfaction isn't a fluke. It's what happens when a tool does exactly what it promises and nothing else. --- *We rebuilt* [*eltexsoft.com*](https://eltexsoft.com/?ref=vorobyov.me) *on Astro + Sanity. The full teardown:* [*Rebuilding Our Agency Website in 2026*](https://vorobyov.me/rebuilding-agency-website-in-2026-lessons-learned/)*.* ### Rebuilding agency website in 2026 – Lessons learned URL: https://vorobyov.me/rebuilding-agency-website-in-2026-lessons-learned/ Last updated: 2026-07-28T05:24:00.000Z I run a software engineering studio. We've built apps for Fortune 500s, scaled marketplaces, shipped iOS apps that Apple featured at WWDC. Our own website was running on Nuxt + Contentful and had zero organic traffic. Zero. For years. Not because the site was bad. The design was fine. The content was fine. The problem was that Nuxt ships JavaScript, and AI search bots don't execute JavaScript. GPTBot, ClaudeBot, PerplexityBot — they fetch your HTML and move on. If your content lives inside a Vue component that renders client-side, it doesn't exist to them. So we rebuilt the whole thing. Here's what happened. ## Why Astro I looked at Next.js first. Everyone does. But for a marketing site — services, case studies, blog — Next.js is overkill. You're shipping 85-180 KB of React runtime to render what is fundamentally static text and images. The App Router adds complexity we'd never use. And the CVE list in 2025 was not confidence-inspiring. Astro ships zero JavaScript by default. A typical page is 5-15 KB of HTML and CSS. That's it. The framework is purpose-built for content-heavy sites: static site generation, Content Collections with Zod validation, built-in sitemap and RSS. State of JS 2025 ranked it #1 by developer satisfaction, 39 points ahead of Next.js. Cloudflare acquired the Astro Technology Company in January 2026\. Framework stays MIT-licensed. If anything, the backing makes the bet safer. The deciding factor was simpler than all of that: AI bots can read our content now. Every word on every page is in the HTML. No hydration, no client-side rendering, no hoping that Googlebot's renderer feels like executing your JavaScript today. ## Why Sanity (and why we left Contentful) Contentful's April 2025 free-plan crackdown sealed it. 25 content type cap, 100K API calls. Their Premium tier starts at $300/month. We're a 35-50 person studio, not a media company. That math doesn't work. Sanity's free tier gives us 20 seats, 2 datasets, 10K documents, 1M CDN requests. The Studio is React-based — same skills our team uses for client work. The official `contentful-to-sanity` CLI migrated our content in one command. And Sanity's structured content model is genuinely better for a site with services, case studies, industries, staffing models, and tech skill pages that all cross-reference each other. We kept marketing pages (services, industries, tech stacks) as Markdown in the repo using Astro Content Collections. Blog posts live in Sanity where I can publish without touching code. Best of both worlds: engineers own the structured pages, I own the editorial. ## The SEO play that actually worked We started at Domain Rating 21 with zero organic traffic. Zero indexed keywords. The Nuxt site had been invisible to search for years. The strategy came from running 75+ keywords through Ahrefs Keywords Explorer. What we found was extraordinary: 41 keywords with KD 0-3 representing 25,000+ monthly searches. Several high-CPC keywords ($15-30) had literally zero competition. "Generative AI development services" — 3,000 monthly searches, KD 1, $20 CPC. Nobody had a dedicated page for it. We built 60 pages targeting those keywords. Services, industries, tech stacks, staffing models, blog posts — each one targeting a specific keyword cluster with FAQ sections and JSON-LD schema. The content architecture follows what we learned from studying competitors. Vention (DR 71, 17.5K traffic) wins by having a dedicated page for every keyword variant. Their MVP page alone captures 5 keyword variants totaling 1,420 monthly traffic from one URL. That's the model. One page per intent, not five intents crammed onto one page. ## Ahrefs MCP changed how I work This is the part that surprised me most. Ahrefs has an MCP (Model Context Protocol) integration that connects directly to Claude. I can ask Claude to check our site audit, pull keyword data, analyze competitor backlinks — and it queries Ahrefs in real time without me opening the dashboard. I'd run a site audit, Claude would parse the results, identify the issues, and generate the exact code fixes — meta descriptions that were too long, title tags over 65 characters, broken image references, redirect chains. We went from a health score of 73 to 99 in three sessions. The meta description audit alone covered 31 pages that needed trimming under 155 characters. The workflow is: Ahrefs finds the problem through MCP → Claude generates the fix → Claude Code applies it to the codebase → push to GitHub → Coolify auto-deploys. The entire cycle from "Ahrefs flagged this" to "fix is live in production" takes minutes, not hours. ## Sanity MCP for content operations Same pattern with Sanity's MCP integration. When we discovered that pasting blog content from Claude's chat interface into Sanity Studio was silently converting relative links (`/blog/...`) to `https://claude.ai/blog/...` — 146 links across 13 posts — we wrote a Node script that patched all of them through Sanity's API in one run. But the MCP connection meant I could verify the fix instantly. "Query all posts for any href containing claude.ai." Zero results. Done. No need to open the Studio, no manual spot-checking. We also use Sanity MCP to create blog posts, patch SEO fields, publish batches of documents, and query content for audits. I published 36 blog posts through a combination of MCP creation and Studio editing. All with full body content, proper author references, FAQ sections, and internal cross-links. ## The legal pages nobody wants to write Every website needs Terms of Service, Privacy Policy, and Cookie Policy. Every founder hates writing them. Most people use a generator that produces generic boilerplate or pay a lawyer $3-5K. I found Raj Jha's [mill-deterrent-pack](https://github.com/mindheadllc/mill-deterrent-pack?ref=vorobyov.me) — an open-source legal template specifically designed to deter mass-arbitration mills and serial litigants. Three tiers of protection: - **Tier 1:** Pre-dispute notice gate with a 12-item disclosure requirement, class-action waiver, AAA arbitration, specific governing law and venue. - **Tier 2:** 60-day informal resolution period, two mandatory principal-level meetings, fee-arrangement disclosure, 24-month prior-claims history. - **Tier 3:** Pre-merits good-faith review by the arbitrator, claimant-pays cost allocation, bad-faith dismissal with fee-shifting. It's serious legal engineering. The Terms include an ML training prohibition, a $100 liability cap, and indemnification. The Privacy Policy covers GDPR (with CNPD as lead authority since we're HQ'd in Lisbon), CCPA/CPRA, explicit tracking disclosures, and a "what we don't use" section. The Cookie Policy documents every cookie by name, purpose, and duration. We adapted it for our entity (Icemint LLC d/b/a EltexSoft, Wyoming LLC) and our specific stack (GA4, Cloudflare, Google Fonts). Took an afternoon. The result is more thorough than what most $5K legal reviews produce, and it's open source. ## Ditching GA4 for Cloudflare Web Analytics While we were at it, we set up Cloudflare Web Analytics alongside GA4\. Server-side, no client JavaScript, no cookies. GDPR-friendly by design — there's nothing to consent to because there's no tracking pixel in the browser. GA4 is still running for now because Search Console integration requires it. But for actual traffic understanding — which pages get visited, where people come from, what devices they use — Cloudflare's server-side analytics gives cleaner data without the privacy overhead. The Cookie Policy got simpler too. Two Cloudflare operational cookies (`__cf_bm` for bot management, `_cfuvid` for rate limiting) plus GA4's `_ga` cookies. That's the complete list. No pixels. No session replay. No heat maps. The privacy policy says what we don't use, and the list of things we don't use is longer than the list of things we do. ## Infrastructure: $8.60/month for everything The whole stack runs on a Hetzner Cloud VPS (CPX21, $8.60/month). Coolify manages the deployment — Nixpacks builds the Astro site, outputs static HTML to an nginx container. Cloudflare sits in front for CDN, WAF, and DDoS protection on the free tier. Sanity's free tier handles the CMS. GitHub Actions is free. Domain was already paid for. Total marginal cost: $8.60/month. Down from whatever Contentful was going to start charging us. SSL is Cloudflare Flexible — they terminate TLS at the edge and send HTTP to the origin. One less thing to manage on the VPS. ## What I'd do differently **Start with the SEO strategy, not the design.** We built pages and then optimized them for keywords. It should be the other way around. The keyword research should dictate the page architecture. Which pages to build, what to call them, how to structure the URLs — all of that should come from the data. **Verify your content proofread by LLMs before it reaches the CMS.** The relative-to-absolute URL resolution bug cost us a full debugging session and a script to fix 146 links. Write content in Markdown, paste Markdown. **Set up Ahrefs site audit on day one.** We caught 57 errors on the first crawl that were trivially fixable but had been silently hurting us. Broken favicon references, missing meta descriptions, redirect chains — all invisible unless you crawl. **Nginx redirects need absolute HTTPS URLs when Cloudflare Flexible SSL is in front.** Relative redirects resolve to `http://` at the origin because Cloudflare connects to your server over HTTP. Every `rewrite ... permanent` needs the full `https://yourdomain.com/path/` target. We had 25+ redirects silently double-hopping (301 → http → https) before catching this. **Disk space on small VPS instances fills up fast with Docker.** Coolify's Nixpacks builds accumulate overlay layers. We hit 100% disk on a 40GB instance after 16 deployments. Weekly `docker system prune -a --volumes -f` via cron is mandatory. ## The numbers so far - 116 static HTML pages - 36 blog posts with full body content - 13 case studies - 10 industry pages - 9 tech stack pages - 22 course chapters (web edition of my book, "42: The AI Builder's Stack") - Lighthouse mobile: 95+ - Ahrefs health score: 99/100 - Page weight: 5-15 KB HTML + CSS per page - Build time: 2.66 seconds for 116 pages - Deploy: push to GitHub → Coolify auto-build → live in under 3 minutes We're still at DR 21\. The content has been live for less than two weeks. Position data starts appearing at 60-90 days. But the foundation is there: 41 keywords with KD 0-3 targeting 25,000+ monthly searches, pure HTML that every bot on the internet can read, and a publishing workflow where I write in Sanity and it's live in minutes. Ask me again in 90 days. --- *The stack:* [*Astro*](https://astro.build/?ref=vorobyov.me) *\+* [*Sanity*](https://sanity.io/?ref=vorobyov.me) *\+* [*Hetzner*](https://hetzner.com/?ref=vorobyov.me) *\+* [*Coolify*](https://coolify.io/?ref=vorobyov.me) *\+* [*Cloudflare*](https://cloudflare.com/?ref=vorobyov.me)*. Legal templates:* [*mill-deterrent-pack*](https://github.com/mindheadllc/mill-deterrent-pack?ref=vorobyov.me) *by Raj Jha. SEO:* [*Ahrefs*](https://ahrefs.com/?ref=vorobyov.me)*. Code:* [*Claude Code*](https://docs.anthropic.com/en/docs/claude-code?ref=vorobyov.me)*. The site:* [*eltexsoft.com*](https://eltexsoft.com/?ref=vorobyov.me)