ENGINEERING · June 16, 2026

Vibe coding a website for my parents' clinic

Adinath is my family's hospital. Not a metaphor, not a portfolio prop — Adinath Multispeciality Hospital in Shahibaug, Ahmedabad, orthopedics and gynecology since 1990, run by my parents: Dr. Ashok Sajnani, joint surgeon, 35 years; Dr. Sunita Sajnani, OB/GYN, 30 years. Real patients book real appointments. The phone number on the contact page rings a real desk.

I built its website mostly on vibes, at speed, with an AI pair. Then I spent the next three months learning — in public, in the git log — which of those fast decisions survived contact with reality and which ones billed me later. This is the honest version, reconstructed from the commit history rather than from memory, because memory flatters and git log doesn't.

The short version: 236 commits, but only 22 days of actual work. Four days hold 72% of everything. The site went from empty repo to a live hospital-management system in about two days, sat untouched for five weeks, then came back three times with rising discipline. What changed over those months wasn't my speed — I was fast the whole way. What changed was the leverage per unit of speed, and how cleanly I could undo a mistake. That's the whole story of taking something vibe-coded and making it real.

Night one: fast, and honest about it

The clock starts December 20, 2025, 19:37. First commit: public pages. Forty-nine minutes later there's a booking flow, a medical store, SEO tags, and a PWA manifest. Two hours and twenty-five minutes in, there's a complete hospital-management system — login, three role-based portals, forms, the works. This is vibe coding at its actual strength: an entire product's shape exists before midnight.

Two things about that night matter more than the speed.

First, the grounding-in-realism move happened immediately, not months later. At 22:42 — two hours in — there's a commit that removes fake data: "Poonam as receptionist, honest stats, emergency notice." I wasn't building a demo with lorem ipsum and stock doctors. The instinct to put the real hospital's real people and real numbers in was there on night one. That turned out to be the thing that separated this from a throwaway.

Second, the git log is honest about the cost of the pace. Between 23:03 and 23:09 that first night there are three commits whose only job is cleaning up merge-conflict markers left in the code. That's the fingerprint of moving too fast to read your own diffs — the literal thing Karpathy meant by "vibe coding": give in to the vibes, forget the code exists. It works, right up until it doesn't.

December 21 was the peak of raw output: 91 commits in a single ~23-hour session, midnight to 23:32. Role-based guides, WhatsApp share links, a mobile nav, and a hard push to "100% test coverage" and "100% E2E pass rate." Remember that number. It doesn't survive.

Then, silence. From December 30 to February 5 — five weeks — exactly one commit, an automated dependency bump. The burst was real and so was the crash. Velocity like this isn't a steady throttle; it's ignition and burnout.

The two bets I baked in while moving fast

Two architectural decisions got made in that first blitz, both for the same reason: I reached for the tool already open on my desk.

Bet one: translate in the browser. The hospital serves three languages — English, Hindi, Gujarati. In Ahmedabad that's not a nice-to-have; it's whether a patient can read the booking form or bounces. I built i18n by tagging DOM elements with data-i18n attributes and swapping the text at runtime with a script, persisting the choice in localStorage. Felt pragmatic.

Bet two: React for everything interactive. Every page that did something — booking, patient signup, status check — became a React island. Felt like the modern default.

Both bets ignored the same truth, and I didn't see it until I'd changed frameworks.

The disciplined return, and the afternoon it all came due

The site came back to life on February 6 with a visibly different character. The commit messages switch to conventional commits — feat:, fix:, refactor: with scopes. The work is deliberate. Security hardening for "7+ vulnerabilities." A real booking wizard. And a theme.

Here I should confess the CSS. I changed the design a lot35 commits touched stylesheets, out of 49 that mention design at all. But it isn't random flailing; it's one real pivot with a tail. On February 8 there's a "warm theme redesign — forest green, cream, Playfair Display," and then a run of follow-up commits dragging the rest of the site into line with it. Later I added a STYLE_GUIDE.md and a stylelint config — design discipline arriving after design churn, which is exactly the order it usually arrives in when you're figuring out what a real business should look like rather than what you can build.

February 19 was the realism sweep: I migrated the entire data layer from localStorage to Supabase, trimmed ~1,450 lines of dead code, and — tellingly — kept removing fake data in favor of the hospital's real staff and real stats. The toy became a backend-backed system.

