Two Agents, One Gateway
My wife and friends could not test my experiments without installing an app. Five days, two coding agents, and one gateway later, they could. The build, told from the friction forward.
Sekou M. Doumbouya
Listen to this article
The idea did not start with a log line. It started with my wife wanting to try something I built, and an app install standing in the way. My internal services ran behind Tailscale, which is excellent for power users and a wall for everyone else. Every experiment I wanted my wife or friends to test began with the same tax: install the app, join the network, keep it running. Most of them stopped at the install. The tax sounds small until you watch someone pay it: install the app, create an account, join the network, hope the operating system cooperates with the VPN permissions. By the time a friend reached the actual experiment, the experiment had to be worth a setup guide.
The views expressed here are my own and do not represent those of any current or former employer.
So the question I carried into September was not “how do I build a gateway.” It was “how do I let people use my services without asking them to install anything.” I worked through it with ChatGPT first, as a thinking partner, and the pieces arranged themselves: verify who a person is, add a second factor, mint them a key I actually trust through passkey. When I said the components out loud, the click was immediate. These are all the parts I need, and Tailscale can come out of their experience entirely.
The wall was an app install
Tailscale stayed in my own workflow. What had to go was Tailscale as a requirement for the people I wanted feedback from. That distinction shaped everything after it: the replacement had to be invisible to a non-power user, which meant the trust work had to happen on my side of the wall. Authenticate them, mint the key, stand up the door. They walk through; they never see the hinges.
The question opened the build
I opened a Codex session at 6:50 in the morning with: “What would it take to use MTLS for communication between api.sekoudoumbouya.com and clients.” The first answer was honest: Caddy plus Cloudflare Tunnel would do most of what we wanted. Most was doing a lot of work in that sentence. As the sessions progressed, the design walked toward components I owned outright: Authelia for the auth layer, self-hosted services for the rest, and Cloudflare phased out of the core entirely. The managed tunnel that carried the first version became the thing the final version no longer needed. The morning sessions set the experience bar by naming another product’s dashboard as the target and instructing token efficiency, because the work was expected to run long.
Four messages in, I typed a forwarder that carries its own client certificate, called it useful, and added a parenthetical: it needs a better name. Sixteen minutes into the session, the reply came back with the name and the architecture in the same turn.
Verbatim from the session log, in order, typos included. The session ran 52 operator turns; these five messages named a product.
-
What would it take to use MTLS for communication between api.sekoudoumbouya.com and clients
-
~/code/ai-infra has more info on this setup
-
I wonder if I can bundle the client within a sdk for accessing the router
-
Or even better, I could create forwarder and has client cert embedded and I can provide a key to forwarder (need a better name). This would be useful
-
Yes. I'd call it 'Anvil Connect': a small client-side agent that gives existing apps a familiar API endpoint while handling mTLS for them.
I did not open that session to design a product. I opened it with a precise question, pivoted twice in four messages, and the parenthetical did the rest. Once the reply named Anvil Connect, the build had a boundary, and everything proposed after that was either inside it or outside it.
The crew built it; the tech lead checked it
The parent thread spawned specialist subagents with proper names and explicit roles: transport review, session handling, the browser, client portability. One of them, an adversarial reviewer, got spawned three separate times for deployment review, which tells you how much of the build was argument rather than typing. For the unfamiliar: subagents are fresh-context specialists that take one task and report back, and background tasks are long-running jobs like test suites or consults that notify on completion instead of blocking the session.
Behind the crew sat a second layer: the consult. When a production bug needed a second opinion, my pi agent wrote a consult file that opens “You are being consulted as the lead development agent on a confirmed production bug and a proposed fix,” then ran it past Astra in a read-only sandbox and incorporated the changes before anything merged. I phone home to Astra like a big brother who happens to be the tech lead: sandboxed, read-only, and incorporated before I accept the design.
The second agent, pi, joined mid-build and ran on my own rack: provider anvil, model llm.primary, GLM-5.3 Flash, with a recorded cost of zero. Game changer is the only honest phrase for what that did to my workflow. Building on the model and promoting the model became the same activity, and the hands-on agent with the home-field model was the right typist for the days the edge needed publishing. Mid-build, pi turned the product on itself: I asked to install pi-web and expose it over Anvil Connect, and the harness ended up published through the gateway it was helping build. By the end of the second day, three releases had shipped, and the evidence docs landed in the same commits as the binaries.
The pause that shook the bet
The bet underneath all of it was simple: self-hosted could be reliable enough to replace the managed thing. If it could not, the whole reason for leaving Tailscale collapses, because the managed tunnel was never unreliable. On the fourth day a router pause surfaced as a confirmed production bug, and for a moment the bet wobbled exactly where it was weakest.
The recovery is the part I would repeat. The agent that would fix it was the same agent class that had written the bug, so nobody trusted that alone. The fix, h2 connection reuse, went into a consult file, through the read-only sandbox, past Astra, and came back with changes that were incorporated before the design was accepted. Reviewer subagents argued about it in a worktree. The pause got fixed, and the bet held, but it held because the second opinion was structural, not because the first answer was confident. The loop is the transferable part, so here it is as it actually ran.
How a fix got trusted in this build. Step through it; step three is the one worth copying.
The router pause, day four. Not a hunch: confirmed, with a proposed fix attached.
The hands-on agent writes a consult file so the second opinion starts from evidence, not memory.
You are being consulted as the lead development agent on a confirmed production bug and a proposed fix
The file runs past the tech-lead agent in a sandboxed, read-only process. It can read everything and change nothing.
The review comes back with changes, and they land before the design is accepted, not after.
Adversarial reviewer subagents take the fix apart in an isolated worktree before anything merges.
Steering was short sentences
Near midnight that day, the onboarding flow forced a fork: carry email addresses, or passkeys one hundred percent of the time. The debate resolved inside the upstream project’s policies, and both sides shipped: email deployment and passkey sign-in went live the next morning. The log keeps the sentences that did the bending, and none of them is longer than a tweet.
One onboarding session, about ten and a half hours, five sentences that bent the build. Verbatim from the log, typos included.
- 23:56 the fork
Hmm. Well, I guess one question is, should we even have the email in the flow? Why don't we just use passkey
- 00:12 the classification
we need a backup process and since the user data is technically like personal identifying information, we should probably classify it a little bit differently
- 02:33 the fallback
tailscale is backup when anvil-connect does not work
- 10:20 the friction
ok, so that login the passkey part is pretty painful
- 10:21 the decision
lets enable the expertmental passkey only login
Watched them get in
A few days after the last release, I handed my wife a link. No app, no extension, no network to join. She authenticated, the key I minted worked, and she was using the service. I watched it happen. Nothing broke. The difference between the two halves of the story is one row of steps.
What a visitor did before the build, and what they do now. The key was minted on my side before they arrived.
- Install the app
- Create an account
- Join the network
- Approve VPN permissions
- Maybe reach the service
- Open a link
- Authenticate with a passkey
- Use the service
That was the whole bet, settled in one unremarkable moment. The wall I started with, the install that stopped people from testing my experiments, was gone, and nobody had to care about the machinery that replaced it.
Questions over code
The surprise of the five days was how little of the work was writing code. The parenthetical did more than any command. The components came from saying the problem out loud; the name came from a precise question; the pause got fixed by asking for a second opinion before trusting a confident answer. Two agents did the typing, and one gateway came out the other side, but the tool that mattered most was the question I could ask precisely. Find the wall your people actually hit. Ask what it would take to remove it. Keep asking until someone walks through.
Co-authored with AI, based on the author's working sessions, dictations, and notes.
Explore the source
fakoli/anvil-serving
This article discusses an open-source project. Star it, fork it, or open an issue.