In my experience, vibe-coding a prototype gets you a working demo in days, or even hours with a well-structured prompt. It covers enough of what you personally need to feel close to done, and it is about a fifth of the total work. Going from something that runs for me to something strangers can use took six months and four times the code: testing, guardrails, monitoring, cost control, privacy enforcement, a multi-user rewrite, and the structured workflow that lets AI keep building at full speed without breaking what already shipped. AI is the builder, but a lazy one prone to shortcuts and a wandering mind. The person driving it is the architect, and the architect's real output is the machinery around the code.

A check-in, start to finish
Haunts Nearby screen listing places to check into, one private place name blurred 01Nearby
Haunts check-in screen with sticker, visibility, and photo options 02Check in
Haunts check-in confirmation screen showing XP earned and keeper bonus 03Confirmed

From Check-Ins to Stories

"Check-ins are the breadcrumbs that become stories" has been the Haunts tagline from the start. Six months of work have turned that line from an intent into a description of the actual product. Check-ins captured on a road trip become map-matched routes. Routes become rendered trip videos with music and cold-open titles. Days become journals written by AI against the evidence of where you went, and trips become printable posters and share cards you can hand to someone.

A user on r/hauntsio described what that feels like from the other side, and their own words carry it better than any summary could:

A Haunts user on r/hauntsio

"My memory absolutely sucks, that's why I enjoy using Swarm, Facebook, Google Maps and now Haunts.

Two years ago, for example, I went to Legoland in Billund, Denmark. One of my favorite trips ever, and I was fascinated to read the recap of my trip from Haunts because it mentioned an aquarium. For the life of me, I couldn't remember until it finally clicked, Legoland's aquarium! And boom, literally memories unlocked.

To be able to read and revisit these memories is amazing for me and this helps me more than I could imagine. There [are] 12 years of stories, that I surely have forgotten a lot, waiting for me to remember them.

Thank you really."

A real afternoon from a favorite trip, lost for two years, returned by a story written from their own check-ins. Twelve years of those afternoons are waiting behind it, and the stories can return them one at a time.

Then and Now

The original case study published its own numbers:

At launch Today Change
Lines of code 250K+ ~1.02M 4.1×
API endpoints 120+ 194 +62%
Database migrations 110 419 3.8×
Background jobs 30+ 120 4.0×
Repositories 1 5 5×
Client surfaces Web, iOS, Android, Watch, Wear OS + CarPlay, Android Auto, widgets +3

The line count is source files only, with vendored libraries, lock files, documentation, and images left out, which is a stricter count than this post first ran with. Of the 1,017,000 lines, 652,000 are product code and 365,000 are tests, so roughly one line of test for every two lines of app, with the backend and the media server running well above that average. The web/api repository alone holds close to 630,000 lines: 437,000 of PHP, 167,000 of TypeScript for the web app and admin console, and 18,000 of SQL. The four repositories that did not exist six months ago tell the growth story on their own. A dedicated media server, 48,000 lines of Go built in seven weeks with more test code than source, hardware-encodes every uploaded video into adaptive HLS and serves it through a Cloudflare R2 edge tier with signed playback URLs. A native Android admin console, built in four days, carries moderation, analytics, live config editing, and system alerts to a phone. A documentation repository holds the contracts between all of them. And a 7.5 GB Google Takeout archive sits on disk as the test corpus for the Timeline importer, because you cannot write a real importer against a spec.

The Prototype Was the Foundation We Built Upon

Going public required one structural rewrite: the original backend assumed a handful of cohorts, and on the day scalable multi-user support landed, the commit history restarted with it, because social graphs, privacy tiers, and moderation are not features you bolt onto a single-user schema.

The native Android admin console went from an empty repository to a finished, sideloaded app in four days, at commercial quality, with fixture tests against real API responses from day one, which is what the fast part looks like once the machinery already exists. The prototype speed never went away. It compounds, because every guardrail built for the last project is already standing when the next one starts.

Six Months, Start to Finish
Mar
Five-day prototype
250K lines, one repo
Spring
Going public
Multi-user rewrite, history restarts
Jul
Security becomes a program
Five audit waves
Summer
Dedicated media server
48K lines of Go in seven weeks
Aug 14
Mobile audit
Twelve auditors in parallel
Aug 22
My Day board goes live
Ideas pipeline with gates
Today
Public app, six surfaces
~1.02M lines, 14,000+ tests

