Skip to content

TendForm: managing a solo SaaS build to production quality

How I scoped, reviewed and validated a HIPAA-capable form builder I built alone: the rules I wrote down, the checks I trusted, the 14 bugs that got through anyway, and the process I'd change.

Tim Urista 11 min TendForm
Title card: Human in the Loop, Part 1. TendForm: managing a solo SaaS build to production quality. A small timeline of 14 bugs from cause to fix. Stats: $49 HIPAA tier per month, 27 MCP tools, 35 pytest plus 5 Playwright tests, 14 bugs traced to cause.
On this page
  1. What I kept for myself
  2. The rules I wrote down
  3. What runs where, and what it costs
  4. The checks I actually relied on
  5. 14 bugs, from cause to fix
  6. The docs drifted, and the agents believed them
  7. What I’d change in the process
  8. If you manage a team, measure rework, not authorship

At a glance

  • I ran TendForm like a tech lead runs a small team: I set the scope, wrote the rules into CLAUDE.md and skills, reviewed every change, and kept the contracts and pricing for myself.
  • The checks I actually relied on were a type-checked builder deploy, 'verified live' runs against production, 390px screenshot review and deploy skills. The test suite was thin, and CI deploys have been off since July 9.
  • I traced 14 bugs from the commit that caused them to the fix. Six were fixed within two hours. Eight lived from 18 hours to 20 days, mostly at the edges between features: plan tiers, teammates, existing data, hostnames.
  • None of those 14 fixes added a regression test. Of the 35 non-merge commits with 'fix' in the subject, two touch a test file. That's the process gap I'd close first.
  • For a team, measure rework per feature and cost per merged change. Who typed the diff is the least useful number in the log.

TendForm is a form builder. You can build a form by hand, paste in markdown, or ask Claude or ChatGPT to make one and get a live link and a QR code back. There’s a free tier, a Pro tier at $19.99 a month, and a HIPAA tier at $49 a month with its own encrypted Postgres enclave.

I built it alone, on my own time, while working my day job as a Senior Cloud Engineer at Apple. The git history runs from June 24 to September 12, 2026. Most of the code was drafted with Claude Code; I owned the scope, the review, and what shipped.

This is Part 1 of Human in the Loop, a series about the part of AI-assisted engineering that doesn’t get demoed: deciding what to build, checking what came back, and catching what slipped. On a one-person team, the manager is also the reviewer, the on-call, and the person who signs the BAA.

MCP tools

27

OAuth 2.1, two scopes

pytest + Playwright

35 + 5

Thin, and I'll say why

Bugs traced to cause

14

8 lived overnight or longer

HIPAA tier / month

$49

The TendForm homepage: headline 'Build beautiful online forms & surveys', a prompt box reading 'Onboard a new client for my meditation studio', and a generated onboarding packet preview with health history, liability waiver, informed consent and cancellation policy.
tendform.com today. The hero prompt uses the same generation path the MCP server exposes to Claude and ChatGPT.

What I kept for myself

When implementation is cheap, the scarce work moves to decisions that aren’t code. I kept four of them.

The architecture’s cost shape. The first real commit, c28faeb Build TendForm: WYSIWYG builder, renderer, FastAPI backend, infra , was 9,371 lines across 92 files, including a Helm chart. About three hours later, 81c645f Backend deploy: plain k8s manifests, migrate Job replaced the chart with plain manifests. Deleting a first infrastructure decision the same afternoon is the right call when it costs an afternoon.

The compliance boundary. DigitalOcean signs a BAA, but its Managed Databases product isn’t on the covered-products list, while DOKS, Droplets, Volumes and Spaces are. So the PHI enclave runs Postgres with CloudNativePG inside the cluster. docs/LESSONS.md puts it plainly: “They sign a BAA” is not the same as “the specific service you want to use is in scope.” No code review would have caught that. Reading the contract did.

Pricing. The HIPAA tier is its own $49 tier, not a Pro add-on. The product doc’s math: about 20 HIPAA customers is roughly $1K MRR, versus about $400 if it were bundled. LESSONS.md again: “Price to survive the risk, not just to win the click.”

Platform policy. The free tier was supposed to be ad-supported. Twelve days after the first commit, 3efce3d Disable live AdSense on form completion screen (AdSense thin-content flag) pulled the ads, because the only ad surface was a form’s thank-you screen and Google flagged it as thin content. A revenue line in a README is not a revenue line.

