Planet Cloudflare

Aggregated posts from Cloudflare employees and community

April 14, 2026

OpenCode School

Zeke Sikelianos ·

A free, self-paced course for learning OpenCode, the open-source AI coding agent.

Portugal

Zeke Sikelianos ·

Trip planning map for Lisbon, May 2026.

April 06, 2026

April 03, 2026

Winning at Solitaire

Zeke Sikelianos ·

Reliving moments of childhood boredom with the help of OpenCode

April 02, 2026

March 31, 2026

March 29, 2026

March 24, 2026

This is Planet Cloudflare

Ade Oshineye ·

Planet Cloudflare is an old-fashioned RSS/Atom aggregator for blogs from Cloudflare employees and the wider Cloudflare community. It's motivated both by my love of Python and my nostalgia for the good old days of blogging.

But more importantly it's a long term demo of Python Workers. They are (as of March 2026) still in beta but this demonstrates their ability to integrate with the rest of the Cloudflare platform in a production environment. I'm also hoping this exemplifies how Python Workers can bring the advantages of the Cloudflare platform (low cost, high scalability and a wide variety of interesting services) to Python developers.

The implementation itself is relatively simple. It's an instance of Planet CF which is an open source RSS/Atom aggregator that supports all the usual features (responsible web fetching, liberal feedparsing via Feedparser and themes) people expect from Planet-style aggregators. It uses Cloudflare's Cron Trigger service to trigger the fetching of all the feeds once an hour. Since the number of feeds is potentially very large I enqueue each feed separately so that I have a separate worker to fetch each one. This has the benefit that I never have to worry about how many feeds are in the system.

The architecture is relatively simple. This simplicity supports a departure from the usual architecture of Planet-style aggregators. I store every blog post in D1 then provide both full-text search (using SQLite's BM25 algorithm) and vector search (based on Cloudflare's Vectorize). I then combine and re-rank the results so that you get a search engine that provides the results you expect when you type literal strings as well as handling synonyms. I'll be writing more in the future about how to combine indices to cheaply provide sophisticated search features.

Over time I expect to see this instance grow as I add more bloggers. Ideally this might even encourage more people to come back to blogging.

Driving Chrome with an Agent

Zeke Sikelianos ·

Your real, logged-in browser can now be natively driven by coding agents like OpenCode.

March 20, 2026

March 14, 2026

Slop Creep: The Great Enshittification of Software

Boris Tane ·

Slop creep is the slow, invisible enshittification of a codebase through an accumulation of individually reasonable but collectively destructive decisions, each one too small to flag, too numerous to track, and too deeply buried to unwind by the time you notice.

Picture a codebase six months from now. Every feature shipped on time, every PR passed review. But the team is slower than ever, and constantly firefighting.

Every new feature touches 10s of files. There are six different ways to do the same thing. The data models have fields that exist purely to work around a limitation that was introduced earlier and never revisited. The on-call rotation is a nightmare because nobody can trace the flow of a request through the system anymore.

I've done this to myself. When building Baselime, I made every mistake in the books: premature microservices, poor and impossible to extend schemas, leaky abstraction, etc. I shipped fast, made "pragmatic" architectural calls, and watched the codebase calcify around them. That used to take months of compounding mistakes to reach crisis point.

Last year I got a coding agent, and my side project reached crisis point in days.

Every change is fine, but the codebase is not

This is the insidious thing about slop creep. No single commit is the problem. The agent didn't introduce a bug, it introduced a slightly wrong abstraction, then built on top of it, then built on top of that.

graph TD
    A[Feature Request] --> B[Agent writes solution]
    B --> C[Looks fine in isolation]
    C --> D[Ships]
    D --> E[Next feature request]
    E --> B
    D --> F[💀 Codebase decays]
    style F fill:#fee2e2,stroke:#fca5a5,color:#991b1b

Two weeks later, unwinding it means fiddling with databases in production and rewriting three services. That's slop creep: death by a thousand reasonable decisions.

The old world had a circuit breaker

Bad architectural decisions used to come one at the time. A junior developer with bad instincts had a natural speed limit: they couldn't build fast enough to bury the codebase before someone noticed. The pain was loud and unavoidable, so you fixed it or you died.

graph TD
    A[Bad abstraction] --> B[Slows everything down]
    B --> C[Team feels the pain]
    C --> D[Forced to fix it]
    style C fill:#fee2e2,stroke:#fca5a5,color:#991b1b
    style D fill:#d1fae5,stroke:#6ee7b7,color:#065f46

Coding agents removed the circuit breaker.

The agent can keep piling crap on top of more crap, indefinitely, and stay productive the entire time. You don't slow down. You build a bigger pile. The reckoning is deferred, compounded, and much more painful when it finally arrives.

Coding agents can't think about systems holistically

The core problem is simple: coding agents are not able to find the right level of abstraction when building software. They don't see the system, they see the prompt.

Ask an agent to add an endpoint and it adds an endpoint. It doesn't know there are four other endpoints that should have been abstracted into a shared handler, or that the data model it's extending was already a mistake. It doesn't know any of that because no one told it.

graph TD
    A[Agent sees: the prompt] --> B[Agent builds: the solution]
    B --> C[Correct in isolation]
    B --> D[Wrong for the system]
    style C fill:#d1fae5,stroke:#6ee7b7,color:#065f46
    style D fill:#fee2e2,stroke:#fca5a5,color:#991b1b

The agent is confidently, competently wrong. You must specifically tell it the important decisions in your codebase.

Will this get better?

Maybe. Context windows are growing, models are getting smarter, and the answer is probably more context. The agent that can read the entire codebase, understand the history of every decision, and anticipate where the system is heading in six months will make far fewer wrong calls.

But today's agents don't do that. They see the prompt, the files they're told to read, and nothing else. They have no foresight about where the system needs to go, and no memory of how it got here. Until that changes, the gap between "correct in isolation" and "right for the system" is yours to fill.

The agent should fill the gaps, not make the calls

A coding agent can turn a 10x engineer into a 100x engineer, but that doesn't mean the engineer disappears. It means the engineer stops typing and starts thinking.

It's unfortunate we're collectively using these tools very wrong. Outputting slop daily, turning our brains off because the computer "can do our job for us".

Data models, service boundaries, key abstractions, etc. These are one-way doors, decisions that are hard or impossible to reverse once they're in production. The agent should never be the one walking through a one-way door alone. That's where you come in.

This doesn't mean you dictate every schema and interface upfront. The agent's first draft of a plan is a starting point, and it's usually terrible. Wrong cardinality, missing constraints, boolean fields where you need an enum, no thought given to how the data will be queried six months from now. But that first draft is incredibly useful as something to react to. You read it, tear it apart, annotate it with corrections, and send the agent back to revise. After two or three rounds of this, you end up with abstractions that are better than what either of you would have produced alone, because you're combining the agent's breadth of knowledge with your understanding of the system and the product.

I wrote about how I use Claude Code with a research-plan-implement workflow built around exactly this kind of iterative refinement.

graph TD
    A[Engineer defines intent] --> B[Agent drafts plan with code snippets]
    B --> C[First draft is usually wrong]
    C --> D[Engineer reviews and annotates]
    D --> E[Agent revises]
    E --> F{Good enough?}
    F -->|No| D
    F -->|Yes| G[Agent implements]
    style C fill:#fee2e2,stroke:#fca5a5,color:#991b1b
    style D fill:#ede9fe,stroke:#c4b5fd,color:#5b21b6
    style G fill:#d1fae5,stroke:#6ee7b7,color:#065f46

I don't write code anymore, but I read pretty much every single line my agent writes. Every time I have let the agent loose without a tight plan, I have regretted it a couple of weeks later, always the same things: bad database schemas, boolean fields everywhere, lack of analytics, and don't get me started on observability.

The answer is not to stop using agents

Slop creep is real, but it is not a reason to stop using coding agents. They're the best thing that has happened to software development in years, and I can now build things I didn't know I had in me to build. I have no intention of going back to typing every character of my codebase myself.

But the planning phase deserves ten times more attention than most people give it. Not a vague description of the feature. Actual code snippets for the key data models. Actual interfaces for the key abstractions. Enough that the agent can't get the important stuff wrong, because there's no ambiguity left to fill with slop.