The Other 80% - The Real Work

Testing

The prototype had close to no tests at the start. The platform now has more than 14,000 automated test cases, and about two thousand of them were written in the first two weeks of September alone. In the backend, 46% of all PHP is test code. In the Go media server the tests outweigh the source, 25,900 lines against 22,100. Among them are more than 150 named regression guard tests, each one pinning a specific class of bug that was found and fixed, so it cannot return quietly.

Unit tests with mocked databases carried the project for months, then stopped being enough. August brought a seeded suite that runs against a real local MySQL with per-branch schemas. The seeded suite runs the same logic against a real MySQL instance, so it catches the cases a mocked database only assumed and never ran.

An independent maintainability review, run read-only and cross-checked by a second AI vendor, scored the mobile codebase 90 out of 100, an A. That score measures how the code is built to change, which is the first thing a fast build is supposed to cost you.

Where the Lines Go
Whole platform 1.02M lines
652K app
365K tests
Backend (PHP) 437K lines · 46% tests
236K app
201K tests
Media server (Go) 48K lines · tests win
22.1K src
25.9K tests
App / source code Test code

Across the platform, one line of tests for every two of app. In the media server, the tests outweigh the source outright.

Guardrails

A commit is blocked unless a code review has passed. A commit message starting with fix: is blocked unless a formal bug hunt has completed first, which requires three written hypotheses and a failing test that reproduces the bug before any fix is written. There are 109 of those hunts on file. A pre-push hook runs the typecheck before any version bump leaves the machine, because a red build wastes a 25-minute release cycle. A deploy guard refuses to ship from any checkout that is dirty or has drifted from what production carries.

Every one of those gates exists because AI writes code fast enough to outrun human review.

I've often spoken to others about the velocity of AI development and the challenges it puts on our human brains. It's not possible for us to keep up with this kind of development pace without taking the time to understand what it's doing. And, even then, when we are switching from one project to another, there is that lag of realigning our thoughts. I can't emphasize enough how important a structured workflow is to try to keep up, and not burn out.

The Architect's Job

Planning goes to the strongest reasoning model available, and implementation to something faster and cheaper. Final review goes to a different model from a different vendor, because a reviewer shouldn't share the author's blind spots. I've built 58 custom skills and slash commands and 55 automated hooks around that routine, covering everything from simulator QA journeys to deploy checklists.

Here's that routine end to end.

The Runbook
Fable
Brainstorm
Shape the idea and the constraints before any code exists
Fable
Ultraplan
Fan the problem out across agents, then synthesize
↻ Repeat until they agree
Fable
Implementation plan
The build order, file by file
Codex
Adversarial review
Attack the plan and find what it got wrong
Opus 4.8
Implementation
Write the code against the agreed plan
Codex Astra
Code review
Language idioms, type correctness, bug patterns
Fable
Whole-branch review
Architecture and API contracts across the full diff
Sim QA
Simulator journeys
Drive the app as a user before anything ships

A repository that contains no code at all holds the architecture contracts between the five repos and an append-only status log for each surface, and in its first six weeks it accumulated 582 commits, more than any other satellite repository in the system. Multiple AI sessions work concurrently across the codebase, and the only reason that works is that writing down what happened is treated as part of the work itself.

Privacy and Cost Controls

A full-codebase audit in early July surfaced real problems, all fixed before any user was affected, the default visibility for new content moved from public to friends, and a privacy probe now runs against production after every deploy. Privacy turned into a hard rule that summer.

Every external vendor now has a quota bucket, spend rolls up daily, and an advisor job files a month-end report on where the API dollars went, because a commercial product cannot run on someone else's free tier. Billing itself is wired end to end, webhooks, reconciliation jobs and all, and sits dormant behind a flag until the moment is right to start charging.

Ideas to Execution

The runbook above is how one feature gets built. By late August the harder problem was volume. Ideas were coming in faster than I could shape them, two different AI vendors were doing the work, and there were usually three or four sessions running at the same time. Without some structure, the backlog turns into whatever I mentioned most recently. So the structure got built, and it's enforced by tools rather than by my memory.

Three Vendors, One Set of Rules