The rules I wrote down

A new engineer on a team reads the onboarding doc once. An agent session reads it every time. That makes CLAUDE.md the highest-leverage file in the repo, so its “Decisions / conventions (read before changing things)” section holds the invariants I didn’t want relitigated:

  • “Public form path must stay lean.” Respondents hit Cloudflare Pages and a Worker that caches rendered HTML at the edge.
  • One container image runs as api, worker, migrate or seed, and cron jobs live in the worker instead of a CronJob fleet.

Skills hold the scar tissue. The deploy-backend skill opens with “ALWAYS build for linux/amd64”, because my Mac is arm64 and the cluster isn’t. It also documents a zsh quirk where "$REG:latest" silently pushed to an image called tendform-appatest, so :latest never updated. Each line is an incident I only wanted once.

Tool descriptions are rules too. Models kept creating “phantom blank fields” by putting a type tag like [email] on its own line. The fix was a MARKDOWN_GUIDE appended to the MCP tool descriptions, under a comment that reads “Authoring rules the model MUST follow.” The model reads the tool description on every call. It never reads your docs site. That same commit, 88e9493 MCP: packet tools, QR/resource-link results, markdown artifact fix , reports “13 pass incl. phantom-field regression.” Remember that one. It’s the rare fix in this story that left a test behind.

The allowlist was the weakest rule. On September 12 I committed a .claude/settings.json that allows mostly read-only commands: kubectl get/describe/logs, curl, type checks, builds and make test. Before that, permissions had accreted one approval at a time in an uncommitted local file, which by then allowed make deploy-app, make deploy-public and git push. Nobody decided that. It just built up, the way IAM policies do.

What runs where, and what it costs

TendForm architecture diagram. Respondents, form owners and Claude or ChatGPT reach Cloudflare: Pages for the public site with an edge-rendering Worker and HTML cache, Pages for the builder, and a proxy for api.tendform.com. The proxy routes to DigitalOcean Kubernetes, where a FastAPI api, in-cluster Redis, an arq worker and a migrate job run. A HIPAA namespace runs CloudNativePG and Prometheus, Grafana and Loki. Managed Postgres and Spaces hold data. The backend calls OpenRouter, Cloudflare Email Sending, Cloudflare Browser Rendering and Stripe. Amber notes mark cost decisions.
What runs where. Amber notes are the cost-relevant decisions, at list prices from the repo, not from an invoice.

There’s no monthly bill in the repo, so I won’t invent one. The repo does show the shape of the bill: the public path barely touches the backend, Postgres is a PgBouncer pool on a shared managed cluster, and the enclave is, in the storage design doc’s words, “‘just’ two more pods + PVCs.”

The checks I actually relied on

Here’s the honest inventory. There are 35 pytest functions across 8 files and 5 Playwright tests, one of which loops over 8 public routes. The last local Playwright run on disk says "status": "failed". CI runs lint, pytest, type checks and builds on every push, but on July 9 7f3b802 ci: disable auto-deploy on push (deploy manually from local for now) turned off the deploy workflows because they were broken. They’re still off. Deploys run from make targets on my laptop, so nothing forces CI to be green before production changes.

So what did I trust?

  • A type-checked builder deploy. make deploy-app runs the builder’s tsc -b && vite build, so a type error stops the deploy. The public site’s build has no tsc step.
  • “Verified live.” The standard for infrastructure work was to exercise the change against production and say what was checked in the commit. The HIPAA work reads like a checklist: 1f55c30 enclave schema + envelope-encryption module (verified live) , then the write path, then the read path, each with what was checked. Commits also reported test counts, like “Tests green (26)” on the HIPAA tier.
  • Screenshot review at phone width. The mobile-test skill runs Playwright at 390×844, writes a PNG per route, and says to “Read each PNG” and look for overlap and clipped text. The run fails if any page scrolls horizontally.
  • Testing in the real client. The MCP server was exercised through Claude itself. The local permission file shows calls to TendForm’s own create_form, update_form and delete_form tools.
A Claude chat message after using the TendForm integration: 'Published and live. Your Airbnb Guest Onboarding packet (all three forms) is now public at tendform.com/p/…', with advice to share the link or print the QR code.
Validation in the real client: one conversation, three forms, a public packet link at the end.

