• GoRead2 Turns One

    A little over a year ago, after having discovered Claude Code, I decided to resurrect GoRead, which was itself a resurrection of Google Reader. On July 26, 2025, I made the first commit. Here we are, one year and 838 commits later! I can still only count myself as the one paying customer, but I never intended this to be a money maker–I just wanted to cover the Google App Engine costs.

    Here’s one way to look at it:

    image github

    I created a web app first, which took most of last year to finally get into a state where I was happy with it and was using it exclusively in place of Reeder. Then there was a lull with just minor polish and tweaking. Finally, early this summer, I decided to make some larger changes including starting work on an iOS mobile app which is only just now starting to come together.

    What’s next? I’d like to have an Android app, too. And Claude keeps nagging me about stuff like GDPR compliance. So there is always something, and the hobby project continues.

    Happy first birthday and cheers to GoRead2! 🎂🎉

  • Internet History

    This hits home. My own history with computers and the Internet is just a couple of years older, so a lot of this is very relevant to me. What a walk down memory lane!

  • The Human Takes A Turn

    The last six posts were largely written by Claude and merely edited by me.

    This post, on the other hand, is all me. In the 6 months or so since we started this GoRead2 adventure, I’ve learned a lot about working with an AI partner.

    First, AI is a terrible writer. If you read all of that GoRead2 stuff, well, I’m sorry. AI tries too hard to sound human by using conjunctions and witticisms. It is overly fond of hyphenated sentences. You can tell when a paragraph was written with AI–just look for the mid-sentence hyphen or weirdly-ordered sentences with clauses separated by hyphens. For the GoRead2 posts, I had Claude generate the initial text (after all, it had done all of the hard work), but then I took about a week to edit each one. I had to put a time limit on the editing, or I would have rewritten everything.

    Second, keep everything on your system updated, because the AI ecosystem evolves fast. I run brew update and brew upgrade reflexively at this point, and I use a shell script to run a bunch of git pull commands every time I open a terminal. If you skip these sorts of tasks for a week, you will be hopelessly out of date, and you’ll have a bunch of extra work to do to catch up again.

    Third, but on a similar note, the capabilities of the AI tools increase almost constantly. It is hard to keep up, but the more of those capabilities you can leverage, the easier things will be. When I started the GoRead2 project, capabilities were limited to having Markdown files that told the AI what to do and what not to do. I know Beads is controversial but using it made sense for this project, and made tracking work across multiple development machines so much simpler. I probably didn’t make use of all of the Claude Code bells and whistles, and I am sure that cost me.

    Fourth, as we alluded to in Part 6 of the GoRead2 series, humans have domain knowledge; but at least as of this writing AI tools do not. I’ve been playing with software in some form or another for 40+ years, and in that time I think I’ve learned a thing or two about how things should be run. Claude does not have this experience and needs to be told that (for example) certain approaches to testing or deployment have inherent advantages. AI has come a long way in the last year, but we are not yet at autonomy. The humans still need to be in charge.

    Finally, I remember getting my Commodore 128 when I was 14 or 15 years old, and being so thrilled when I could enter 10 lines of BASIC code and get something back. I remember typing in entire programs from magazines, being disappointed when they did not run, spending hours debugging them, realizing the issue was because the magazine was incorrect, experimenting with and finding a fix, and then getting the program working. This process was a rush unlike anything else I’ve ever experienced…until recently. The process of working with an AI and getting it to spit out a working piece of software based on my description alone, tweaking the description and prompts to make the software better, and getting the result I wanted…this is why I love working with computers in the first place. There are many legitimate reasons to look at AI with wary eyes, but I cannot deny that the process of going from zero to working product has become ridiculously easy.

  • Goread2 - Chapter 6: The Long Tail

    This is the sixth (and mercifully, the last) in a series of posts wherein I attempt to recount the history of Goread2 as it approaches a state in which I might actually try to share it more broadly.

    I don’t think mature software announces itself with a precise moment of completion. It just quietly changes character: commits get smaller, features get more specific, problems get subtler. What was once “make it work at all” becomes “make it work better, cost less, and fail more gracefully.”

    Migration and Its Consequences

    January opened with a clean-looking commit: switch from App Engine Flexible to Standard. Flexible runs on always-on VMs. Standard can scale to zero when idle, so you pay nothing when the app is unused. The migration was a handful of lines in app.yaml.

    Costs tripled within days!

    Three causes had converged at once. The CPU utilization target had been silently dropped during the migration, making the autoscaler aggressive. Deleting expired sessions asynchronously, which we added that same week to avoid blocking requests, turned out to keep Standard instances alive; a running goroutine is not an idle instance. And three background cleanup tickers had always been running, harmless on Flexible (which never scaled to zero anyway) and fatal to the billing on Standard. The fix was moving cleanup to cron jobs and reverting the async session deletion, which then introduced a data race in the test suite since the mock had not been written for concurrent access. Sometimes, it just be that way.

    Steady Work

    Once the cost explosion was resolved, work settled into productive incrementalism. Security headers arrived in February: CSP, HSTS, X-Frame-Options, the full set, deployed in report-only mode first so that article images from arbitrary RSS sources would not immediately break. HTTP conditional requests followed: storing ETags and Last-Modified headers from feed servers, then sending If-None-Match on subsequent fetches. A 304 Not Modified response means no re-download, no re-parsing, no Datastore writes. Estimated bandwidth savings: roughly 90% for unchanged feeds.

    The most satisfying commits found structural inefficiencies hiding in plain sight. Three separate Datastore operations per feed per cron run (UpdateFeedTracking, UpdateFeedLastFetch, UpdateFeedCacheHeaders), each doing its own Get+Put round trip, were collapsed into a single UpdateFeedAfterRefresh. Per-article URL deduplication queries became one batch query per feed. We added a 90-day time window to that query since feeds do not recycle old URLs; scanning the full article history had always been waste. Each change touched both backends and required test updates, but each measurably reduced the monthly bill.

    The goroutine trap appeared one more time in March. The cache stats logger added in February was keeping instances alive. Out it went. By that point the pattern took about thirty seconds to recognize: check the uptime logs, find a suspiciously low scale-to-zero rate, follow the thread back to whatever was ticking in the background.

    The commits keep coming. The open issue count stays nonzero. The billing dashboard shows a number in the single digits most months. The app does what it was built to do.

    What Would We Do Differently?

    GoRead2’s arc traces a pattern that has less to do with AI and more to do with questions. When you are moving fast, the natural prompt is “make this work.” Those prompts get answered. What does not get answered automatically are follow-on questions: is this safe? What does this cost at scale? What keeps this instance alive at 3am?

    The TLS bypass existed because the prompt was “make HTTPS work on App Engine,” not “make HTTPS work securely.” The SSRF vulnerability existed because the prompt was “fetch this feed URL,” not “safely fetch a URL a potentially malicious user submitted.” The memory leaks existed because the prompt was “cache these reads,” not “cache these reads in a way that cleans up after itself.” The goroutine that prevented scale-to-zero existed because the prompt was “make this cleanup non-blocking,” not “make this cleanup non-blocking without keeping the instance alive.”

    None of this is a failure of the AI. It is a failure of the question.

    What worked better: the November code review, where the explicit goal was to find what could go wrong. That review produced nineteen actionable findings in a single session because the question was adversarial by design. Detailed issue specs with file locations, current behavior, and pseudocode for the fix produced better implementations than vague feature prompts. Measuring rather than assuming: “90%+ coverage” in the August commit versus the 9.7% that showed up when someone actually ran the tool in November.

    The quality of AI-assisted code is downstream of the quality of the questions being asked. You can build in eight months what used to take years. But speed compounds mistakes as efficiently as it compounds progress. Build “is this secure?”, “what does this cost?”, and “what breaks this?” into the prompting habit from the start rather than discovering the answers expensively later.

    tl;dr - ask better questions.

  • Goread2 - Chapter 5: Getting Serious About Process

    This is the fifth in a series of posts wherein I attempt to recount the history of Goread2 as it approaches a state in which I might actually try to share it more broadly.

    At some point in the development of a software project, work shifts from “building things” to “knowing things”1. Knowing whether the code works. Knowing whether the system is healthy. Knowing what still needs to be done.

    Testing Gap

    The multi-user transformation back in August had shipped with a commit message claiming “90%+ coverage.” By late November, with a proper coverage tool run against the handlers package, the actual number came in at 9.7%.

    This is not an unusual situation. Coverage numbers in commit messages are often aspirational, or measured against a subset of the code, or simply optimistic. What was unusual was the speed at which the gap was closed once it became a priority.

    November 28 and 29 were a testing blitz. A single commit on the 29th, Add comprehensive handler tests to increase coverage from 9.7% to 33.6%, ticked through the list of handlers that had never been tested: DeleteFeed went from 0% to 100%, MarkRead from 0% to 100%, ToggleStar from 0% to 100%, eight other handlers from zero to something meaningful. The commit message annotated each one individually, which has the quality of a student showing their work because it demonstrates something was actually thought about rather than just thrown at the wall.

    By the end of the weekend, handler coverage was 33.6%, database tests added, edge case tests for the services package increased coverage from 49.3% to 57.8%, integration tests added for the critical multi-user isolation workflows. Frontend test files that had quietly broken at some point were found and fixed.

    None of this testing revealed new bugs, which is its own kind of result. It meant the November hardening sprint actually fixed what it said it fixed, and the code’s behavior matched the intent. One could argue that is what a test suite is for: creating the conditions under which future bugs announce themselves immediately rather than silently.

    Fun With Monitoring

    Alongside the testing push, we needed proper visibility into what was happening in production. The plan was straightforward: set up Cloud Monitoring dashboards to track Datastore operations, App Engine instance hours, bandwidth, and request rates. Add alert policies so that cost spikes would trigger a notification rather than a surprise at the end of the month.

    On November 19, six alert policies and a ten-widget dashboard were committed. This was immediately followed, over the next three days, by a sequence of commits that tells a familiar story:

    • Fix: Add working App Engine dashboard, document Datastore metrics limitation.
    • Fix: Remove unavailable App Engine HTTP metrics from Cost Tracking dashboard.
    • Fix: Remove broken billing dashboard and clarify cost tracking options.
    • Fix: Create minimal dashboard with only working App Engine Standard metrics.

    It turns out that App Engine Standard, which the app had migrated to, does not expose many of the metrics that App Engine Flexible exposes — including request_count, which is a fairly foundational thing to want to graph. The operational dashboard we had planned ended up being replaced with a billing-focused one: daily cost trend, cost breakdown by service, instance count, month-to-date spend. Less “how is the app performing” and more “how much is the app costing,” which, given the events of Chapter 3, was arguably the more important dashboard to have anyway.

    The six alert policies — Datastore reads over 1,000/minute, writes over 500/minute, instance count over 5, egress over 100MB/minute, a 2x read spike detector, and a 5xx rate over 1% — survives intact and were deployed successfully. Whether App Engine Standard would actually fire them in the ways intended remained, per the documentation, “LIMITED.”

    Issue Tracker

    The third thread of this chapter is the one that is the most interesting to think about from a project-management perspective, and the most unusual: we adopted Beads as the project’s issue tracker.

    Beads describes itself as “AI-native issue tracking.” What this means in practice is that issues are stored as JSONL files directly in the repository. No GitHub Issues, no Jira, no Linear. The issue tracker lives in the git history alongside the code it tracks. Each issue is a JSON object with an ID, title, description, status, priority, and whatever other metadata is relevant. The whole database travels with the repository.

    The reasons for this design become clear when you look at how issues are actually used in this project. The November code review that opened nineteen issues at once (from Chapter 4) was done by reading code and writing structured issue descriptions with file locations, code snippets, and pseudocode for the fix already included. Those were not written for a human to eventually read and act on. Rather, they were written as machine-readable task specifications. When a developer agent (human or AI) picks up an issue, it has everything it needs: the location of the problem, the nature of the fix, sometimes example code. The issue IS the spec.

    This became explicit in January, when AGENTS.md was updated to document a full multi-agent orchestration pattern. A supervisor agent coordinates work by creating Beads issues as task specifications and never writing code directly. A developer agent receives a task via bd show ISSUE-ID, implements it, and pushes. A reviewer agent checks the implementation and either approves or requests changes. The whole workflow is mediated through the issue tracker, which is just files in the repo, accessible to any process that can read the filesystem.

    Whether or not you are running multiple AI agents, having the issue tracker in the repository has practical advantages: it is versionable, diffable, and never goes down. The prefix on issue IDs even changed during this period (from goread2- to the shorter gr-) and the migration was just a find-and-replace in JSONL files, committed to git, visible in the history like any other change.

    The three threads of this chapter are expressions of the same impulse: wanting to know, with confidence, what the state of the system is:

    • Tests tell you whether the code does what it is supposed to do.
    • Monitoring tells you whether the system is behaving the way you expect in production.
    • An issue tracker tells you what is known to be wrong and what has been decided to fix it.

    Together they create a feedback loop that turns “I think it works” into “I can verify that it works.”

    That loop takes time to build. It doesn’t produce features users can see. But without it, the code review in Chapter 4 would have been the end of a conversation rather than the beginning of one.

    Next: are we done yet?

    1. At least until the product managers show up. 

  • 1
  • 2