I use Claude and Codex side by side in the same five repositories, with Gemini as a third that only ever reviews. The rules for how work gets done are written once, and the two that write code read the same file. One copy each of the skills that execute a plan, ship, and build an idea, shared by Claude and Codex. A plan that Codex builds follows the same model choices, reviews, and stop rules as one that Claude builds.

When Claude reviews a plan, it hands the plan to Codex for a second read, with no ability to change anything. When Codex reviews, it does the same in reverse, calling Claude with read-only tools. Gemini is the adversarial third read over both of them, taking the plans and diffs from Claude and from Codex and hunting for what each one missed, and it only ever reads. Models from three different companies fail in different ways, so one catches what another would wave through.

Who Reviews Whom
Claude
Writes code, and reviews the plans and diffs the others wrote
Build + review
Codex
Writes code, and reviews the plans and diffs the others wrote
Build + review
Gemini
Reviews both Claude's and Codex's plans and diffs, an independent third read
Read-only

Every plan and every diff is read by at least one vendor that didn't write it.

Both toolchains carry the same blocklist naming one model that is never to be used, after it burned us on this codebase. On September 10 I moved implementation from Sonnet 5 to Opus 4.8, because reviews kept catching the same kind of small logic mistakes and fixing them cost more than the cheaper model saved. The runbook above reflects that. What matters is that the process didn't have to change when the model did.

The last piece is shared memory. AI sessions don't remember yesterday, and two of them running at once don't know about each other. So every session writes what it did to a status log in the documentation repository, which now holds 399 entries, 391 of them tagged by vendor: 292 from Claude and 99 from Codex. The rules for writing to it fit on an index card. Re-read the file right before you write. Add to the end, never edit what's there. Commit the second you're done. Never touch text another session left behind. Uncommitted text is the only thing one session can wipe out in another, so the rules make sure it only exists for a few seconds.

My Day: A Board That Enforces Its Own Rules

My Day is the tool that sits between an idea and a branch of code. It's three pieces: a Mac app, a small server on a Raspberry Pi, and a phone app. The Mac app watches 31 repositories across seven projects and tells me which ones need a human, whether that's something to push, something to commit, something to merge, or something blocked. The phone can capture an idea from anywhere and read the board, but it can't move a card. The piece that matters here is the Ideas board. Think of a JIRA board where every column move runs a skill, and every skill has a gate.

The Board
Anyone
Inbox
Captured word for word from the desktop, the phone, or a session. Capture isn't allowed to plan or build
Claude / Codex
Shaped
A short brief with kill criteria, an effort rating, and rough tasks, written after reading the actual code
Claude / Codex
Planned
A step-by-step plan committed into the target repository
Both vendors
Reviewed
The other vendor tries to poke holes in the plan. Capped at ten waiting, because my reading is the bottleneck
Michael
Queued
The go decision. Small ideas can skip plan and review through an express lane
Claude / Codex
Building
Runs on its own, in its own copy of the code, with four written reasons to stop
Claude / Codex
Built
Reviewed, tested, committed, pushed, and written up as a GitHub issue. Never deployed
Michael
Shipped
Deployed or accepted by a human, issue closed. A killed state is reachable from anywhere and records why

Behind the board is a single JSON file, and exactly one piece of code is allowed to write to it. That code locks the file, re-reads it, bumps a version number, writes, and commits, because several sessions run at once and any of them might reach for the board at the same moment. There's no way to just set an idea's stage. The only path is the move command, and when it refuses, it tells you what stage the idea is in and what would advance it. A stage has to be earned. An idea can't become shaped without a brief, planned without a plan file, reviewed without a verdict, or built without a GitHub issue, so the board can't show progress that didn't happen. The moves only I can make are queue, ship, and kill, plus three that need a written reason: sending a plan back, resetting a build that died, and reopening something that was killed.

The board went live on August 22. In the 23 days since, 74 ideas have gone through it, 62 of them for Haunts. Thirty-eight are shipped, ten were killed with the reason on record, and seven are built and waiting on the next app store build. The median idea takes twelve and a half hours from inbox to shipped. Across 45 build runs, the median time from queued to a pushed branch is thirty minutes, and the longest was just under three and a half hours. Twenty-five of the ideas that made it to built or shipped were rated high effort. This isn't a board for typo fixes.

