I recently noticed that my blog has fewer than 30 posts, but I've been maintaining it for almost a decade. I'm sure you can guess my next thought:

"This is because I don't have my own hyper-personalized version of Google Docs to use as an editor."[0]

So I set out to fix that. And I have a major advantage: from 2010-2015, I was a Google employee that worked on Google Docs. It's been a while, but I still remember a few things about what makes the ol' girl tick.

It took an 8-hour Q&A with Codex. It took four ChatGPT resets on my $200/mo plan to get it done with Astra on xhigh. But it's done. This is my first post produced entirely by my own custom Google Docs "clone". It powers my blog's editor. It has its own custom operational transform stack. It's slow as shit. I love it to death.

A screenshot of an earlier draft of this post, being edited within the new blog editor

Here is my own personal Google Docs, in all of its glory and horror, from an earlier draft of this post.

To be clear, you're not reading directly from the Google Docs editor's software. The public-facing site and the editor are decoupled. The public site is just static content. The editor accumulates changes and snapshots while I work, tracking everything in a Postgres database. A job is started each time I publish, republish, unpublish, or delete. That job regenerates the static site. That is what you are reading now.

The Q&A from hell

I planned this with GPT 5.6 Sol on xhigh. Astra came out right when I was finishing up the Q&A. Otherwise I would have used it for the Q&A too.

I started the same way everyone starts a big task: I wrote out a huge prompt and hit send.

But I also had one more request: Codex was not allowed to leave any inconsistency or ambiguity in the spec. It had to keep asking me questions until the spec was fully resolved, starting with the most important unanswered question.

It mulled over my initial prompt and cached about 380 questions in a Markdown file. Many of my answers induced Codex to ask several follow-up questions. I didn't do it in a single block. This is just a side project and I have a family and a job. So I did the interview over 4 or 5 days.

Some of these questions were grueling and required long explanations of major pieces of architecture that I wanted. Some required a lot of thought. Some of them were crucial for correctness. Some of them were implementation details that I happen to know about implementing word processors. I settled on a flow that let me gallop through questions pretty quickly.

  • I used voice input and told it "I'm using voice input which might transcribe incorrectly; work harder to try to guess what I originally said."

  • I asked it to ask between 1 and 3 questions at the same time, but it could only ask multiple questions if they were related.

  • And if it could think of an obvious default, it would explain that option and then ask, "agree?" So if all three were okay I could say "agree to all." Usually I needed a hybrid approach like "one: agree. two: do this instead. three: yes but with a small modification."

By the time I got to the last 50 or so questions, I was basically just saying "Agree. Agree. Agree." to each triplet because all of the decisions were inconsequential.

What I asked for

I wanted the static site to have feature parity with my existing blog. For historical context, my blog was WordPress for a long time. In February, I converted it to a Golang server that I host on DigitalOcean. The existing blog needed only very basic features for full parity: headings, text styles, links, lists, images, inline code, block code, quotes, footnotes, embedded YouTube content. Nothing too crazy.

I also wanted my favorite feature from Google Docs: magic redo.

"What is magic redo?"

I'm glad you asked! Go to doc.new and copy/paste this post into the editor. Select some text and bold it. Now scroll down and select some other text (without doing anything else in the meantime!) and type the Redo keyboard shortcut (of undo/redo fame). The text in the new location is now bold. Select some more text and type redo. That text is also now bold. And that is magic redo! It executes the last action as if it was done with the current selection model. It's useful when you want to do the same thing a lot, like adding a few columns to a table. Magic redo has some implications on the backend design, because it means that everything the editor does needs to act on f(selection).

Where I compromised

The biggest compromise was in the view layer. When I worked on Google Docs, it did a lot of clever things to try to make the editor as efficient as possible. They also measured every change to guard against regressions. After I left, they later switched rendering to canvas, which I understand was a huge performance win.

I did not do any of this. I just have it update the DOM node of the paragraph it's editing. I'm not doing any clever tricks. I don't have performance guardrails yet.