That’s a reasonable stack for a solo product, with one blind spot: every check exercises the path you just built. None re-walks the paths you built last month.

14 bugs, from cause to fix

To see where quality actually slipped, I traced 14 bugs back to the commit that introduced the behavior, using git log -S and the fix commits’ own explanations.

Timeline of 14 TendForm bugs from the commit that introduced each to its fix, June 22 to August 24, 2026. Fixed within two hours: Grafana dashboard showed No data, 16 minutes. BAA modal asked for confirmation twice, 17 minutes. Preview image dropped by claude.ai, 36 minutes. Google sign-in landed on the marketing site, 37 minutes. Rich text lost blank lines on save, 39 minutes. Analytics beacons silently dropped, 1 hour 41 minutes. Lived overnight to three weeks: packet BAA gate checked the wrong user, 18 hours. Form edits never reached the edge cache, 2 days. Spinner stuck over every public form, 5 days. Start Over always returned 422, 5 days. Ads stayed after a Pro upgrade, 8 days. HIPAA-plan accounts saw ads, 18 days. HIPAA flip stranded existing responses, 19 days. Workspace invite links 404'd, 20 days.
Each row runs from the commit that introduced the behavior to the commit that fixed it. Short bars were fixed in the same working session. Long bars sat on paths nobody re-walked.

The pattern is clean. The six quick fixes landed in the same working session as the change that caused them, while it was still being checked. Two of the commit messages show the check doing its job: a Grafana dashboard that was “all ‘No data’” until its queries were fixed and “verified returning data,” and a preview image that “never reached the user” in claude.ai. The eight slow ones lived at the edges between features, where no one was looking: plan tiers, teammates, existing data, and the gap between three hostnames.

None of the 14 fix commits added a test. Across the whole history, 35 non-merge commits have “fix” in the subject, and two of them touch a test file.

The stale cache the MCP write path exposed

The public Worker cached rendered HTML under a key built from the slug, the form version and the build ID. version only bumps when answer fields change, which made sense when I designed the key on June 27, because responses are versioned by their questions. Two days later the MCP update_form tool started making cosmetic edits: titles, descriptions, themes. Those refreshed the JSON in Redis but left version alone, so the edge kept serving the old HTML.

49a26d3 Fix: editing a live form never reached end users (stale edge cache)

The commit body calls them “silent edits that never went live.” The fix adds a content hash, rev, to the cache key. A sibling bug had the same shape: bc6a402 Bust public-form cache on plan change so Pro upgrade removes ads immediately , which lived eight days. Upgrading didn’t remove ads until the TTL ran out, which is a great way to annoy someone who just paid you.

A CSS fix that hid every public form

2e13fe4 fix(web): eliminate homepage/login FOUC+CLS was a good change: it took layout shift from about 0.98 to about 0, and it added a boot spinner hidden by #root:not(:empty)+#tf-boot. That’s an adjacent-sibling selector. The edge Worker injects a <script> between those two elements when it renders a form, so on every server-rendered public form the rule never matched and a full-screen spinner sat on top of a working form. It took five days and 12871d8 fix(public-web): unstick form spinner to swap + for ~.

This one is on my validation design. The change touched the public form path, and the public form route wasn’t in the screenshot sweep. A type check wouldn’t have caught a selector. One Playwright assertion that a published form’s first input is visible would have.

Three hostnames, one bug class

TendForm lives on tendform.com, app.tendform.com and api.tendform.com. The split is good for caching and deploys. It also produced the same bug again and again:

  • 02f87ab Fix analytics beacons dropped cross-origin : beacons sent as application/json need a CORS preflight that sendBeacon can’t do, so the browser “silently dropped every beacon.”
  • 2e13fe4 fix(web): polish loading & redirects : Google sign-in landed users on the marketing site.
  • 7fd8ea3 fix(workspaces): point invite link at the builder, not the public site : “the very first click 404’d,” 20 days after invites shipped.
  • b97b36c fix(links): route builder-only deep links to app.tendform.com , nineteen minutes later, which opens with “Same class of bug as the invite link” and sweeps billing, MCP and notification links in one pass.

That last commit is what good delegation looks like: fix the instance, then hunt the class. It just came three weeks late, and it left no test behind to keep the class dead.