The Board in 23 Days
74 ideas through the board 62 for Haunts
38 shipped
7
19
10
Shipped · 38 Built, awaiting store build · 7 Still in flight · 19 Killed, reason on record · 10
12.5h
Median idea, inbox to shipped
30min
Median build, queued to pushed
25
High-effort ideas built or shipped

Unattended Work Has Written Stops

A build runs while I'm doing something else. That's only safe because the reasons it has to stop are written down as tests, so halting never rests on the AI's judgment. There are four. Any production action beyond the routine ones. Any file the plan didn't name. A test suite that was already failing when the work started, or a serious finding it can't fix inside the plan. And any database change the plan didn't expect. When one of those hits, the session writes why on the GitHub issue, puts the idea back in the queue, and stops. A build also never deploys. It ends at push, every time.

And every session, watched or not, ends by filing a GitHub issue that records what was asked, what changed, and where the files are. There are 296 of those across the five repositories, the paper trail for six months of work done by agents that don't remember yesterday.

One Afternoon on the Board

One Idea, Start to Push
17:03
Inbox
Fix the findings from a fresh mobile-app audit
17:24
Shaped
Codex reads the code and writes the brief
17:51
Planned
Twelve tasks, each testable, across 123 places in the app
18:25
Reviewed twice
Codex and Claude, each reading the source. Fourteen findings folded in, one dropped
18:29
Queued
Michael gives the go
↻ Two stops, scope widened twice
18:32
Build starts
In its own copy of the code
~18:47
Stops on an unnamed file
The right fix needs a file the plan didn't name. Scope widened, and it stops again
20:04
Pushed, every check green
2,898 node · 776 Jest · 482 component · typecheck · lint. Still not deployed

September 14 shows the whole thing in one idea. At 17:03 a card landed in the inbox: fix the findings from a fresh audit of the mobile app. Codex shaped it by 17:24 and had a plan by 17:51, twelve tasks, each testable on its own, with a list of the 123 places in the app the fixes would touch. By 18:25 the plan had been reviewed twice, once by Codex and once by Claude, each reading the source on its own. Fourteen real findings were folded in and one worry about a deadlock was dropped because it didn't hold up. I queued it at 18:29. The build started at 18:32 in its own copy of the code and stopped fifteen minutes later, because the right fix needed a file the plan hadn't named. It wrote that on the issue and handed the card back to me. I widened the scope, it hit the same kind of stop on a different file, I widened it again, and at 20:04 the branch was pushed with every check green: 2,898 node tests, 776 Jest tests, 482 mounted-component tests, typecheck, and lint.

Three hours from idea to a reviewed, tested, pushed branch, with two decisions from me in the middle. It still isn't deployed, because deploying isn't something the pipeline is allowed to do.

Testing the App the Way a User Does

The camera bug that shipped in 3.2.0 lived in the gap between passing tests and a tap that actually lands: unit tests prove that functions return the right values, but they don't prove the check-in sheet opened or that a photo made it through the filters. So the last gate before a build goes to the app stores is an AI agent driving the real app in the iOS Simulator, the way a user would.

Six Journeys in the Simulator

The Sim QA skill builds a release version of the app, boots an iPhone 17 Pro simulator, installs it, and runs six journeys in order.

  1. Cold launch. Open the app and land on the signed-in tab bar, not the sign-in screen.
  2. The core loop. Nearby fills with places, a check-in gets written and posted, the confirmation screen appears.
  3. Movement. The simulator streams a fake drive at about 40 miles an hour through three points in downtown Washington, and the map has to follow once you've moved more than a hundred meters.
  4. Photos and filters, through the photo library. The simulator has no camera, the surface where the 3.2.0 bug hid.
  5. Deep links. One from the app's own link scheme and one public web link, both have to open the right screen.
  6. The map and the globe. A missing map token shows up here as a blank basemap.

Every tap that changes something is followed by a screenshot or a read of the accessibility tree, and the rule in the skill file is blunt: an assertion counts only what the pixels show. A failed journey gets logged as a finding, and the run keeps going. The app under test talks to production with a real test account, so the agent never types a password, every QA check-in is private, and all of them go to the trash when the run ends. A run on September 13 counted exactly 34 selectable photos in the postcard picker by reading the accessibility tree. A screenshot can't count.