But the afternoon that actually taught me something was March 18 — the day I moved the whole site to Astro. The timestamps tell the story better than I could:

  • 02:56 — scaffold the Astro migration. The single biggest commit in the repo. Both bets come along for the ride: i18n as a runtime hook, forms as React islands.
  • 16:31 — first crack. "Translations were empty objects because loadTranslations() was never called." The runtime approach is failing on a static build.
  • 17:05 — I double down. I add a bespoke i18n-static.ts — a runtime script that walks the DOM, reads localStorage, and listens for a lang-change event fired by a React switcher. I bolt a runtime translator onto a static site to save the runtime translator.
  • 18:08 — I reverse it. Completely. Build-time i18n with Astro's [...lang] dynamic routes: each page generates /en, /hi, /gu variants with the translated text baked into the HTML. Zero runtime JS for translation. I delete i18n-static.ts and the React switcher, convert the forms from React to plain HTML plus a fetch(), and shed about 900 lines of component code.

The Adinath homepage in English — warm theme, real doctors, the language switcher top-right.
The Adinath homepage in English — warm theme, real doctors, the language switcher top-right.

The same homepage at its own /gu/ URL, fully rendered in Gujarati — headline, nav, and stats all baked into the HTML at build time. This is what the runtime approach could never do cleanly: three real pages, one per language.
The same homepage at its own /gu/ URL, fully rendered in Gujarati — headline, nav, and stats all baked into the HTML at build time. This is what the runtime approach could never do cleanly: three real pages, one per language.

One hour and three minutes from doubling down on the wrong idea to deleting it.

The bug that forced it is worth stating plainly, because it's the kind only a real user hits: click Gujarati, and the navigation translates but the booking form right below it stays in English. Half the page switches. It looks broken on exactly the page where a patient is about to hand over their name and phone number to a hospital. The root cause wasn't a missing event listener — it was that I had two sources of truth for "the current language" and no plan for them to agree. A data-i18n DOM system and a React hook system aren't two implementations of i18n; they're two different applications sharing a page.

Bet two died in the same commit. Those nine interactive pages were shipping roughly 360KB of JavaScript each — React plus the full Supabase client — so a user could type their name and have it land in a database via a single INSERT. A form doesn't need a component tree to do that. It needs an HTML <form> and a fetch. The fix was about 3KB a page. [VERIFY: the "~3.1MB JS eliminated total" figure is order-of-magnitude, not a hard measurement.]

The booking flow — pick a doctor (Dr. Ashok or Dr. Sunita), then date, details, confirm — or book straight over WhatsApp. Every one of these steps once shipped ~360KB of React and Supabase client to collect a few fields; now it is plain HTML and a fetch.
The booking flow — pick a doctor (Dr. Ashok or Dr. Sunita), then date, details, confirm — or book straight over WhatsApp. Every one of these steps once shipped ~360KB of React and Supabase client to collect a few fields; now it is plain HTML and a fetch.

Strip away the specifics and both bets are the same mistake: doing at runtime, per user, what could be done once at build time. Astro's entire pitch is zero JS by default, opt into interactivity where it pays. I'd had it backwards — interactive by default, static as the exception — and the framework quietly let me, then sent the bill to the slowest phone on the worst connection in Ahmedabad.

The uncomfortable part: the tests

Here's the part that's easy to leave out. On December 21 I hit "100% test coverage" and "100% E2E pass rate." Today the repo has zero test files.

They didn't fail. They rotted. Thirty-five commits touched test files across the project's life, and by the end the tests were gone — quietly dropped somewhere in the churn of two framework migrations and a redesign. And here's the sharp bit: no test ever covered the i18n layer. Not one touched translation or language routing.

Which means the split-brain language bug — the Gujarati-nav-English-form bug, the single most user-visible failure in the whole project — shipped and survived precisely because the one dimension that made this site matter to its users was the one dimension nothing tested. I had 100% coverage of the parts that were easy to test and 0% of the part that was load-bearing. That's not a testing failure. It's a failure to point testing at the risk.

Peak, and done-for-now

Once the platform settled, the same daily throughput I'd once spent on plumbing went entirely into product. March 28: 26 commits of pure product value — a comprehensive SEO overhaul with structured data and hreflang, doctor profiles enriched with verified research, community and events sections with photo galleries, WebP conversion, a WhatsApp deep-link so a patient can start a conversation in one tap. The project ends in early April on three commits of SEO polish — canonical slashes, hreflang codes, FAQ rich snippets. Not features. The sign of something that's reached done for now.

That's the acceleration curve, and it isn't about getting faster. Three peaks, same throughput, rising leverage: December was raw output, March 18 was architectural judgment, March 28 was product. The speed was constant. What compounded was knowing where to point it — and how fast I could admit I'd pointed it wrong.