HIPAA in two days, edge cases for weeks

On July 6 and 7 the PHI enclave went from nothing to a BAA click-through: envelope encryption, separate write and read paths, Prometheus, Grafana and Loki, a WORM log archive, compaction crons and auditor tooling. The verified-live checks were real, and the core held up.

What verification didn’t cover was everything around the enclave:

  • a890a71 fix(pages): BAA gate checks the acting user : packets with a teammate’s form “402’d forever and re-opened the BAA modal in a loop.” Live for 18 hours.
  • 73f365d fix(app): don’t show ads to HIPAA-plan accounts : the builder checked for exactly "pro", and HIPAA is a Pro superset. Live for 18 days.
  • d597a91 fix(app): copy existing responses into the PHI enclave : flipping a form to HIPAA showed “1 response” and “no responses yet” at once. Live for 19 days.

None of these are exotic. A reviewer who asks “what about a team member, a higher plan, or a form that already has data?” has a good shot at all three. I didn’t ask, so the checks proved the happy path very thoroughly.

The docs drifted, and the agents believed them

CLAUDE.md is read at the start of every session, and today it’s wrong in at least five places. It says the edge cache key is slug + version + BUILD_ID, ten weeks after rev was added. It says Google OAuth is disabled in the UI, though Google sign-in shipped July 10. It says email goes through a Cloudflare Email Worker with MailChannels, while the code has used the Email Sending API since day one. It still lists infra/helm/, which was deleted on day one, and it describes push-to-deploy, which has been off since July 9. docs/LESSONS.md, written after the cache fix, repeats the old key.

In September I added AGENTS.md so other coding agents get the same playbook. It’s a byte-for-byte copy of CLAUDE.md, so the drift now comes in two copies.

Docs that describe infrastructure are a cache with no invalidation. A new hire would question them. An agent reads them with complete confidence.

What I’d change in the process

  1. Every fix ships a regression test. Not a suite overhaul, just one test per fix. The cache key, the plan check and the invite link are each a ten-line test. “Verified live” was a standard I set and enforced. This one I never set.
  2. Ask for the bug class, not the bug. After the second hostname bug, the request should have been “find every place we build a URL across hostnames,” plus one URL-builder module with a test per link type.
  3. Put the edges in the review checklist. For any plan, permission or data-model change: other plan tiers, other workspace members, existing rows. That covers at least three of the eight slow bugs.
  4. Screenshot the product, not just the marketing. The mobile sweep covers 8 public routes and no published form. The spinner lived five days in that gap.
  5. Docs change in the same diff as infrastructure. And AGENTS.md becomes a symlink.
  6. Commit the allowlist on day one. Deploy and push stay prompts. A permission file that grows by accident is a policy nobody reviewed.
  7. Put a stop condition on pixel-pushing. Cheap edits produced five heading-weight commits in fifteen minutes on August 22. Each one is review time too.

If you manage a team, measure rework, not authorship

It’s tempting to count how much code the agent wrote. That number doesn’t answer anything a manager needs. The useful ones are above: how long bugs live, where they cluster, and whether each fix left the system harder to break.

My day job is cost attribution for private cloud infrastructure, and I’d set this up the same way:

  • Tag work to tickets. Every agent session and commit carries a ticket or feature ID. I traced these 14 bugs by hand with git log -S; with tickets it’s a query.
  • Measure rework per feature. Count the fix commits that follow a feature and how long each bug lived. For TendForm the answer was 8 of 14 bugs living overnight or longer, and zero fixes with tests. That’s a process finding, not a model finding.
  • Tag the spend and compute cost per merged change. Per-team or per-repo API keys, so tokens land on someone’s ledger, joined monthly to merged work and its rework. I can’t give you that number for TendForm, because I never tagged the tokens. That’s the same gap I close on server fleets at work: the spend exists, the work exists, and nothing joins them.

The model drafts. The team still owns scope, review, validation and the bill. Part 2 applies the same lens to validating a production AI system in TrendVesting, and Part 3 covers the quality gates on AI-generated content in TigerMill.

If your team is shipping with agents and the rework or the invoice is getting hard to explain, that’s the work I do.

engineering-managementcode-qualitymcpunit-economics

Cash-pay advisory
from $250/hr

Check fit