The first touch driver we used reported success on every tap while the tap never reached the app. We caught that on August 29 and swapped it out. After any journey with a pop-up sheet, the agent now scans the device log for sheets that failed to appear, because a screenshot can't show a thing that never showed up. And the bottom of the file lists what the simulator can't do and stays on a physical phone: real camera capture, background GPS on an actual drive, the widget gallery, the Watch and CarPlay companions, and push notifications.

Audits in Waves

The July security program in the next section was the first audit we ran as a project instead of a checklist, and the habit stuck. On August 14 the mobile app got the same treatment: twelve auditors reading in parallel, each checking every file for five things (bugs, security, architecture, dead code, and missing tests), about 715 file passes in all, with every API call checked against the 163 endpoints the backend serves. The result was zero critical, zero high, 25 medium, and about ninety low. A second audit in September found eight verified issues, and fixing them is the idea that went through the board in three hours above.

One agent drives the simulator and takes screenshots. A second agent looks at each screenshot against a short written description of what the screen is supposed to look like and returns approve or revise, with findings ranked from blocker down to low. A third makes the smallest fix for the blockers and highs, runs the typecheck, and hands the screen back to the first agent. The loop is capped so polish never holds a build forever. This covers the part of the app the 14,000+ tests can't reach.

Six Months of Receipts

The Numbers

~1.02M
Lines of Code
14,000+
Automated Test Cases
419
Database Migrations
194
API Endpoints
120
Background Jobs
6
Client Surfaces
109
Recorded Bug Hunts
6,800+
Commits Since March

Phone, Apple Watch, Wear OS, CarPlay, Android Auto, and home-screen widgets, backed by five repositories, a dedicated media pipeline, and a test suite that outnumbers the features. All of it from one architect and AI.

The Hard Parts

When Sessions Collide

For several days in August we had enough Claude and Codex sessions running at once, across separate worktrees, that they started colliding. Two subagents committed against the same git index. Work from one session was swept into another session's commit. Branches drifted far enough apart that pulling them back together meant rebasing sessions against each other before any of it could merge. On August 19 a deploy from a stale working copy silently reverted the live admin interface to an older version, and nothing about the page looked wrong afterward.

Getting out of that cost a day of careful rebasing. Every session now takes a lease on its own worktree. A reaper reclaims abandoned checkouts and refuses to delete one without positive evidence that the session ended, on the principle that visible toil is a better failure than a deleted session. A deploy guard refuses to ship from any checkout that does not match what the repository says production carries, with an override that has to be typed out in full and only for hotfixes.

One person running AI at this speed produces the parallel work of a team and inherits its coordination problems along with it. A small headcount only buys you time before you need the same workflow.

Failures That Pass Every Check

For five days in August, edge delivery of media was silently disabled. Nothing threw an error and no user saw a symptom, since the system fell back to the origin, and every documented post-deploy check passed the entire time, because all of them pass with edge delivery fully off. The fix re-enabled edge delivery and added a new alert for the condition the existing checks were structurally unable to see.

A schema completeness check on the GDPR export passed across three migrations while two columns were quietly missing from the export, because the fixture it compared against had gone stale alongside the code. Two stale things matching is not a pass, and a green suite is only worth what its assertions reach.

Security Became a Program

In early July we stopped treating security as something to check before a release and ran it as its own project. Five audit waves swept the full codebase over three days and turned up seven privacy leaks and two data-loss bugs, all fixed before any user was affected, with a written remediation plan and a retrospective kept in the repository. What came out of it is not a checklist. Request handling gained bounded body reads and typed field validators, rate limiting now runs through more than ninety files, remote image fetches reject private address ranges and redirects outright, the database users were cut down to least privilege, and a privacy probe runs against production after every deploy.

Performance Stopped Being Free

The feed query behind the app's busiest screen ran 275 milliseconds against real data volumes, and covering indexes plus buffer pool tuning brought it to roughly 1.5. A prototype serving one person never has to notice. Benchmarking is now a daily read of the numbers.

Keeping Track of All of It

The volume of feedback, half-formed features, and known problems outgrew notes and memory somewhere around midsummer. The My Day board described above came out of that pressure, and so did the cross-repo status log. Before the board, an idea lived wherever it was last mentioned, and the plan for it lived in whichever session happened to write one. Neither is glamorous work, and without them priority drifts to whatever came up last.