graph TD
    A[Vague plan] --> B[Agent makes architectural decisions]
    B --> C[Slop creep]
    D[Tight plan with code snippets] --> E[Agent executes within constraints]
    E --> F[Clean codebase]
    style C fill:#fee2e2,stroke:#fca5a5,color:#991b1b
    style F fill:#d1fae5,stroke:#6ee7b7,color:#065f46

Spend more time in the plan. Write the code snippets for the decisions that matter. Then let the agent cook.

"Code is the new assembly"

There's a popular counterargument: none of this matters. Code is the new assembly language. Nobody reads it. The agent writes it, the tests pass, the feature works, who cares if the internals are ugly?

This misunderstands what a compiler does. The role of a compiler is to translate higher-level languages into efficient, performant machine code. A compiler that produced bloated, redundant, subtly incorrect machine code would not be used by anyone serious about building software.

If your agent is the compiler, and the output is slop, you don't have a compiler. You have a liability.

graph TD
    E[Good agent workflow] --> F[Clean, maintainable code]
    G[Vibe coding] --> H[Slop that ships]
    style F fill:#d1fae5,stroke:#6ee7b7,color:#065f46
    style H fill:#fee2e2,stroke:#fca5a5,color:#991b1b

And the enshittification doesn't stay in the code, it spills into the user experience. Every vibe-coded app has the same telltale signs, a plethora of tiny cuts that make the whole thing feel off:

  • An app that hogs all the available resources on your computer
  • A button that doesn't quite disable when it should
  • A loading state that flickers
  • A form that loses your input on navigation
  • An error message that says "something went wrong" because nobody modelled the failure modes

None of these show up in a demo. All of them are bugs your users feel every single day. Slop creep in the codebase becomes slop creep in the product, and your users will not read your code to figure out why the experience is bad. They'll just leave.

The job has changed, not disappeared

Honestly, everything in this post could be obsolete with the next major model release. An agent that truly understands systems holistically, that has the foresight to see where a codebase is heading and the context to know why it got here, would change the equation entirely.

But that's not what we have today. And in the next 6 to 12 months, the engineer who makes the difference is the one who can look at an agent's output and say "this is wrong for reasons you can't see from where you're standing." The one who knows which doors are one-way, which abstractions will calcify, and which corners will cost you later.

Until the models catch up, fight slop creep. The enshittification of software is quiet, entirely preventable, and happening everywhere right now.

March 13, 2026

let the code do the talking

Solving the decision problem ·

(why llms and safe sandboxes may change the basic contract between users and software)

Anyhenge

Zeke Sikelianos ·

See what dates the sunset aligns with any street grid on Earth.

March 11, 2026

Migrating My Blog to Astro 6

Gift Egwuenu ·

Astro 6 just dropped and I couldn't resist upgrading my blog right away. I've been running on Astro 5.16 for a while and the release notes had a few features that caught my eye immediately. Let me walk you through the migration, what stood out, and the features I'm most excited about.

TL;DR

  • Migrated from Astro 5.16 to Astro 6 in about an hour.
  • The biggest wins for me: first-class Cloudflare support (migrated from Pages to Workers and consolidated a standalone likes API into the main codebase) and the built-in Fonts API (goodbye render-blocking Google Fonts imports).
  • Live Content Collections are now stable, bringing request-time content fetching to Astro's content layer.
  • Zod now imports from astro/zod instead of astro:content.
  • Enabled experimental queued rendering for faster builds.
  • Zero errors on the first build. Smoothest major version upgrade I've done with Astro.

The Features I'm Loving

First-Class Cloudflare Support

This is the big one for me. I was previously deploying my blog to Cloudflare Pages with the old @astrojs/cloudflare adapter. It worked, but the dev experience had rough edges: the adapter didn't run workerd locally, so I was always testing against Node.js and hoping nothing broke in production. I also had a per-post like counter running as a separate Worker with its own wrangler config and deployment pipeline. It was a small API, but maintaining it as a standalone project just to increment a number felt like overkill.

With Astro 6, the rebuilt adapter now runs workerd at every stage: dev, prerender, and production. That gave me the confidence to migrate the blog from Pages to Workers. I consolidated that standalone likes API directly into the Astro project. One codebase, one deployment, full access to bindings during development. It's the setup I always wanted.

Built-in Fonts API

This is the other feature I was really excited about. Before Astro 6, I was loading Inter and JetBrains Mono via Google Fonts @import in my CSS. This is render-blocking, sends user data to Google, and honestly just feels wrong for a static site that cares about performance.

Now I just configure fonts directly in astro.config.ts:


	fonts: [
		{
			provider: fontProviders.google(),
			name: "Inter",
			cssVariable: "--font-inter",
			weights: [400, 500, 600, 700],
			styles: ["normal"],
			fallbacks: ["sans-serif"],
		},
		{
			provider: fontProviders.google(),
			name: "JetBrains Mono",
			cssVariable: "--font-jetbrains-mono",
			weights: [400, 500],
			styles: ["normal"],
			fallbacks: ["monospace"],
		},
	],
});

Then add a `` component in my layout's <head>:

---

---

<head>
	
	
</head>

Astro downloads the fonts at build time, generates optimized fallbacks, and serves them from my domain. No more third-party requests. No more render-blocking imports. The fonts are just there, and they're fast.

Experimental Queued Rendering

My blog has 25+ posts with OG image generation for each one, so build performance matters. The new queued rendering replaces Astro's recursive rendering with a two-pass approach: first traverse the component tree, then render it in order. Early benchmarks show up to 2x faster rendering.

Enabling it is just a config flag:


	experimental: {
		queuedRendering: {
			enabled: true,
		},
	},
});

I've enabled this and I'm curious to see how it performs as I add more content.

Live Content Collections

Live Content Collections are now stable in Astro 6. Content collections have always required a rebuild when content changed. Live collections flip that: they fetch content at request time using defineLiveCollection() and getLiveEntry(), with no rebuild needed. Your content updates the moment it's published.

You define a live collection in src/live.config.ts:





const updates = defineLiveCollection({
	loader: cmsLoader({ apiKey: process.env.MY_API_KEY }),
	schema: z.object({
		slug: z.string(),
		title: z.string(),
		excerpt: z.string(),
		publishedAt: z.coerce.date(),
	}),
});

Then query it in your page with built-in error handling:

---


const { entry: update, error } = await getLiveEntry("updates", Astro.params.slug);

if (error || !update) {
	return Astro.redirect("/404");
}
---

<h1>{update.data.title}</h1>
<p>{update.data.excerpt}</p>
<time>{update.data.publishedAt.toDateString()}</time>

I'm not using this yet since my blog posts are all Markdown files in the repo, but now that I'm running on Workers with full binding access, I can see pairing this with a CMS or D1-backed content source down the line. The fact that live and build-time collections use the same APIs (getCollection(), getEntry(), schemas, loaders) makes it easy to adopt incrementally.

Zod 4

The Zod upgrade is mostly invisible if your schemas are straightforward. The main change is where you import it from. Instead of importing z from astro:content, you now import it from astro/zod:



If you're using .default() with .transform(), check the Zod 4 changelog because the behavior around default values changed.

How I Migrated

The actual migration took about an hour. Here's the rough process:

  1. Created a branch. Always migrate on a separate branch.
  2. Ran pnpm dlx @astrojs/upgrade. This handles the package version bumps automatically.
  3. Updated Zod imports. z from astro/zod instead of astro:content.
  4. Migrated fonts. Removed Google Fonts @import, configured the new Fonts API, added `` to my layout.
  5. Migrated from Pages to Workers. Switched to the rebuilt @astrojs/cloudflare adapter and consolidated my standalone likes API Worker into the Astro project.
  6. Enabled experimental features. Queued rendering for faster builds.
  7. Built and tested. pnpm build + pnpm check, zero errors on the first try.

What I'm Planning Next

Now that I'm on Astro 6, there are a few things I want to explore:

  • Live Content Collections with a CMS. Now that I'm on Workers with full binding access, I want to pair live collections with a headless CMS so content updates go live without a rebuild.
  • Responsive images. Astro's image handling keeps getting better and I'm not using srcset/sizes anywhere yet.
  • View Transitions. I've been putting this off, but Astro's `` has matured a lot since it was introduced.
  • Tailwind v4. The @astrojs/tailwind integration works fine for now, but Tailwind v4 with its native Vite plugin is the future.