One honest scoping note, because a security engineer should draw the line: Adinath handles booking-form PII — names, phones, appointment requests — behind Supabase Row Level Security, HTTPS, and no secret keys in the browser. That's a defensible posture for what it is. It is not HIPAA compliance, which needs a signed BAA and a very different budget, and I won't imply otherwise. And to be fully straight: the site isn't revenue-generating. It's a real thing for real patients and also, honestly, an experiment — proof that I can take a family business online properly, which is its own kind of stake.

Where the field landed while I was learning this

The useful thing about doing this in 2026 is that the whole industry was learning the same lesson in parallel, and it's worth ending on where the art actually is now — because every mistake above has a name and a current answer.

On vibe coding. The vocabulary moved even as I worked. Karpathy's "vibe coding" (early 2025) already carries a faint pejorative charge for production work; Simon Willison's "vibe engineering" (late 2025) named the disciplined opposite — accelerate with AI while staying accountable for the code — and by early 2026 Karpathy's own "agentic engineering" is overtaking it. The honest consensus: vibe coding gets you the demo in an afternoon, and the demo is the easy 70%. The last 30% — the edge cases, the security, the language that actually renders — is where shipping lives, and it still demands a spec to ground the work, evals to prove it, and a human who owns the result. The vibes get you the prototype; engineering is what makes it survive production.

On i18n. What I clawed my way to on March 18 is now just the settled answer: build-time, one real HTML page per language, per-locale URLs, zero runtime translation. Astro's built-in i18n is a routing layer, not a translation engine — and that distinction is the whole design. The field even moved past me, toward compiler-based, type-safe i18n like Paraglide — but for a trilingual content site, a small hand-rolled dictionary plus build-time routes is still exactly right. Doing i18n right in 2026 isn't a plugin you bolt on. It's a build-time decision: three languages, three real URLs, three real pages, baked once and served forever.

On the framework. The pendulum didn't just swing back to the server — it found a new equilibrium: ship HTML by default, treat interactivity as islands you opt into, not a tax you pay everywhere. Astro 6 shipped in March 2026; Cloudflare acquired Astro's company in January; even Next.js converged on the same static-shell-plus-dynamic-holes idea. The bet I fumbled into — static-first, hydrate only what earns it — is now the mainstream one.

On security. The one sentence that matters: the publishable key is not a secret, so Row Level Security is your only real boundary. Ship the key to every browser without a thought — it was never the lock. Your patients' names are only as safe as the RLS policy standing between that key and the table. (Supabase even renamed the keys in 2025 to make this obvious — sb_publishable_ vs sb_secret_.) The 2025 cautionary tale, CVE-2025-48757, was 170+ live apps that shipped with RLS simply never turned on. Write the policy like it's the only lock on the door, because it is.

On testing. Here's my actual takeaway, and the field's: on a static content site, TDD-first is mostly the wrong hammer — there's no behavior to drive, and a green astro build already tests more than most hand-written unit tests would. The multilingual dimension is the exception that earns its keep. A five-line key-parity check that fails the build when hi or gu is missing a key that en has, plus one browser pass per language to catch untranslated keys leaking onto the page, would have caught my headline bug — and buys more real safety than the 100% coverage I was so proud of on night one. Because the bug that ships on a trilingual site isn't a broken function. It's a missing translation.

On being found. This is the part that surprised me most. The next person looking for this hospital may never type its name into Google — they'll ask an AI. And AI is roughly 30× more selective than Google local search; one 2026 index found ChatGPT recommends only about 1.2% of local businesses versus Google's ~36%. But here's the quiet payoff: the way you get recommended by a machine turns out to be the same unglamorous groundwork — a complete Google Business Profile, byte-consistent name/address/phone across Google, Bing, Foursquare, and Maps, clean hreflang, honest MedicalClinic and Physician schema. The AI engines read from the live web index; the SEO work is the AI-discoverability work. The clinic that did the boring parts is, almost by accident, the one the assistant can quote when someone asks "which orthopedic clinic in Ahmedabad?" And the one thing that won't get you there is a hopeful llms.txt — 97% of them, in a 137,000-site study, never get read.

That's the through-line of the whole thing, from the fake data I deleted at 22:42 on night one to the schema I was still polishing in April. The default you don't think about is the one that bills you later. Vibe coding is very good at generating defaults very fast. Turning that into an actual product — one a patient's parents can use, in their own language, on a cheap phone — is just the slow, honest work of noticing which defaults were wrong, and having the nerve to delete them.


I spend my days on trading and security systems, where a wrong default costs milliseconds and money. Adinath is the opposite end of the spectrum: a static site for a family hospital, where a wrong default costs a patient who can't read the booking form in their own language. Same lesson in both worlds. This account is reconstructed from the git history; corrections — especially from my parents, who know the hospital better than the commit log does — are welcome.