The System Today

The architecture diagram below shows the current state. In March this picture had three boxes: a web app, an API, and a database. The media pipeline, the R2 edge tier, the admin console, the monitoring plane, and the cross-repo documentation layer are all products of the last six months, and each exists because the simple version stopped being enough at public scale.

Haunts.IO system architecture diagram showing web, mobile, media pipeline, R2 edge delivery, admin console, and monitoring plane The Haunts.IO platform, September 2026. Click to enlarge.
The Math

What the Whole Build Would Cost the Traditional Way

The launch case study priced the first phase at twelve specialists over eighteen to twenty-four months, which at 2026 US rates of $175 to $300 an hour comes to $2M to $4M. The estimate folded technical ownership into the project manager. Running the budget and owning the system design across six client surfaces are two jobs held by two people, so the honest number is thirteen.

The six months since are a second engagement with a different roster. A streaming media engineer for the HLS pipeline, the hardware transcoding ladder, and the edge delivery tier. A Go engineer for the media daemon and its job queue. An SRE for the monitoring plane, the alerting rules, and the deploy guards. A database performance engineer for the index and buffer pool work that took the feed query from 275 milliseconds to 1.5. A security firm for the audit program that swept the codebase in five waves. A QA team to write and maintain more than 14,000 automated tests. An Android engineer for the admin console, a privacy review for the visibility model, a technical architect holding the design together across five repositories, and a project manager over the whole engagement.

That team does not finish in six months either. Hardening a live platform while shipping new features runs slower than greenfield work, and twelve to eighteen months is the realistic window, which puts the second phase at $1.5M to $3M.

Add the two together and the traditional path to what Haunts is today runs $3.5M to $7M across thirty to forty-two months, most of it spent before anyone outside the team could use the product.

One architect and AI tooling delivered both phases for a fraction of that, and the app has been live and taking check-ins throughout the second one.

Phase One · Prototype & Polish
18–24 months
Project Manager Technical Architect Backend Engineer Web Frontend Lead Mobile Lead iOS / Apple Watch Specialist Android / Wear OS Specialist Mapping Engineer AI Engineer Media Engineer DevOps Engineer UI/UX Designer QA Engineer
$2M – $4M the original case study estimate
+
Phase Two · Production Hardening
12–18 months
Project Manager Technical Architect Streaming Media Engineer Go Engineer SRE / DevOps Engineer Database Performance Engineer Security Audit Firm QA Automation Team Android Engineer Privacy Review
$1.5M – $3M the six months in this post
=
Combined Traditional Cost
30–42 months
$3.5M – $7M 23 specialist engagements across both phases
Both Phases, Vibe-Coding with MDPSync
5-day sprint + 6 months of sessions
Senior Architect + AI
A fraction of the cost
The Team This Replaces

Twenty-three specialist engagements across both phases. Commercial ownership and technical ownership sit in two different chairs.

Project Manager
Budget, staffing, schedule
Technical Architect
System design, all surfaces
Platform & Data
Backend Engineer
Go Engineer
Database Performance Engineer
Web & Design
Web Frontend Lead
UI/UX Designer
Mobile
Mobile Lead
iOS & Apple Watch
Android & Wear OS
Android Admin Console
Media, Maps & AI
Media Engineer
Streaming Media Engineer
Mapping Engineer
AI Engineer
Infrastructure
DevOps Engineer
Site Reliability Engineer
Quality & Security
QA Engineer
QA Automation Team
Security Audit Firm
Privacy Review
Phase one, prototype and polish Phase two, production hardening

The Bottom Line

The five-day prototype was a real app, and the original case study captured how fast that kind of build can come together. What six months of production hardening adds is the part of the argument that matters to anyone deciding whether to build this way: the demo comes fast on the same models everyone has, and the other 80% is a long stretch of testing, guardrails, and operations where an experienced architect earns the title. Haunts became a commercial product because that machinery got built.

Today the app runs across six surfaces, carries more than a million lines of code, and holds up under strangers using it in ways no demo ever predicted. If you want the same arc for your product, tell us what you need built and we will tell you what it takes.

Tell Us What You Want Built

From a five-day prototype to a commercial platform in six months, with the receipts to show for it. Start a conversation and we will tell you what your build takes.

← Back to Writing