If you're still on Astro 5, I'd recommend giving the upgrade a try. The migration path is smooth and the new features are worth it. Have you upgraded yet? I'd love to hear how it went.

Resources

March 08, 2026

March 04, 2026

Dotfiles: One command to set up any Mac

code.charliegleason.com ·

Setting up a new Mac used to take me days. I'd install Homebrew, configure my shell, set up Git with SSH keys, install Node, download my editor extensions, and inevitably realize at some point that I'd forgotten to copy over some config file.

I came across Matt Silverlock's dotfiles and realized this could be automated. The idea is simple - version control your configuration so one command reproduces your entire environment on a new machine.

My dotfiles now handle everything from Xcode Command Line Tools to VS Code extensions. What used to take hours now happens while I make a crisp beverage.

What it does

  1. Checks prerequisites - Verifies macOS and internet connection
  2. Installs Xcode CLT - The foundation for everything else
  3. Installs Homebrew - Package manager
  4. Installs packages - From a Brewfile with CLI tools and apps
  5. Sets up mise - Manages Node, Bun, pnpm, Python versions
  6. Configures zsh - oh-my-zsh with plugins and theme
  7. Generates SSH keys - Ed25519 keys with commit signing
  8. Symlinks dotfiles - GNU Stow links configs into place
  9. Sets up secrets - API token configuration
  10. Installs editor extensions - VS Code and Cursor
  11. Imports Raycast settings - Window management and shortcuts

The script is idempotent - safe to run multiple times, skipping what's already installed.

Work mode

The --work flag separates personal from work setups, given I don't need certain tools or apps on a work laptop, but I want a consistent environment for non-sensitive configurations.

Running sh install.sh --work swaps the Brewfile:

  • Brewfile - Core tools for everyone
  • Brewfile.personal - Personal stuff
  • Brewfile.work - Work-specific stuff

CLI tools that make a difference

These tools fundamentally change how I work in the terminal:

  • bat - Syntax-highlighting cat
  • delta - Beautiful git diffs
  • fd - Intuitive find replacement
  • fzf - Fuzzy finder bound to Ctrl+T and Ctrl+R
  • ripgrep - Fast search respecting .gitignore
  • zoxide - Smarter cd that learns your directories

Managing configuration with Stow

GNU Stow manages the dotfiles by creating symlinks from ~ back to the repo. Edit ~/.zshrc and the change is immediately in version control.

Stow has a .stow-local-ignore file that prevents certain files from being linked. I use it to skip the README, install scripts, and anything else that shouldn't live in ~:

\.git
\.gitignore
README.md
install.sh
Brewfile

My .zshrc configures fzf with custom previews, integrates zoxide, and activates mise. Everything is configured exactly how I like it, on every machine.

Should you do this?

If you set up more than one Mac every couple of years, yes. The investment pays for itself quickly. It's also very comforting.

Start small - fork my repo or Matt's, and edit it to your liking. You could just use Homebrew, your shell, and your editor, and add more as you find repetitive tasks.

February 20, 2026

The Software Development Lifecycle Is Dead

Boris Tane ·

AI agents didn't make the SDLC faster. They killed it.

I keep hearing people talk about AI as a "10x developer tool." That framing is wrong. It assumes the workflow stays the same and the speed goes up. That's not what's happening. The entire lifecycle, the one we've built careers around, the one that spawned a multi-billion dollar tooling industry, is collapsing in on itself.

And most people haven't noticed yet.

The SDLC you learned is a relic

Here's the classic software development lifecycle most of us were taught:

graph TD
    A[Requirements] --> B[System Design]
    B --> C[Implementation]
    C --> D[Testing]
    D --> E[Code Review]
    E --> F[Deployment]
    F --> G[Monitoring]
    G --> A

Every stage has its own tools, its own rituals, its own cottage industry. Jira for requirements. Figma for design. VS Code for implementation. Jest for testing. GitHub for code review. AWS for deployment. Datadog for monitoring.

Each step is discrete. Sequential. Handoffs everywhere.

Now here's what actually happens when an engineer works with a coding agent:

graph TD
    A[Intent] --> B[Agent]
    B --> C[Code + Tests + Deployment]
    C --> D{Does it work?}
    D -->|No| B
    D -->|Yes| E[Ship]
    style E fill:#d1fae5,stroke:#6ee7b7,color:#065f46

The stages collapsed. They didn't get faster. They merged. The agent doesn't know what step it's on because there are no steps. There's just intent, context, and iteration.

AI-native engineers don't know what the SDLC is

I spent a lot of time speaking with engineers who started their career after Cursor launched. They don't know what the software development lifecycle is. They don't know what's DevOps or what's an SRE. Not because they're bad engineers. Because they never needed it. They've never sat through sprint planning. They've never estimated story points. They've never waited three days for a PR review.

They just build things.

You describe what you want. The agent writes the code. You look at it. You iterate. You ship. Everything simultaneously.

These engineers aren't worse for skipping the ceremony. They're unencumbered by it. Sprint planning, code review workflows, release trains, estimation rituals. None of it. They skipped the entire orthodoxy and went straight to building.

And honestly? I'm jealous.

Every stage is collapsing

Let me walk through the SDLC and show you what's left of it.

Requirements gathering: fluid, not dictated

Requirements used to be handed down. A PM writes a PRD, engineers estimate it, and the spec gets frozen before a line of code is written. That made sense when building was expensive. When every feature took weeks, you had to decide upfront what to build.

That constraint is gone. When an agent can generate a complete version of a feature in minutes, you don't need to specify every detail in advance. You provide the direction, the agent builds a version, you look at it, you adjust, you try a different approach. You can generate ten versions and pick the best one. Requirements aren't a phase anymore. They're a byproduct of iteration.

Now, what is Jira when the audience isn't humans coordinating across a pipeline? What is Jira when it's agents consuming context? Jira was built to track work through stages that no longer exist. If your "requirements" are just context for an agent, then the ticketing system isn't a project management tool anymore. It's a context store. And it's a terrible one.

System Design: discovered, not dictated

System design still matters. But the way it happens is fundamentally shifting.

Design used to be something you did before writing code. You'd whiteboard the architecture, debate trade-offs, draw boxes and arrows, then go implement it. The gap between the design and the code was days or weeks.

That gap is closing. Design is becoming something you discover by giving the agent the right context, not something you dictate ahead of time. The model has seen more systems, more architectures, more patterns than any individual engineer. When you describe a problem, the agent doesn't just implement your design, it suggests architectures that are often superior to what you'd have come up with on your own. You're having a design conversation in real-time, and the output is working code.

You still need to know when an agent is over-engineering or missing a constraint. But you're collaborating on design, not prescribing it.

Implementation: this is the agent's job now

This one is obvious. The agent writes the code. Whole features. Complete solutions with error handling, types, edge cases.

I don't personally know anyone who still types lines of code. We review what agents write, feed them context, steer direction, and focus on the problems that actually require human judgment.

Testing: simultaneous, not sequential

Agents write tests alongside the code. Not as an afterthought. Not in a separate "testing phase." The test is part of the generation. TDD isn't a methodology anymore, it's just how agents work by default.

The entire QA function as a separate stage is gone. When code and tests are generated together, verified together, and iterated together, there's no handoff. No "throw it over the wall to QA.". The agent can do the QA itself.

Code review: give it up

The pull request flow needs to go. I was never a fan, but now it's just a relic of the past.

I know that's uncomfortable. Code review is sacred. It's how you catch bugs, share knowledge, maintain standards. It's also an identity thing. We're engineers, and reviewing code is what engineers do. But clinging to the PR workflow in an agent-driven world isn't rigor. It's an identity crisis.

Think about it. An agent generates 500 PRs a day. Your team can review maybe 10. The review queue backs up. This isn't a bottleneck worth optimising. It's a fake bottleneck, one that only exists because we're forcing a human ritual onto a machine workflow.

graph TD
    A[Agent generates PR] --> B[Waits for human review]
    B --> C{Reviewer available?}
    C -->|No| D[Sits in queue for hours/days]
    C -->|Yes| E[Review + Comments]
    E --> F[Agent addresses feedback]
    F --> B
    D --> B
    style B fill:#fee2e2,stroke:#fca5a5,color:#991b1b
    style D fill:#fee2e2,stroke:#fca5a5,color:#991b1b

This diagram shouldn't exist. The entire flow is wrong.