I also probably could have just pointed an LLM at its obfuscated source and said, "reverse engineer every feature this editor has" and wound up with a fairly complete feature list. But then it would have generated an editor that did not suit the needs of my blog.

I don't have any accessibility or right-to-left text support. If I ever go blind or move to Jordan I'll yell at Claudex until it makes it happen.

I don't have offline support. No suggested changes.

When I joined Google, Docs was missing what I now consider an essential OT command: "move text". A lot of operations in Docs were implemented with "delete the text here and re-add it here," which could transform poorly against incoming commands and blow them away (since they'd get applied to the range that got deleted). I am not using it at the moment, but I have plans for an "outline" feature that allows me to reorder lists very quickly, and I plan to implement that with move.

"But Jake, you're the only editor. Why do you need to care about how stuff OTs?" Buddy, I implemented my own Google Docs instead of editing a Markdown file. We're beyond logic here.

I've also been experimenting with a "proof of human" feature, which is effectively a read-only Etherpad that I'd link from my posts. Since I have the full command history, every essay could have a complete playback to show that I wrote it. I don't feel completely comfortable launching it, because my writing is VERY exploratory. I write a lot of things that I don't mean which I later delete. I don't want people to be able to screenshot it out of context.

The implementation

I implemented the project using GPT-6 Astra on xhigh.

I have a $200/mo ChatGPT account for my side project coding, and needed four resets to implement it. But I had one advantage: the week I implemented it was an incredible week for resets. So huge shoutout to Tibo. I started right after my weekly quota refresh and already had three banked resets available for use. And there was one more unbanked reset. The initial implementation used 4 full weekly quotas and almost finished the fifth before it finally finished.

I made some basic mistakes in prompting that came back to bite me. A lot of time was wasted on testing and verification. I don't remember exactly how I prompted the model since I did the Q&A a month ago. But I'm sure that I said that it needed to verify everything, that it needed to test all cases, and stuff like that. I think I also suggested that it try fuzz testing on the editor to test out the OT stack.

I made the mistake of acting like I was describing a design to a senior engineer. But no, Astra is not human and does not have a shared human understanding of what I mean when I speak inexactly. So it handled these in the most robotic way possible. "Fuzz testing" turned into a series of 10,000+ case integration tests that needed over five hours to run each. Each day I'd tell it to delete a massive test and replace it with the minimum number of test cases to achieve equivalent case coverage, and each night it would birth another massive 10,000 case integration test that took 5 hours to run. I'm sure this burned a massive number of tokens. Eventually I banned it from generating tests with more than a few hundred cases. It still mentions this fact in completely unrelated situations. This always makes me wonder, "..... but were you actually thinking you might do that?"

Telling it to "verify" things was also a mistake. The app was unusably slow at first. It added runtime checks that choked the app to death.

For background, when you have a full operational transform stack, many features need to be transformed against incoming changes. If you have an undo and redo stack, everything in the undo and redo stack needs to be modified so that they can still apply cleanly. If a command bolds the letter 'b' and a letter is added before it, the indexes referenced in the command need to shift forward so that 'b' is still bolded if it gets applied.

In this case, I requested that grapheme clusters always had to be valid when edits were applied. To me, this means "after application of the edit but before sending the commands to the server, check the surrounding model context until a safe boundary to ensure that the operation did not result in an invalid character sequence." But the generated app would "verify" the undo and redo stack on every single character press by making a copy of the document, applying the edit, and then running a full-document grapheme cluster check. So typing a few characters into the editor was enough for it to stall and stutter.

I've done a few rounds of performance work at various granularities, like "walk me through everything that happens when I press a key" and "optimize typing 1000 characters", and I think I've eradicated most of the unnecessary work. Typing is relatively smooth, which is nice given that I did not optimize the design for performance.

The cutover

Since the project involved migrating from the static site to the hybrid site (dynamic backend, static frontend), the cutover became an Epoch event of religious significance to Astra. Astra was obsessed with the cutover. My Makefile and Ansible implementations became littered with a ton of extra commands and checks. Not only support for the old version and the new version, but tons of verification checks to ensure complete site sanity at every intermediate step of the cutover. And Astra needed Verification, which it got by hashing lots of different things and calling it Proof. So the entire thing was Verified with Proof, which was very comforting.

Since my site was approximately 30 Markdown files that were backed up on GitHub, the stakes could not be lower. I decided to just try Astra's cutover. If it failed I was just going to recreate the droplet and have it start fresh. But it did a fine job. Migration successful. The site works great.

But then the cutover continued to exist. Every change needed to be tested for compatibility with the pre-cutover and post-cutover and intermediate steps. It still asked me about how I'd like to handle the legacy Markdown site when describing new features.

So obviously I eradicated the cutover. I tasked Sol with the cleanup. It deleted the old Markdown static site as well as the Go code for serving it. It also eliminated all of the LLMslop guardrails: the cutover mentions, verification checks, all Proof and Evidence of Verification. All of it gone. And as a bonus, I haven't seen it mention the cutover in a while. Codex has truly forgotten about the cutover.

How do I edit it now that it's live?

First, I have a bunch of skills that manage each stage of the process. File a ticket (and give me a Q&A over any ambiguities), start a demo server, clean up and submit, etc.

I have a vibecoded workspace switcher, lovingly called ngmi, which manages all of my agents and how they interact with the project lifecycle. When it picks up a ticket, it automatically creates an agent, blocks on unmerged dependencies, has a merge queue to serialize the submission of finished tickets, etc. It runs on a separate DigitalOcean droplet, and can handle 3 or 4 agents running at once without struggling too much. This would be too limiting for my day job, but is fine for a side project. I run it on a DigitalOcean droplet since I work from both my Windows gaming PC and my MacBook M1.

ngmi can spawn its own non-task agents and has its own MCP, so I can also command it to do things from within the agents managed within ngmi.

Since I love taking my side projects too far, I have my own ticket tracking program with its own MCP at bitlogwork.com. Whenever I want to change something, I just file a ticket and ngmi automatically picks it up. It detects what part of the stack I'm working on (ticket tracking, analytics, the blog) and starts a demo server when it completes the task. It can generate and serve explainer HTML pages to highlight different aspects of the change that I'm curious about. I can switch to the agent and ask for changes.

I've been doing performance work and adding features over the past month, as I have energy to do side project work. It's finally in a position where it's not so painful to write a post, so here it is! The inaugural blog post coming from my own personal Google Docs clone.

Lesson: reality has an infinite amount of detail

One-shotting is for losers. Find ways to uncover all of the hidden spec that matters without getting bogged down in tangents that don't matter.

If you tell an LLM "Make a modal dialog with options OK/Cancel that asks if you're sure you want to delete this before executing the delete action," you may think that you very exactly specified what it does. You can probably already picture what it looks like. But the LLM will guess an insane amount of spec behind the scenes. Where is it positioned? Do the corners have border-radius? Does it have a border? Does it have a drop shadow? What is the padding? Does it need a separate title or does the raw question text suffice? What order are the buttons? What color are the buttons? What is the exact copy? It just goes on and on. There is no limit to how detailed the spec can be.

But when you're building something and you care about the output, there are an infinite number of possible details that matter. There are a lot of types of implementation where you can just shut off your brain and allow the LLM to pick something for you. But if you really want to produce a great product, you still need to have vision and taste and take control of the output. Take it from me, the guy who does not have vision or taste but really wanted to take control of the output.

Lesson: software isn't a moat

Any of your old employees can recreate large swaths of your product in a few days of motivated work (or a month or two if they're kinda just winging it on the side). It's all about ecosystem, partnerships, and all of those other intangibles that actually require hitting the pavement, working the phones, hunting through your network, and doing all of the other grueling work of building up your place in the world brick by brick.

Footnotes

[0]

You might say, "But Jake, you do have your own copy of Google Docs. Google Docs is free. You could simply use it and copy-paste the content." But I turn that back around to you, and ask, "Is it really mine? Do I have the freedom to make the real Google Docs this underfeatured and unperformant?"