What I Didn't Release
I missed last Tuesday. Today is Tuesday too, and there is no new app today either.
This is the post where I say that out loud instead of quietly skipping two weeks and hoping nobody was counting. Two weeks, no launch. The last time that happened was February, when App Review sat on two submissions and I wrote about the waiting instead. The difference is that February was Apple’s fault and this one is mine.
What did happen is that I released a pile of updates, spent an unreasonable number of hours standing up server infrastructure for an ad blocker, and got genuinely tired. All three of those deserve an explanation.
What actually shipped
Soundshine 1.3.3: the real reason this one took a week was a corruption bug that only appeared when two things read the audio ring at once. I had it addressing the ring with per-client cursors, which drifts the moment the readers disagree about where they are. Now it addresses by sample time, which cannot drift because the timeline is the same for everybody. The driver also reports a concrete mute control class, so the device finally has a mute that the system respects, and it announces volume and mute changes instead of silently changing them. The menu bar panel got tightened up, and the German and Hindi strings got fixed by someone who noticed the German was switching grammatical person mid-sentence.
Soundshine Record 1.1: twenty-four fixes across capture, recording, licensing, and the UI. I ran an adversarial bug sweep against it, which is where I hand the codebase to a reviewer whose only job is to find things that are broken and refuse to be reassured. It found twenty-four. Version 1.0 had been out for three days at that point.
Portie’s website: I had thirty-nine separate pages, one per port number, each one thin and none of them ranking. They are now one consolidated reference hub, with the old URLs redirected into the right anchors. I also fixed a claim I had been making about port 5000 for months: on a Mac it is AirPlay Receiver that grabs it, not a web framework defaulting there.
Ejecta’s website: same treatment, retargeted at the questions people actually click on instead of the ones I assumed they typed, and mirrored into eleven other languages.
Decant’s website: an SEO foundation it never had, plus the first search-targeted guide, localized into Spanish, Portuguese, Japanese, German, French, and Italian.
Freesend: custom headers on the send endpoint, and it now returns the real Message-ID so replies actually thread.
CostKit: an adversarial bug hunt that turned up problems in billing, auth, team invites, and the PDF pipeline. Five were bad enough to fix on the spot. I triaged the remaining thirty by severity intending to get to them later, then got annoyed and fixed almost all of them the same day. The test suite went from 512 to 560.
Abner: the tracking snippet my MCP server generates was embedding the wrong identifier, which meant several of my sites were recording nothing at all. Every site I checked was affected. Fixed, and the sites are reporting again.
None of that is a launch. All of it is work.
Blindfold ate the rest
Blindfold is my ad and tracker blocker for iPhone and iPad. It went live in late July. The Safari content blocking has worked from day one. The feature called Everywhere, which blocks in every app rather than only in Safari, is the part that has been fighting me since.
The first version used a packet tunnel to do DNS filtering. That is the standard trick: you declare a VPN, route DNS through yourself, and drop the requests you do not like. App Review rejected it under guideline 2.5.1, unapproved use of NEVPNManager. Fair enough, it is not really a VPN.
So I migrated it to a DNS proxy provider, which is Apple’s sanctioned API for exactly this, and shipped it. Then I read the technote properly. DNS proxies only run on supervised devices, which means devices enrolled in mobile device management. On a phone somebody bought at a store, the extension simply never activates. I had shipped a feature that could not work, to anyone, ever. That is the single worst thing in this post and it is entirely my fault for building against an API before reading its deployment requirements.
That is still the version on the App Store today, toggle and all, which is not a sentence I enjoy typing. I have built the version that disables it honestly, with a “being upgraded” explanation instead of a switch that pretends, and it is uploaded. It is not attached to a release yet, which means as things stand right now it is doing nobody any good. That is the top of my list this week.
The replacement is Apple’s URL Filter API, and this is where it got out of hand. Filtering URLs system-wide means the device has to ask a server whether a URL is on a blocklist. Apple, reasonably, does not want you learning which URLs a person is visiting. So the query goes through private information retrieval: the device asks in a way that is mathematically incapable of revealing what it asked.
That is a lovely property. It also means that if you want to ship this feature, you do not get to call an API. You have to run the service, permanently, for as long as the app exists.
So I now operate three pieces of infrastructure for one ad blocker:
- A PIR service on Cloudflare Containers at pir.getblindfold.app, serving a processed blocklist of 91,294 entries plus a Bloom prefilter so the device can skip the expensive query most of the time.
- An Oblivious HTTP gateway at gateway.getblindfold.app, because Apple’s relay forwards encapsulated requests that something has to decapsulate.
- A Privacy Pass issuer on its own subdomain, so devices can authenticate without being identifiable.
Every one of those is a thing I had never deployed before. On top of that, Apple’s Identity and Trust process has to approve the URL filter configuration itself, and until it does, a release build will most likely fail validation. That went into review in late July and I am still waiting.
I also built a self-test into the app, because the honest problem with a privacy-preserving blocker is that neither of us can see it working. Blindfold now fetches a canary host that should be blocked and a control host that should not, and reports back. If the blocked one fails and the control succeeds, it tells you “blocking confirmed.” If it cannot tell, it says so instead of showing you a green light it has not earned. Last week I fixed a related lie: a filter that had failed to start was displaying as “starting,” forever.
And I know exactly where this breaks. The PIR service holds each user’s evaluation keys in process memory, which caps the whole deployment at roughly 1,300 users and loses every key on restart. That is a problem I get to have if the app succeeds, and I would like to have it.
A server I have to run forever also has to be paid for forever, and that changes what Blindfold has to cost. The original prices assumed an app that phones nobody. This one has a standing infrastructure bill for as long as a single customer is using it, and a lifetime purchase has to actually cover its share of that. I have the new numbers worked out and I will publish them properly, on the app’s own site, rather than burying a price increase in the middle of a post about being tired.
Blindfold 1.0.1 is not out. I pulled it back from review myself once I understood the DNS proxy was a dead end, because shipping it would have meant shipping the broken thing twice. A cold-launch crash fix has landed since, and both of them are now waiting on the same release. Version 1.0 is still live and still blocking in Safari.
Sitting in the queue
Things that are finished but not in your hands:
Léelo 1.1: submitted Sunday, waiting for review. Importing an EPUB went from 212 seconds to 2.9. Word explanations now use guided generation, cache their results, and recover when the on-device model refuses a word instead of just failing. I also deleted 23,000 lines of dead code.
Just Habits 1.5: build 11 is uploaded and attached, release notes are written in fifteen languages, and it is blocked on me deploying a CloudKit schema change to production. It adds per-habit notes, and notes that do not sync are worse than no notes, so it waits. It also carries eighteen performance fixes from a beachball audit.
Train531: a rest timer that runs as a Live Activity, a watch wire contract that stops a stale tap from logging a lift that never happened, and a sync fix so a stale remote snapshot can no longer erase a workout you are in the middle of. Not submitted yet.
SongSplit AI: bulk splitting for whole folders on macOS. Not submitted yet.
What is next: Message Album
The new one is Message Album, at messagealbum.com.
It exports a single Messages conversation from your Mac into a private, searchable offline archive. Not a backup. A backup restores a device; it does not hand you the conversation. Message Album gives you one folder with an index.html you open in any browser, both sides of the thread, every photo and video and voice note that is actually on the Mac at original quality, JSON and Markdown transcripts, and a manifest of SHA-256 checksums so you can prove in ten years that the copy in your hands is the one that came out. Anything iCloud has not pulled down yet is listed as an explicit warning rather than quietly left out.
It reads your Messages library read-only and never modifies it. It can also read a conversation out of an iPhone backup made with Finder, encrypted or not. There are no accounts, no telemetry, and no network requests at all. It will be sold directly from its own site, because reading your Messages library requires Full Disk Access and that is not something a Mac App Store app can have.
I am not putting a price or a date on it yet. The price is still literally a placeholder in my own config file, and I would rather tell you nothing than tell you something I have to walk back.
On being tired
Here is the count. Thirty-six products this year, and this is week thirty-three. More products than weeks. Still on pace for fifty-two.
I should feel fine about that, and mostly I do. But the number that goes up is not the number that costs anything. Shipping the thirty-seventh app is a known quantity now. Keeping thirty-six of them alive is the part nobody warned me about, and it is the part that does not show up in a launch post. Soundshine had a corruption bug. Blindfold needed a server. Abner was silently recording nothing. Portie had thirty-nine pages that should have been one. None of that is glamorous, all of it is real, and every week I spend on it is a week the counter does not move.
When I wrote the challenge post in February, the thing I was trying to fix was that I never finished anything. My hard drive was a graveyard of prototypes. That problem is solved. Thoroughly. The new problem is that finishing something is not the end of it, and I built a scoreboard that only rewards the start.
So the rule changes, slightly and honestly: a substantial update to something I already sell counts as that week’s ship. Not a version bump, not a typo fix. Something a user would notice.
By that standard last Tuesday was covered. Soundshine 1.3.3 and Record 1.1 both went out the Friday before it, and twenty-four fixes plus a corruption bug is not a rounding error. Today is not covered, and I am not going to argue otherwise. Everything I finished this week is infrastructure and websites. Not one line of it changed an app that somebody already has installed. One out of two, under a rule I wrote myself, which is about as gentle a grader as I am ever going to get.
I would still rather have an accurate rule than a heroic one I quietly break and then apologize for.
The other thing I am doing is resting, which I say here mainly so that I actually do it.
Next Tuesday I intend to have Message Album out. If I do not, I will tell you that too.