The review has to be rethought from scratch. Either it becomes part of the code generation itself, the agent verifies its own work against the plan document, runs the tests, checks for regressions, validates against architectural constraints, or a second agent reviews the first agent's output. Adversarial agents plough through the proposed changes, try to break it in every dimension. We already have the tools for this. Human-in-the-loop review becomes exception-based, triggered only when automated verification can't resolve a conflict or when the change touches something genuinely novel.

What does a world without pull requests look like? Agents commit to main. Automated checks, tests, type checks, security scans, behavioral diffs, validate the change. If everything passes, it ships, automatically. If something fails, the agent fixes it. A human only gets involved when the system genuinely doesn't know what to do.

graph TD
    A[Agent generates code] --> B[Agent self-verifies]
    B --> C[Second agent reviews]
    C --> D[Automated checks]
    D --> E{All clear?}
    E -->|Yes| F[Ship]
    E -->|No - resolvable| A
    E -->|No - novel issue| G[Human review]
    G --> A
    style F fill:#d1fae5,stroke:#6ee7b7,color:#065f46

We're spending our review cycles reading diffs that an agent could verify in seconds. That's not quality assurance. That's luddism.

Deployment: decoupled and continuous

Agents are already writing deployment pipelines that are more intricate and more specialised than what most teams would ever bother building by hand. Feature flags, canary releases, progressive rollouts, automatic rollback triggers, the kind of release engineering that used to require a dedicated platform team.

The key shift is that agents naturally decouple deployment from release. Code gets deployed continuously, every change, as soon as it's generated and verified, produces an artifact that lands in production behind a gate. Release is a separate decision, driven by feature flags or traffic rules.

Some teams are already approaching true continuous deployment and release. Code is generated, tests pass, artifacts are built, and the change is live, all in a single automated flow with no human in the loop between intent and production.

Where this goes next is even more interesting. Imagine agents that don't just deploy code but manage the entire release lifecycle, monitoring the rollout, adjusting traffic percentages based on error rates, automatically rolling back if latency spikes, and only notifying a human when something genuinely novel goes wrong. The deployment "stage" doesn't just get automated. It becomes an ongoing, self-adjusting process that never really ends.

graph TD
    A[Agent generates code] --> B[Automated verification]
    B --> C[Artifact produced]
    C --> D[Deploy behind feature flag]
    D --> E[Progressive rollout]
    E --> F{Healthy?}
    F -->|Yes| G[Full release]
    F -->|No| H[Auto-rollback]
    H --> I[Agent investigates]
    I --> A
    style G fill:#d1fae5,stroke:#6ee7b7,color:#065f46
    style H fill:#fee2e2,stroke:#fca5a5,color:#991b1b

Monitoring: the last stage standing, and it needs to evolve

Monitoring is the only stage of the SDLC that survives. And it doesn't just survive, it becomes the foundation everything else rests on.

When agents ship code faster than humans can review it, observability is no longer a nice-to-have dashboarding layer. It's the primary safety mechanism for the entire collapsed lifecycle. Every other safeguard, the design review, the code review, the QA phase, the release sign-off, has been absorbed or eliminated. Monitoring is what's left. It's the last line of defense.

But most observability platforms were built for humans. Alerts, log search, dashboard, etc. all designed for a person to look at, interpret, and act on. That model breaks when the volume of changes outpaces human attention. If an agent ships 500 changes a day and your observability setup requires a human to investigate each anomaly, you've created a new bottleneck. You've just moved it from code review to incident response.

Observability without action is just expensive storage. The future of observability isn't dashboards, it's closed-loop systems where telemetry data becomes context for the agent that shipped the code, so it can detect the regression and fix it.

The observability layer becomes the feedback mechanism that drives the entire loop. Not a stage at the end. The connective tissue of the whole system.

graph TD
    A[Intent] --> B[Agent builds, tests, deploys]
    B --> C[Production]
    C --> D[Observability layer]
    D -->|Anomaly detected| E[Agent investigates + fixes]
    E --> B
    D -->|Healthy| F[Next intent]
    F --> A
    style D fill:#dbeafe,stroke:#93c5fd,color:#1e40af

The teams that figure this out first, observability that feeds directly back into the agent loop, not into a human's pager, will ship faster and safer than everyone else. The teams that don't will drown in alerts.

The new lifecycle is tighter loop

The SDLC was a wide loop. Requirements → Design → Code → Test → Review → Deploy → Monitor. Linear. Sequential. Full of handoffs and waiting.

The new lifecycle is a tight loop.

graph TD
    A[Human Intent + Context] --> B[AI Agent]
    B --> C[Build + Test + Deploy]
    C --> D[Observe]
    D -->|Problem| B
    D -->|Fine| E[Next Intent]
    E --> B
    style B fill:#ede9fe,stroke:#c4b5fd,color:#5b21b6

Intent. Build. Observe. Repeat.

No tickets. No sprints. No story points. No PRs sitting in a queue. No separate QA phase. No release trains.

Just a human with intent and an agent that executes.

So what is left?

Context. That's it.

The quality of what you build with agents is directly proportional to the quality of context you give them. Not the process. Not the ceremony. The context.

The SDLC is dead. The new skill is context engineering. The new safety net is observability.

And most of the industry is still configuring Datadog dashboards no one looks at.

February 18, 2026

February 12, 2026

Rebuilding charliegleason.com

code.charliegleason.com ·

Back in June 2024, I wrote about a hack I'd cobbled together to open-source my personal site while keeping some routes behind a password. The gist: npm link and sync-directory would watch a private repo and pipe protected routes into the public Remix app's node_modules. It worked. It also felt like it could fall apart at any moment, which didn't feel great.

The site has been completely rebuilt. The new charliegleason.com is a proper pnpm monorepo running Astro on Cloudflare Workers, with Durable Objects for real-time features and a real session-based authentication system. No more symlinks, either.

The architecture

The monorepo has four packages:

  • @charliegleason/web - The Astro SSR site, deployed as a Cloudflare Worker.
  • @charliegleason/visitor-counter - A Durable Object that tracks real-time visitors via WebSocket.
  • @charliegleason/lastfm-tracker - A Durable Object that broadcasts what I'm listening to on Last.fm via WebSocket.
  • @charliegleason/private - Protected content that never leaves the private repo.

Each package has its own wrangler.jsonc and its own deploy step. You find them all on GitHub: charliegleason/charliegleason.com.

Authentication flow

The auth system is deliberately simple. Password-based, session-stored, no OAuth dance.

The flow: visit a protected route, middleware redirects you to /login, and you enter the password. A session gets created in Cloudflare KV with a 7-day TTL, a cookie gets set, and you're redirected back to where you were trying to go. KV handles expiration automatically.

I'm using Effect for the session management, mostly because I wanted typed error handling without a bunch of try/catch nesting. The createSession function is a good example of what that looks like:

export const createSession = (
  kv: KVNamespace,
  userId: string,
): Effect.Effect<string, KVError> =>
  Effect.gen(function* () {
    const sessionId = crypto.randomUUID();
    const now = Date.now();

    const session: Session = {
      userId,
      createdAt: now,
      expiresAt: now + SESSION_DURATION_MS,
    };

    yield* Effect.tryPromise({
      try: () =>
        kv.put(`session:${sessionId}`, JSON.stringify(session), {
          expirationTtl: SESSION_DURATION_SECONDS,
        }),
      catch: (error) =>
        new KVError({
          operation: "put",
          key: `session:${sessionId}`,
          message: "Failed to create session",
          cause: error,
        }),
    });

    yield* Effect.logDebug(`Created session: ${sessionId}`);
    return sessionId;
  });

A UUID session ID, a JSON blob in KV, and a TTL that means I never have to clean up stale sessions. If you're thinking "that's a key-value store with extra steps," you're right, but the extra steps have types.

Protected routes injection

This is the part I'm most pleased with. Instead of the old symlink-and-sync approach, I (read: the robots) wrote an Astro integration that scans the private content directory at build time and uses injectRoute to register protected pages:

export default function protectedRoutes(
  options: ProtectedRoutesOptions = {},
): AstroIntegration {
  return {
    name: "protected-routes",
    hooks: {
      "astro:config:setup": ({ injectRoute, config, logger }) => {
        const rootDir = fileURLToPath(config.root);
        const protectedDir = options.protectedDir || "../private/content";
        const contentDir = join(rootDir, protectedDir);

        if (!existsSync(contentDir)) {
          logger.info("No protected content directory found");
          logger.info("This is expected in public mirror builds");
          return;
        }

        const routes = findAstroFiles(contentDir, contentDir);

        for (const route of routes) {
          logger.info(`Injecting protected route: ${route.pattern}`);
          injectRoute({
            pattern: route.pattern,
            entrypoint: route.entrypoint,
          });
        }
      },
    },
  };
}

The key detail: when the private package isn't there - like in public mirror builds - it logs a friendly message and moves on. No crash, no build failure. The public site builds and deploys perfectly fine without the protected content. The private monorepo builds with everything.

Durable Objects for real-time features

I wanted a live visitor counter and a now-playing widget. Both felt like natural fits for Durable Objects - they're stateful, long-lived, and need to broadcast to multiple clients.

Visitor counter

The visitor counter is ridiculous simple. The count is the number of open WebSocket connections. No database, no persistence needed. Someone connects, the count goes up. Someone disconnects, the count goes down. Everyone gets a broadcast.

export class VisitorCounter extends DurableObject<Env> {
  async fetch(request: Request): Promise<Response> {
    const upgradeHeader = request.headers.get("Upgrade");
    if (upgradeHeader !== "websocket") {
      return new Response("Expected WebSocket", { status: 426 });
    }

    const webSocketPair = new WebSocketPair();
    const [client, server] = Object.values(webSocketPair);

    this.ctx.acceptWebSocket(server);
    this.broadcast();

    return new Response(null, {
      status: 101,
      webSocket: client,
    });
  }

  async webSocketClose(ws: WebSocket, code: number, reason: string): Promise<void> {
    ws.close(code, reason);
    this.broadcast();
  }

  private getCount(): number {
    return this.ctx.getWebSockets().length;
  }

  private broadcast(): void {
    const count = this.getCount();
    const message = JSON.stringify({ count });
    for (const ws of this.ctx.getWebSockets()) {
      try { ws.send(message); } catch {}
    }
  }
}

WebSocket hibernation means Cloudflare isn't charging me for idle connections, and the global singleton pattern means everyone sees the same count. It's the kind of feature that's disproportionately fun relative to the effort involved.

Last.fm tracker

The Last.fm tracker is slightly more involved. The frontend connects directly to the tracker Worker via WebSocket - it doesn't go through the main Astro app at all. When a connection comes in, the Durable Object immediately sends the current track (if it has one) and starts polling. Every 30 seconds, an Alarm fires, hits the Last.fm API, and checks if the track has changed. If it has, it broadcasts to all connected clients. If not, it does nothing.

The current track gets stored in Durable Object storage so it survives cold starts - when the DO spins back up, it restores from storage in the constructor before accepting any connections. The Last.fm API response includes a nowplaying attribute, which gets passed through as an isNowPlaying flag. The frontend uses that to show "Listening to" with animated equalizer bars when music is playing, or "Last played" with static bars when it's not.

One nice detail: the Alarm only reschedules itself if there are active WebSocket connections. No listeners, no polling. It starts up again when someone connects.

Hosting on Cloudflare

Everything runs on Cloudflare's edge. The main Astro site is a Worker. Sessions live in KV. The Durable Objects are deployed as independent Workers with service bindings connecting them to the main app.

The wrangler.jsonc for the web app ties it all together:

{
  "name": "astro-charliegleason-com",
  "kv_namespaces": [{ "binding": "SESSION", "id": "..." }],
  "durable_objects": {
    "bindings": [
      { "name": "VISITOR_COUNTER", "class_name": "VisitorCounter", "script_name": "visitor-counter" },
      { "name": "LASTFM_TRACKER", "class_name": "LastFmTracker", "script_name": "lastfm-tracker" }
    ]
  }
}

One thing that tripped me up: deployment order matters. The Durable Object Workers need to exist before the web app can bind to them. GitHub Actions handles the sequencing - Durable Objects deploy first, then the web app. I learned this the hard way, which is how I learn most things.

The public mirror

The mental model has completely flipped from the old approach. Previously, the public repo was the source of truth - the private repo consumed it via npm link and sync-directory. Now the private monorepo is the source of truth, and git subtree push mirrors apps/web/ to the public repo. Protected content never leaves the private repo. It never gets committed to the public mirror. It's a much cleaner separation.

Wrapping up

The old system worked, but the new setup works better. It's a real monorepo with real auth, real-time features, and a deployment pipeline that doesn't make me nervous. It's the kind of rebuild where the end result looks simple, which I think means it was worth doing.

February 11, 2026

Components Will Kill Pages

Brayden Wilmoth ·

Components allow users using AI applications to experience your brand when pages can't

February 10, 2026

How I Use Claude Code

Boris Tane ·

I've been using Claude Code as my primary development tool for approx 9 months, and the workflow I've settled into is radically different from what most people do with AI coding tools. Most developers type a prompt, sometimes use plan mode, fix the errors, repeat. The more terminally online are stitching together ralph loops, mcps, gas towns (remember those?), etc. The results in both cases are a mess that completely falls apart for anything non-trivial.

The workflow I'm going to describe has one core principle: never let Claude write code until you've reviewed and approved a written plan. This separation of planning and execution is the single most important thing I do. It prevents wasted effort, keeps me in control of architecture decisions, and produces significantly better results with minimal token usage than jumping straight to code.

flowchart LR
    R[Research] --> P[Plan]
    P --> A[Annotate]
    A -->|repeat 1-6x| A
    A --> T[Todo List]
    T --> I[Implement]
    I --> F[Feedback & Iterate]

Phase 1: Research

Every meaningful task starts with a deep-read directive. I ask Claude to thoroughly understand the relevant part of the codebase before doing anything else. And I always require the findings to be written into a persistent markdown file, never just a verbal summary in the chat.

read this folder in depth, understand how it works deeply, what it does and all its specificities. when that's done, write a detailed report of your learnings and findings in research.md

study the notification system in great details, understand the intricacies of it and write a detailed research.md document with everything there is to know about how notifications work

go through the task scheduling flow, understand it deeply and look for potential bugs. there definitely are bugs in the system as it sometimes runs tasks that should have been cancelled. keep researching the flow until you find all the bugs, don't stop until all the bugs are found. when you're done, write a detailed report of your findings in research.md

Notice the language: "deeply", "in great details", "intricacies", "go through everything". This isn't fluff. Without these words, Claude will skim. It'll read a file, see what a function does at the signature level, and move on. You need to signal that surface-level reading is not acceptable.

The written artifact (research.md) is critical. It's not about making Claude do homework. It's my review surface. I can read it, verify Claude actually understood the system, and correct misunderstandings before any planning happens. If the research is wrong, the plan will be wrong, and the implementation will be wrong. Garbage in, garbage out.

This is the most expensive failure mode with AI-assisted coding, and it's not wrong syntax or bad logic. It's implementations that work in isolation but break the surrounding system. A function that ignores an existing caching layer. A migration that doesn't account for the ORM's conventions. An API endpoint that duplicates logic that already exists elsewhere. The research phase prevents all of this.

Phase 2: Planning

Once I've reviewed the research, I ask for a detailed implementation plan in a separate markdown file.

I want to build a new feature <name and description> that extends the system to perform <business outcome>. write a detailed plan.md document outlining how to implement this. include code snippets

the list endpoint should support cursor-based pagination instead of offset. write a detailed plan.md for how to achieve this. read source files before suggesting changes, base the plan on the actual codebase

The generated plan always includes a detailed explanation of the approach, code snippets showing the actual changes, file paths that will be modified, and considerations and trade-offs.

I use my own .md plan files rather than Claude Code's built-in plan mode. The built-in plan mode sucks. My markdown file gives me full control. I can edit it in my editor, add inline notes, and it persists as a real artifact in the project.

One trick I use constantly: for well-contained features where I've seen a good implementation in an open source repo, I'll share that code as a reference alongside the plan request. If I want to add sortable IDs, I paste the ID generation code from a project that does it well and say "this is how they do sortable IDs, write a plan.md explaining how we can adopt a similar approach." Claude works dramatically better when it has a concrete reference implementation to work from rather than designing from scratch.

But the plan document itself isn't the interesting part. The interesting part is what happens next.

The Annotation Cycle

This is the most distinctive part of my workflow, and the part where I add the most value.

