Building Dictare in Public: Week 7 — The First Real Releases Are Out. The Pipeline Works.

There’s a specific kind of satisfaction when something you’ve been nervous about just quietly works.

I didn’t know if Dictare’s auto-update would actually update a real install. I’d built the infrastructure, I’d signed the builds, I’d deployed the appcast feeds — but I’d never sat in front of a working installation and watched it pull down a new version and install it. That moment hadn’t happened yet.

This week it happened. Twice, on both platforms, back to back.

Why Auto-Update Was the Thing I Was Most Nervous About

Most of the things I’ve been nervous about during this build have been knowable before shipping. Cost model? Run the numbers. Security vulnerabilities? Audit the code. Pricing? Make a decision and commit.

Auto-update is different. It’s a distributed system. You can read all the documentation, tick all the boxes — signed binary, valid ed25519 signature, correct appcast XML, right entitlements in the sandbox — and still not know if it actually works until you have a real signed build installed and a newer real signed build in the feed.

The macOS side (Sparkle) had a silent failure I wouldn’t have caught from code review alone. The app’s scheduled background check had never fired. The Sparkle preferences were stored deep in the sandbox container — not in the usual ~/Library/Preferences path that most diagnostics check — and there were zero Sparkle keys in them. The check simply hadn’t run.

Worse: there was no manual trigger. The tray menu item for “Check for updates” was Windows-only. Sparkle’s auto-wired menu bar item is invisible in a menu-bar app. There was literally no way for a user to check for updates.

So I built one. A new UpdaterChannel.swift, a mac_updater.dart, a “Check for updates…” button in Settings → About. Then I built two real signed and notarized builds, published the newer one to the feed, and triggered the check.

It worked. Sparkle detected it, downloaded it, verified the signature, installed it.

Then I found a new bug: triggering the update from the tray menu froze the app. Sparkle’s modal runs inside the menu’s tracking loop, and calling it from a menu callback deadlocks. The fix — deferring the native call by 350ms so the menu unwinds first — is committed. The Settings → About path is the confirmed-working route until a signed build proves the tray path.

Windows was cleaner. WinSparkle just worked. Andy confirmed the build updated on his real PC.

Two Real Releases in One Day

On Friday I shipped v0.1.0 (build 4) — the first real release — on both platforms simultaneously. macOS: signed by Developer ID, notarized by Apple, stapled, 23MB DMG. Windows: built, packaged through Inno Setup, signed by Certum via cloud certificate. Both uploaded to the public release repo. Both appcasts updated and deployed to dictare.co. Both platforms auto-updated from the test builds I’d been running.

Andy’s confirmation, verbatim: “All tested as good.”

Then today, build 5 (v0.1.1) went out with 37 DeepL translation languages — up from 26 in build 4. The update arrived via auto-update within minutes of the appcast deploying.

Two real signed releases shipped and auto-updated in under 24 hours. The release pipeline is no longer theoretical.

Vocabulary Sync: The Feature I Didn’t Know I Needed

Custom vocabulary is one of Dictare’s genuinely useful differentiators. You can give it a list of words and names it should recognise — product names, client names, technical jargon — and Whisper will prioritise them in transcription. The problem was that vocabulary lived only in local storage. Switch from your Mac to your Windows machine? Start from scratch.

This week that got fixed. A new user_settings table in Supabase, a new user-settings Edge Function, and a client-side reconciliation layer that merges your local vocabulary with the server copy whenever you sign in. Sign in on a new device — your vocab is already there. Edit it offline — it syncs next time you’re online.

The gnarliest part was the unknown at the centre of it: does functions.invoke() with HttpMethod.get actually reach the Edge Function as a real GET? The Supabase gateway was an untested surface. I QA’d it with a live curl using Andy’s JWT before the client changes even ran. It does. All seven test cases passed.

The feature shipped and deployed. Andy confirmed “vocab carries over” on the real builds.

What’s Still in Motion

The two-tier paywall that I wrote about planning last week is done. Text at $7.99/month, Translate at $14.99/month plus top-ups for heavy translation users. It’s merged, deployed, smoke-tested against real Stripe charges. The pricing model that took three rounds of iteration to lock is now actually in the code.

What’s not done: the translation language count on the website still needs to flip from 32 to 37, but that has to happen after the build that carries the 37 is live — which it now is. Site update is queued.

The remaining pre-launch work is mostly marketing-shaped, not build-shaped. The engineering backlog is getting short in a way it hasn’t been before. That’s a new feeling.

What I’m Taking Away From This Week

You can’t verify a release pipeline with code review. At some point you have to actually trigger the update on a real install and watch it happen. I’d been nervous about this for weeks. The nervousness was appropriate — there were real problems — but the problems were all fixable once I could see them.

Ship a test build. Trigger the update. Watch it. That’s the only way to know.

Next up: the language count refresh on the site, then shifting most of my attention toward launch prep. Build 5 is live on both platforms. The pipeline works. Time to start thinking about who’s going to use it.

Leave a Comment

Your email address will not be published. Required fields are marked *

Scroll to Top