Skip to main content

Right now I have three iOS apps sitting in Apple’s review queue — Feastmark, PayoffPilot, and ReelTalk — and a fourth, Gunmark, out in TestFlight beta.

One developer. A day job doing DevOps and secrets management at enterprise scale. Two kids, ages eight and six. The hours that are actually mine start after everyone else is asleep.

This is not a post about how I hustled. It’s a post about what broke, because the thing that broke wasn’t the part I was worried about.

Four at Once Was a Decision

It looks like an accident. It wasn’t.

Two of those apps had been sitting half-finished for months. Not abandoned exactly — just in that state where you open the project, remember where you left off, run out of evening, and close it again. I’d been doing that for a while, and I could see where it ended: two projects that quietly rot until the SDK moves on without them.

So I made a call. I gave myself two weeks, finished what was already there, tested it myself, and submitted it. Not “polished until I’m proud of it” — submitted. The alternative wasn’t a better version of these apps later; it was no version of them ever.

I’ll say the obvious thing plainly: I got a lot further in those two weeks than I would have two years ago, and AI-assisted development is a real part of why. An hour after bedtime isn’t enough time to reload an entire codebase into your head. It’s enough when something else is holding that context for you. That’s a tool, not a shortcut — the decisions and the judgment calls were still mine to get wrong.

What I did not anticipate was the bottleneck.

The Marketing Site Was the Bottleneck

I assumed shipping four apps meant four times the Swift. It didn’t. The apps were the manageable part. The studio site was where everything jammed.

Adding a single app to bigbeardapps.com meant roughly thirty edits spread across thirteen files. The app’s own page, obviously — but also the nav on every other page, the “three apps in App Review” count that appears in more than one place, the cross-promo block at the bottom of each app page, the press kit, the footer’s privacy links, the sitemap.

Thirty edits done by hand means thirty chances to miss one. And I did miss them. A nav that listed three apps on one page and four on another. A count that said three when it should have said four. Cross-promo blocks pointing at apps that hadn’t been added yet. None of it broken enough to notice while I was looking at the app I’d just added, all of it obvious to a stranger who landed on the wrong page.

The fix was to stop hand-editing. There’s now a single data/apps.json that describes every app — slug, status, URLs, media, which pages it appears on — and a generator that rewrites every shared region of every page from it. The managed regions are fenced off in the HTML with comments that say, more or less, do not edit by hand. Adding an app is now one entry in one file and one command.

Then I made it impossible for me to skip that command. A pre-push hook stands in front of the deploy, because on this host a push to main is a deploy. It has two hard gates:

  1. The generated regions have to match the data. If I hand-edited a managed block and didn’t regenerate, the push stops. This is what makes “I’ll just fix this one nav by hand” structurally impossible to ship.
  2. The site audit has to pass. Eight checks: every app has a page, no dead internal links, every page has its head tags, canonical matches og:url matches the sitemap, media files exist, the press kit rows are complete, and the app counts are consistent everywhere they appear.

That class of bug — the site quietly contradicting itself — is now gone. Not “less likely.” Gone, in the sense that it can’t reach the server.

Which brings me to the bug that made all of that feel a lot less clever.

The Bug That Passed Every Check

The studio site has a launch-list form. One email when an app goes live, no newsletter, no tracking. It posts to a third-party provider.

It worked. You typed your email, hit the button, and the form was replaced with a friendly “check your inbox.”

Nobody was subscribing.

The form posted cross-origin, so instead of fetch — which for an opaque cross-origin response literally cannot tell success from failure — I retargeted the POST at a hidden iframe so the page would stay put. Then, in the submit handler, I called replaceWith() to swap the form for the success message.

replaceWith() detaches the form from the document. The browser was still in the middle of sending it. Detach the form mid-flight and the browser cancels the request outright. So the sequence was: visitor submits, form vanishes, success message appears, POST cancelled, provider never hears from us.

Every automated check passed. The markup was valid. The audit was green. The gates I’d just built to catch inconsistency had nothing to say, because nothing was inconsistent — the page was perfectly, verifiably wrong.

I found it by opening a browser and typing in my own email address.

The fix is two rules, both now written down in that file so I don’t rediscover them:

  • The form is hidden, never removed, and only after the response arrives. Nothing gets detached mid-flight.
  • Success is announced on the iframe’s load event, not on submit. Claiming success at submit time means telling someone they subscribed before the server has said a word — and if it fails, you’ve lied to them.

There’s a third, smaller one: the iframe target is set from JavaScript, never in the markup. Put it in the HTML and a visitor with JS disabled posts into a hidden iframe and watches nothing happen.

The lesson isn’t “write more tests.” It’s narrower and less comfortable than that. Your tests tell you what you thought to ask. I had built two hard gates and eight checks around the failure mode I’d already been burned by — inconsistency between pages — and they worked. They were also completely blind to a form that lies, because it hadn’t occurred to me that a form could lie.

What’s Still Unsolved

The apps are done. The site is honest and can’t easily go crooked again. That part I know how to do.

Here’s what I don’t: nobody knows any of this exists.

I have no audience. Ten karma on Reddit, which in the communities where these apps would actually be useful is indistinguishable from a spam account — and those communities are, correctly, hostile to someone showing up to promote something. Gunmark has a further problem: it catalogs firearms, and Meta, Google, and Reddit all prohibit firearms advertising, with automated enforcement that doesn’t stop to consider that a cataloging app isn’t a gun store. Paid acquisition isn’t a lever I have.

So four apps are about to exist in public and I have no distribution. I don’t have an answer yet. I’d rather write that down honestly than pretend the hard part was the code.

The code was the easy part. It usually is.


The apps live at bigbeardapps.com.

Raul C. Peña

Raul C. Peña

Senior Software Engineer at Dell Technologies. Air Force veteran, 20+ years as a Texas real estate broker, self-taught coder. Passionate about DevOps, IBM Vault (formerly HashiCorp Vault), and building things that matter.