flowchart TD
    W[Claude writes plan.md] --> R[I review in my editor]
    R --> N[I add inline notes]
    N --> S[Send Claude back to the document]
    S --> U[Claude updates plan]
    U --> D{Satisfied?}
    D -->|No| R
    D -->|Yes| T[Request todo list]

After Claude writes the plan, I open it in my editor and add inline notes directly into the document. These notes correct assumptions, reject approaches, add constraints, or provide domain knowledge that Claude doesn't have.

The notes vary wildly in length. Sometimes a note is two words: "not optional" next to a parameter Claude marked as optional. Other times it's a paragraph explaining a business constraint or pasting a code snippet showing the data shape I expect.

Some real examples of notes I'd add:

  • "use drizzle:generate for migrations, not raw SQL" -- domain knowledge Claude doesn't have
  • "no -- this should be a PATCH, not a PUT" -- correcting a wrong assumption
  • "remove this section entirely, we don't need caching here" -- rejecting a proposed approach
  • "the queue consumer already handles retries, so this retry logic is redundant. remove it and just let it fail" -- explaining why something should change
  • "this is wrong, the visibility field needs to be on the list itself, not on individual items. when a list is public, all items are public. restructure the schema section accordingly" -- redirecting an entire section of the plan

Then I send Claude back to the document:

I added a few notes to the document, address all the notes and update the document accordingly. don't implement yet

This cycle repeats 1 to 6 times. The explicit "don't implement yet" guard is essential. Without it, Claude will jump to code the moment it thinks the plan is good enough. It's not good enough until I say it is.

Why This Works So Well

The markdown file acts as shared mutable state between me and Claude. I can think at my own pace, annotate precisely where something is wrong, and re-engage without losing context. I'm not trying to explain everything in a chat message. I'm pointing at the exact spot in the document where the issue is and writing my correction right there.

This is fundamentally different from trying to steer implementation through chat messages. The plan is a structured, complete specification I can review holistically. A chat conversation is something I'd have to scroll through to reconstruct decisions. The plan wins every time.

Three rounds of "I added notes, update the plan" can transform a generic implementation plan into one that fits perfectly into the existing system. Claude is excellent at understanding code, proposing solutions, and writing implementations. But it doesn't know my product priorities, my users' pain points, or the engineering trade-offs I'm willing to make. The annotation cycle is how I inject that judgement.

The Todo List

Before implementation starts, I always request a granular task breakdown:

add a detailed todo list to the plan, with all the phases and individual tasks necessary to complete the plan - don't implement yet

This creates a checklist that serves as a progress tracker during implementation. Claude marks items as completed as it goes, so I can glance at the plan at any point and see exactly where things stand. Especially valuable in sessions that run for hours.

Phase 3: Implementation

When the plan is ready, I issue the implementation command. I've refined this into a standard prompt I reuse across sessions:

implement it all. when you're done with a task or phase, mark it as completed in the plan document. do not stop until all tasks and phases are completed. do not add unnecessary comments or jsdocs, do not use any or unknown types. continuously run typecheck to make sure you're not introducing new issues.

This single prompt encodes everything that matters:

  • "implement it all": do everything in the plan, don't cherry-pick
  • "mark it as completed in the plan document": the plan is the source of truth for progress
  • "do not stop until all tasks and phases are completed": don't pause for confirmation mid-flow
  • "do not add unnecessary comments or jsdocs": keep the code clean
  • "do not use any or unknown types": maintain strict typing
  • "continuously run typecheck": catch problems early, not at the end

I use this exact phrasing (with minor variations) in virtually every implementation session. By the time I say "implement it all," every decision has been made and validated. The implementation becomes mechanical, not creative. This is deliberate. I want implementation to be boring. The creative work happened in the annotation cycles. Once the plan is right, execution should be straightforward.

Without the planning phase, what typically happens is Claude makes a reasonable-but-wrong assumption early on, builds on top of it for 15 minutes, and then I have to unwind a chain of changes. The "don't implement yet" guard eliminates this entirely.

Feedback During Implementation

Once Claude is executing the plan, my role shifts from architect to supervisor. My prompts become dramatically shorter.

flowchart LR
    I[Claude implements] --> R[I review / test]
    R --> C{Correct?}
    C -->|No| F[Terse correction]
    F --> I
    C -->|Yes| N{More tasks?}
    N -->|Yes| I
    N -->|No| D[Done]

Where a planning note might be a paragraph, an implementation correction is often a single sentence:

  • "You didn't implement the deduplicateByTitle function."
  • "You built the settings page in the main app when it should be in the admin app, move it."

Claude has the full context of the plan and the ongoing session, so terse corrections are enough.

Frontend work is the most iterative part. I test in the browser and fire off rapid corrections:

  • "wider"
  • "still cropped"
  • "there's a 2px gap"

For visual issues, I sometimes attach screenshots. A screenshot of a misaligned table communicates the problem faster than describing it.

I also reference existing code constantly:

  • "this table should look exactly like the users table, same header, same pagination, same row density."

This is far more precise than describing a design from scratch. Most features in a mature codebase are variations on existing patterns. A new settings page should look like the existing settings pages. Pointing to the reference communicates all the implicit requirements without spelling them out. Claude would typically read the reference file(s) before making the correction.

When something goes in a wrong direction, I don't try to patch it. I revert and re-scope by discarding the git changes:

  • "I reverted everything. Now all I want is to make the list view more minimal -- nothing else."

Narrowing scope after a revert almost always produces better results than trying to incrementally fix a bad approach.

Staying in the Driver's Seat

Even though I delegate execution to Claude, I never give it total autonomy over what gets built. I do the vast majority of the active steering in the plan.md documents.

This matters because Claude will sometimes propose solutions that are technically correct but wrong for the project. Maybe the approach is over-engineered, or it changes a public API signature that other parts of the system depend on, or it picks a more complex option when a simpler one would do. I have context about the broader system, the product direction, and the engineering culture that Claude doesn't.

flowchart TD
    P[Claude proposes changes] --> E[I evaluate each item]
    E --> A[Accept as-is]
    E --> M[Modify approach]
    E --> S[Skip / remove]
    E --> O[Override technical choice]
    A & M & S & O --> R[Refined implementation scope]

Cherry-picking from proposals: When Claude identifies multiple issues, I go through them one by one: "for the first one, just use Promise.all, don't make it overly complicated; for the third one, extract it into a separate function for readability; ignore the fourth and fifth ones, they're not worth the complexity." I'm making item-level decisions based on my knowledge of what matters right now.

Trimming scope: When the plan includes nice-to-haves, I actively cut them. "remove the download feature from the plan, I don't want to implement this now." This prevents scope creep.

Protecting existing interfaces: I set hard constraints when I know something shouldn't change: "the signatures of these three functions should not change, the caller should adapt, not the library."

Overriding technical choices: Sometimes I have a specific preference Claude wouldn't know about: "use this model instead of that one" or "use this library's built-in method instead of writing a custom one." Fast, direct overrides.

Claude handles the mechanical execution, while I make the judgement calls. The plan captures the big decisions upfront, and selective guidance handles the smaller ones that emerge during implementation.

Single Long Sessions

I run research, planning, and implementation in a single long session rather than splitting them across separate sessions. A single session might start with deep-reading a folder, go through three rounds of plan annotation, then run the full implementation, all in one continuous conversation.

I am not seeing the performance degradation everyone talks about after 50% context window. Actually, by the time I say "implement it all," Claude has spent the entire session building understanding: reading files during research, refining its mental model during annotation cycles, absorbing my domain knowledge corrections.

When the context window fills up, Claude's auto-compaction maintains enough context to keep going. And the plan document, the persistent artifact, survives compaction in full fidelity. I can point Claude to it at any point in time.

The Workflow in One Sentence

Read deeply, write a plan, annotate the plan until it's right, then let Claude execute the whole thing without stopping, checking types along the way.

That's it. No magic prompts, no elaborate system instructions, no clever hacks. Just a disciplined pipeline that separates thinking from typing. The research prevents Claude from making ignorant changes. The plan prevents it from making wrong changes. The annotation cycle injects my judgement. And the implementation command lets it run without interruption once every decision has been made.

Try my workflow, you'll wonder how you ever shipped anything with coding agents without an annotated plan document sitting between you and the code.

February 04, 2026

February 02, 2026

Setting Up a Custom Email with Zoho Mail and Cloudflare

Gift Egwuenu ·

I've always wanted a professional email address for my domain - something like hello@giftegwuenu.com instead of a generic Gmail address.

I've tried using Cloudflare Email Routing in the past, and while it works great for forwarding emails to your personal inbox, I was missing having an actual custom mailbox - a dedicated inbox where I could send and receive emails as my custom address.

After exploring a few options, I landed on Zoho Mail. Here's how I set it up with my Cloudflare-hosted domain.

Why Zoho Mail?

There are several providers for custom domain email - Google Workspace, Microsoft 365, Fastmail, and more. I chose Zoho Mail for a few reasons:

  • Free tier available - Up to 5 users with 5GB storage each
  • Clean interface - Modern UI without the clutter
  • Privacy-focused - No ads, even on the free tier
  • Good deliverability - Emails actually land in inboxes

If you're a freelancer, creator, or just want a professional email without paying monthly fees, Zoho's free tier is hard to beat.

What You'll Need

Before we start, make sure you have:

  • A domain name (I'm using my domain)
  • Access to your Cloudflare DNS dashboard (or if your domain is hosted elsewhere, access to that dashboard to change DNS settings)
  • About 15-20 minutes of your time

Step 1: Sign Up for Zoho Mail

Head to Zoho Mail and click "Sign Up Free". Choose the Personal email which includes:

  • Up to 5 users
  • 5GB storage per user
  • Web access + mobile apps
  • Email hosting for one domain

During signup, you'll enter your domain name and the email address you want to create (e.g., hello@giftegwuenu.com).

After signing up, head over to the Zoho admin panel.

Step 2: Verify Domain Ownership (One-Click Method)

Zoho needs to confirm you own the domain. The good news is that Zoho offers a one-click verification method that makes this incredibly easy. If you use one-click verification, you can also configure your MX, SPF, and DKIM records using the same method - no manual DNS editing required!

Using One-Click Verification (Recommended)

  1. In the Zoho setup wizard, look for the One-click verification option
  2. Authenticate with your DNS provider (Cloudflare, in my case)
  3. Zoho will automatically add the verification TXT record to your domain
  4. Once verified, you can use the same one-click method for MX, SPF, and DKIM records

This is the method I used, and it took less than a minute to configure everything. Zoho handles all the DNS record creation for you.

Manual Method (If One-Click Isn't Available)

If one-click verification isn't available for your DNS provider, you can add the records manually. Zoho will give you a verification code that looks something like:

zoho-verification=zb12345678.zmverify.zoho.com

Add this TXT record in Cloudflare:

  1. Log in to your Cloudflare dashboard
  2. Select your domain
  3. Go to DNS > Records
  4. Click Add record
  5. Configure the record:
Type Name Content TTL
TXT @ zoho-verification=zb12345678.zmverify.zoho.com Auto

Click Save and head back to Zoho to verify. It usually takes a few minutes for DNS to propagate.

Step 3: Configure MX Records

If you used one-click verification, you can configure MX records the same way - just click the button and Zoho handles it automatically.

For manual configuration, MX (Mail Exchange) records tell the internet where to deliver emails for your domain. This is the most important step.

In Cloudflare, add the following MX records:

Type Name Mail server Priority TTL
MX @ mx.zoho.com 10 Auto
MX @ mx2.zoho.com 20 Auto
MX @ mx3.zoho.com 50 Auto

The priority numbers matter - lower numbers have higher priority. If the primary server (mx.zoho.com) is unavailable, email routes to the backup servers.

Step 4: Set Up SPF Record

Again, if you used one-click verification, Zoho can add this automatically for you.

SPF (Sender Policy Framework) helps prevent email spoofing by specifying which servers can send email on behalf of your domain.

For manual setup, add this TXT record:

Type Name Content TTL
TXT @ v=spf1 include:zoho.com ~all Auto

Note: If you already have an SPF record (maybe from another service), don't create a duplicate. Instead, merge them:

v=spf1 include:zoho.com include:other-service.com ~all

Step 5: Configure DKIM

One-click verification supports DKIM too! If you're doing it manually, here's how:

DKIM (DomainKeys Identified Mail) adds a digital signature to your emails, proving they came from your domain. This improves deliverability significantly.

In Zoho Mail:

  1. Go to Admin Console > Email Authentication > DKIM
  2. Click Add Selector
  3. Zoho will generate a TXT record value for you

Add the DKIM record in Cloudflare:

Type Name Content TTL
TXT zmail._domainkey v=DKIM1; k=rsa; p=MIGfMA0GCS... (your key) Auto

The selector name might be different (like default._domainkey or zoho._domainkey) - use whatever Zoho provides.

Step 6: Add DMARC Record (Optional but Recommended)

DMARC ties SPF and DKIM together and tells receiving servers what to do with emails that fail authentication.

Add this TXT record:

Type Name Content TTL
TXT _dmarc v=DMARC1; p=none; rua=mailto:hello@giftegwuenu.com Auto

The p=none policy means you're just monitoring for now. Once you're confident everything works, you can change it to p=quarantine or p=reject.

Final DNS Configuration

Here's what your Cloudflare DNS records should look like when you're done:

# MX Records
@    MX    mx.zoho.com     10
@    MX    mx2.zoho.com    20
@    MX    mx3.zoho.com    50

# TXT Records
@              TXT    zoho-verification=zb12345678.zmverify.zoho.com
@              TXT    v=spf1 include:zoho.com ~all
zmail._domainkey    TXT    v=DKIM1; k=rsa; p=MIGfMA0GCS...
_dmarc         TXT    v=DMARC1; p=none; rua=mailto:hello@giftegwuenu.com

Testing Your Setup

Once everything is configured:

  1. Send a test email from your new address to a Gmail account
  2. Check email headers - Look for SPF=pass and DKIM=pass
  3. Use a testing tool like mail-tester.com to check your deliverability score

If SPF or DKIM fails, double-check your DNS records and wait for propagation (can take up to 48 hours, though usually much faster).

Accessing Your Email

You can access your Zoho Mail through:

  • Web: mail.zoho.com
  • Mobile: Zoho Mail app for iOS/Android
  • Desktop client: Configure IMAP/SMTP in your preferred email client

IMAP/SMTP Settings

If you want to use a desktop client like Apple Mail or Thunderbird, here are the settings. Note that IMAP/POP access requires a paid Zoho plan, so if you're optimizing for free, you'll want to stick with the web and mobile apps.

Incoming (IMAP):

  • Server: imap.zoho.com
  • Port: 993
  • Security: SSL

Outgoing (SMTP):

  • Server: smtp.zoho.com
  • Port: 465
  • Security: SSL

Wrapping Up

Setting up a custom email might seem intimidating at first, but Zoho's one-click verification makes it incredibly straightforward - you can have everything configured in minutes without touching DNS records manually. Even if you go the manual route, it's really just a matter of adding a few DNS records. With Zoho's free tier and Cloudflare's DNS management, you get a professional email setup without any monthly costs.

Now when I send emails from hello@giftegwuenu.com, they land in inboxes properly authenticated. That's a win!

Have you set up custom email for your domain? I'd love to hear what provider you chose and why.

Resources

February 01, 2026

January 26, 2026

January 24, 2026

How I Use Clawdbot

Kristian Freeman ·

[Clawdbot](https://clawdbot.com) lets me message an AI from Telegram and have it do stuff for me. Not "here's some information" stuff — actual stuff. Running shell commands, querying my finances, managing my task list, requesting movies for my media server. I've been running Clawdbot on my Mac mini since mid-January and it's become one of those tools I forget isn't normal. I named mine Roman. ![Roman introducing himself in Telegram](/images/roman-telegram.jpg) ## What Clawdbot is Clawdbot is a gateway between messaging apps and AI agents. You text it, it runs an agent with access to your systems — shell, files, browser, APIs, whatever you hook up. I use Telegram, but Clawdbot supports WhatsApp, Discord, iMessage, others. The Mac mini M4 runs 24/7 on my Tailscale network. I can message Clawdbot from my phone, my laptop, wherever. Same context, same capabilities. For models: MiniMax-M2.1 handles general chat (fast, cheap), but Clawdbot escalates to Claude Opus when I need code written or debugged. I'm on the Claude Max plan ($200/mo) which gives me heavy Opus usage without worrying about API costs. ## Clawdbot skills I use The magic is in "skills" — markdown files that teach Clawdbot how to use specific tools. Here's what I've got running: **Finances.** I do plain-text accounting with hledger. The skill knows my journal files and how to query them. "How much did I spend on food last month?" — it runs the right hledger command, gives me a number. ~7,800 transactions going back to mid-2023, all queryable via text message. **Linear.** My task management lives in Linear. "What's on my plate this week?" pulls my assigned issues sorted by priority. I can create tasks, update status, search across projects — all from Telegram. **NixOS NAS.** My home server runs NixOS. The skill knows the config structure and can SSH in. "Add a new Podman container for X" — it edits the Nix config, commits, runs `nixos-rebuild`. I've modified my server config from my phone while walking around. **Jellyseerr.** Media requests. "Add the new Lanthimos movie" — searches, finds it, submits the request. Shows up in my library once Radarr grabs it. Stupid simple. **X Bookmarks.** I bookmark way too much on Twitter. Health stuff, AI papers, programming tips. The skill has a DuckDB database with embeddings — 512+ bookmarks with vector search. "What did I bookmark about sleep optimization?" actually works. **Tweets.** Stores my past tweets (1200+), analyzes what performs well, hooks into Typefully for drafting. Syncs daily. **Skill Creator.** Meta, but useful. When I need a new Clawdbot integration, I describe what I want and it scaffolds the skill structure. Saves me from writing boilerplate every time I want to add something. ## Clawdbot automations **Daily briefing.** Every morning at 9am, Clawdbot sends me a Telegram message with today's calendar (pulled from macOS Calendar via icalBuddy), my open Linear tasks sorted by priority, and anything else that needs attention. Nice way to start the day without opening five apps. **Tweet sync.** Daily job pulls my latest tweets and appends them to an ndjson file. **Bookmark sync.** Hourly job fetches new X bookmarks, generates embeddings, updates the search index. ## Real examples from this week - "Sync my tweets" - "How much have I spent on rideshares this month?" - "What's the status of the Containers launch tasks?" - "Request Bugonia on Jellyseerr" - "Check my bookmarks for anything about sauna protocols" Responses come back like any other text. I'm on my phone, I ask a question, I get an answer. Sometimes that answer is "done, I pushed the changes to git." ## What makes Clawdbot useful **Memory.** There's a memory system where Clawdbot stores facts, preferences, decisions. It knows my account structures, project names, common queries. I don't re-explain context every time. **Action.** Clawdbot doesn't just answer questions. It runs commands, edits files, hits APIs, pushes to git. "Ship it" means it actually ships. **Composable.** Each skill is a markdown file. Want to add a new API? Write instructions in a markdown file. Want to share it? Copy the file. ## What I'm adding next - Email triage (summarize what needs attention, draft responses) — in progress - Content capture (tweet something good → auto-draft a blog post expansion) - Health logging (workouts, supplements, sleep scores via message) ## Clawdbot setup ```bash curl -fsSL https://clawd.bot/install.sh | bash clawdbot onboard --install-daemon ``` The wizard sets up auth, channels, and optionally installs Clawdbot as a background service. Then: ```bash clawdbot gateway status clawdbot status ``` Docs: [docs.clawd.bot](https://docs.clawd.bot) Source: [github.com/clawdbot/clawdbot](https://github.com/clawdbot/clawdbot) I'll update this as the setup evolves. The goal is an assistant that handles the boring operational stuff so I can focus on the interesting work. So far, it's working.

How I Use OpenClaw

Kristian Freeman ·

**Update (Jan 30, 2026):** Clawdbot has been renamed to **OpenClaw**. The project was previously known as Moltbot before that. All the functionality described below remains the same — just a new name. --- [OpenClaw](https://openclaw.ai) lets me message an AI from Telegram and have it do stuff for me. Not "here's some information" stuff — actual stuff. Running shell commands, querying my finances, managing my task list, requesting movies for my media server. I've been running OpenClaw on my Mac mini since mid-January and it's become one of those tools I forget isn't normal. I named mine Roman. ![Roman introducing himself in Telegram](/images/roman-telegram.jpg) ## What OpenClaw is OpenClaw is a gateway between messaging apps and AI agents. You text it, it runs an agent with access to your systems — shell, files, browser, APIs, whatever you hook up. I use Telegram, but OpenClaw supports WhatsApp, Discord, iMessage, others. The Mac mini M4 runs 24/7 on my Tailscale network. I can message OpenClaw from my phone, my laptop, wherever. Same context, same capabilities. For models: MiniMax-M2.1 handles general chat (fast, cheap), but OpenClaw escalates to Claude Opus when I need code written or debugged. I'm on the Claude Max plan ($200/mo) which gives me heavy Opus usage without worrying about API costs. ## OpenClaw skills I use The magic is in "skills" — markdown files that teach OpenClaw how to use specific tools. Here's what I've got running: **Finances.** I do plain-text accounting with hledger. The skill knows my journal files and how to query them. "How much did I spend on food last month?" — it runs the right hledger command, gives me a number. ~7,800 transactions going back to mid-2023, all queryable via text message. **Linear.** My task management lives in Linear. "What's on my plate this week?" pulls my assigned issues sorted by priority. I can create tasks, update status, search across projects — all from Telegram. **NixOS NAS.** My home server runs NixOS. The skill knows the config structure and can SSH in. "Add a new Podman container for X" — it edits the Nix config, commits, runs `nixos-rebuild`. I've modified my server config from my phone while walking around. **Jellyseerr.** Media requests. "Add the new Lanthimos movie" — searches, finds it, submits the request. Shows up in my library once Radarr grabs it. Stupid simple. **X Bookmarks.** I bookmark way too much on Twitter. Health stuff, AI papers, programming tips. The skill has a DuckDB database with embeddings — 512+ bookmarks with vector search. "What did I bookmark about sleep optimization?" actually works. **Tweets.** Stores my past tweets (1200+), analyzes what performs well, hooks into Typefully for drafting. Syncs daily. **Skill Creator.** Meta, but useful. When I need a new OpenClaw integration, I describe what I want and it scaffolds the skill structure. Saves me from writing boilerplate every time I want to add something. ## OpenClaw automations **Daily briefing.** Every morning at 9am, OpenClaw sends me a Telegram message with today's calendar (pulled from macOS Calendar via icalBuddy), my open Linear tasks sorted by priority, and anything else that needs attention. Nice way to start the day without opening five apps. **Tweet sync.** Daily job pulls my latest tweets and appends them to an ndjson file. **Bookmark sync.** Hourly job fetches new X bookmarks, generates embeddings, updates the search index. ## Real examples from this week - "Sync my tweets" - "How much have I spent on rideshares this month?" - "What's the status of the Containers launch tasks?" - "Request Bugonia on Jellyseerr" - "Check my bookmarks for anything about sauna protocols" Responses come back like any other text. I'm on my phone, I ask a question, I get an answer. Sometimes that answer is "done, I pushed the changes to git." ## What makes OpenClaw useful **Memory.** There's a memory system where OpenClaw stores facts, preferences, decisions. It knows my account structures, project names, common queries. I don't re-explain context every time. **Action.** OpenClaw doesn't just answer questions. It runs commands, edits files, hits APIs, pushes to git. "Ship it" means it actually ships. **Composable.** Each skill is a markdown file. Want to add a new API? Write instructions in a markdown file. Want to share it? Copy the file. ## What I'm adding next - Email triage (summarize what needs attention, draft responses) — in progress - Content capture (tweet something good → auto-draft a blog post expansion) - Health logging (workouts, supplements, sleep scores via message) ## OpenClaw setup ```bash curl -fsSL https://openclaw.ai/install.sh | bash openclaw onboard --install-daemon ``` The wizard sets up auth, channels, and optionally installs OpenClaw as a background service. Then: ```bash openclaw gateway status openclaw status ``` Docs: [docs.openclaw.ai](https://docs.openclaw.ai) Source: [github.com/openclawai/openclaw](https://github.com/openclawai/openclaw) I'll update this as the setup evolves. The goal is an assistant that handles the boring operational stuff so I can focus on the interesting work. So far, it's working.

January 20, 2026

Building an Instant Messenger

Brayden Wilmoth ·

I built an Instant Messenger application all on Cloudflare. It took 1 day, 3 files, 4 resources... and it's ready to scale from 0 to millions and this is how I did it.

January 18, 2026