Case study
Cira: designing trust into a dictation app
A macOS menubar dictation app I took from blank repo to a paid, live backend in five weeks, where most of the hard problems turned out to be about trust, not transcription.
Solo founder · May 2026 to now · macOS 14+, SwiftUI, Supabase, Stripe · In daily personal use
C
Hotkey
01
Recording
02
Transcribing
Cleaning up
Learning
03·04
Inserting
05
One dictation, step by step through Cira’s menubar. The numbered steps are where trust broke; each challenge below picks up one of them.
Hero video: one dictation, from the hotkey to text landing in Slack
At a glance
Role
Solo founder: product, UX, brand, PRDs. Designed and built with AI coding tools, me directing and reviewing every decision.
Team
Me, plus separate AI agents for app, backend, admin, website and marketing, coordinated through a written sync log I owned.
Timeline
First commit 30 May 2026. Core code through 7 July. Landing page live 1 September. In daily personal use, with a UI redesign next.
Platform and stack
macOS 14+, Swift / SwiftUI / AppKit (SPM). Deno on Supabase Edge Functions and Postgres. Stripe. Static web, vanilla-JS admin.
Status
Dictation, billing, trial and Memory v1/v2 are live in production. Memory v2.5 is finished on dev but held for legal review.
Not publicly launched yet. I use it every day; the next step is a UI redesign.
The friction wasn’t accuracy. It was uncertainty.
Typing is slower than talking, but the good dictation tools either wanted your own API key or cost around $15 a month, and none handled Persian well. Using them, I found the friction wasn’t mainly accuracy.
Is it listening, what is it waiting on, where will the text land?
Cira was my attempt to design that uncertainty out, and to build it for real to see if the design held up.
PRD snapshot
One north-star metric, three non-goals
Users
English-speaking professionals who write faster by voice: the main market. Persian speakers are a differentiator, not the design driver.
Goals
No setup and no API key. Text pasted into whatever app you’re in. It should learn how you write over time.
Constraints
Solo, no budget for paid tooling, and a privacy promise (“your text isn’t stored”) that every later feature had to keep.
Non-goals
×
iOS
Assessed, not started.
×
A “Hey Cira” wake word
Researched, then deprioritised.
×
Speed or accuracy claims without a benchmark
A written brand rule.
North-star metric · post-paste edit rate
What you rewrite by hand
What Cira pasted
↓
Lower means closer to your voice
Computed on the Mac, so no text leaves the device to measure it.
Business model · Cira Pro
Setup
No API key
Free trial
8 min
Pro, monthly
$8
Raised from $6 when Memory v2 shipped, to cover its cloud cost.
Source: board, ToS draft, marketing sync.
PRD scoring table from the audio-ducking PRD: appeal, usefulness, difficulty (translated)
Every feature got a PRD and a scoring table before any code.
Challenge 01 of 05 · Hotkey
The anti-abuse fix that would have broken every user
Situation
A free trial with a device key generated on the client can be farmed: a script can create unlimited devices and get unlimited trials.
Why it was hard
The textbook fix is Apple’s App Attest. We built it end to end, client and server, with a hybrid “attest or fall back” trial.
What happened
Testing showed DCAppAttestService.isSupported is always false on native macOS: across Developer ID, provisioning and Full Security, every Mac failed. It’s an iOS-only API.
Shipping it would have silently cut every Mac user’s trial from 8 minutes to 2.
App Attest check · native macOS
Developer ID
→ false
Provisioning
→ false
Full Security
→ false
8:00
Trial as designed
2:00
Every Mac, had App Attest shipped
It’s an iOS-only API. Every Mac would have failed the check and fallen back to the 2-minute trial, silently.
The decision
Shipped
Atomic rate limits in Postgres
Per device, per account and per IP, so a script can’t farm trials.
Parked
App Attest, end to end
Kept off production, preserved on a branch for a future iOS app.
Outcome
Trial abuse was covered for a soft launch with no regression. The lesson I kept: test the platform’s assumptions before trusting a well-known pattern.
Challenge 02 of 05 · Recording
Bluetooth headphones made music sound like a phone call
Situation
Dictating with AirPods dropped the music to call quality, and it stayed there afterwards.
Why it was hard
Opening a Bluetooth mic forces the headset from its high-quality stereo profile into a mono call profile, and macOS is slow to switch back. Users blamed Cira, and they were right to.
Options
Tell users to change their input
Pushes the problem onto them.
Keep the default mic
The bug stays.
Choose the mic per recording
Chosen. Checked on every recording start.
The decision
Check the default input on every recording start and route around Bluetooth. I ranked this P0, ahead of the ducking feature it unblocked.
Input · at every recording start
AirPods
Bluetooth default input. Skipped, so the headset never drops to its mono call profile.
✓
Built-in microphone
Cira records here. Music stays in stereo, in high quality.
Choose input manually…
A manual picker overrides the routing.
Ducking: music lowering while dictating
Settings → Audio
Ducking shipped with three choices: off, lower, or pause and resume.
Outcome
Music stays in high quality. Ducking then shipped with three choices (off, lower, pause and resume), and volume is restored on every exit path.
Challenge 03 of 05 · Learning
“Never stored” vs. “this leaves your device”
Situation
Memory v2 (“Learn from me”) builds a style card in the cloud from your corrections. That clashed directly with the privacy promise on the homepage.
Why it was hard
A UX review produced 15 ranked problems. The top three are below.
UX review · 15 ranked problems
01
The two messages sat side by side and read like a caught lie.
02
Two switches had labels that were the reverse of their actual risk.
03
“Accept all” consent trained users to tap yes blindly.
04–15
Twelve more, ranked below these.
Open design questions
26 of 26 answered
The decision
I answered all 26 open design questions, starting with the claim itself.
Split About-you into three tiers: see it, steer it, control my data. Made redaction fail-closed: if the redactor isn’t sure, the sample isn’t learned from.
The claim, before and after
Before · homepage
“Never stored.”
Before · Learn from me
“This leaves your device.”
After · both screens
“Never stored on our servers.”
Vendor named inline. One switch at the moment of value; the other moved to Settings.
Memory “About you” screen in three tiers: see it, steer it, control my data
About-you, split by how much control each part needs.
Release gates
Memory v2 · cloud learning
Live
Built
Data agreement
Legal review
Production
Memory v2.5
Held
Built
Data agreement
Legal review
Production
Outcome
Cloud learning went live only after a data-processing agreement and a legal review. Memory v2.5 is finished but held off production until it clears the same gates.
Challenge 04 of 05 · Learning
Choosing the model with evidence, not vibes
Evidence
To pick the model that builds the style cards, we ran a blind A/B on 9 synthetic personas with two independent judges. The judges liked Qwen. The numbers around them didn’t.
Blind A/B · 9 personas · 2 judges
13 of 18*
comparisons went to Qwen
Time to a Persian style card · seconds
12–18 s
Shipped
3–5 s
0
5
10
15
20
* Machine judges favour longer answers.
** Qwen leaked a Chinese character into a Persian card.
The decision
Ship gpt-oss-120b, add a guard that strips foreign-script characters from any model’s output, and keep Qwen as the upgrade path if users complain about card quality.
I picked reliability over a judge score I had concrete reasons to discount.
Challenge 05 of 05 · Inserting
Never paste something the user didn’t say
Situation
Cleanup is done by a language model, and its output is pasted straight into whatever app you’re in. A model can add things you never said.
The decision
If the cleaned text contains a URL that wasn’t in what you said, your clipboard or the context, Cira asks before pasting, with a “Paste anyway” button.
Paste guard · for every URL in the cleaned text
Did it come from what you said, your clipboard or the context?
Yes
Paste
No
Ask first: “Paste anyway”
Why it over-flags
Miss
Could leak data
False alarm
Costs one tap
Paste guard toast with “Paste anyway”
A false alarm costs one tap.
Design decisions and system
One hotkey, and a state for every wait
Input
One hotkey, two gestures
A quick press toggles recording; holding it is push-to-talk. The app tells them apart by how you release the key, so there’s no mode to pick up front.
Same key · read on release
Hotkey
Quick press
Toggles recording on or off
Hotkey
Hold
Push-to-talk: records while held, stops on release
Feedback
A state for every wait
Idle grip, then recording with a live waveform, then Transcribing…, Cleaning up… and Inserting…, then a post-dictation chip with Fix.
Cleanup is shown separately so users can see which step is slow.
State 01, dev build
01 · Idle grip
State 02, dev build
02 · Recording
State 03, dev build
03 · Transcribing…
State 04, dev build
04 · Cleaning up…
State 05, dev build
05 · Inserting…
State 06, dev build
06 · Cancelled
State 07, dev build
07 · Chip with Fix
The full state strip, captured from a dev build.
Placement
Out of the way
Idle, the bar collapses to a grip. It can sit at eight screen edges or flush with the notch, which is measured at runtime. Displays without a notch get a pseudo-notch.
Bar flush with the MacBook notch
Bar on an external display, pseudo-notch
Notch measured at runtime; displays without one get a pseudo-notch.
Engineering
Testable design
All presentation logic (bar state, positions, notch geometry, level meter, ducking math) lives in a pure, unit-tested CiraKit target, separate from the UI.
Brand
A circular C, three colours
A circular “C” mark and three colours: Ink, Cream and Icy blue (#1E140B, #FEF7ED, #BCEFFB).
Brand: logo and app icon
The mark and the icon.

The landing page that went live on 1 September.
Process
A PRD before any code
Every feature got a PRD with a scoring table (appeal, usefulness, difficulty) before any code. A board and a one-writer-per-file sync log kept six repos consistent.
Diagram: the multi-repo system, six repos and one sync log
Six repos, one agent per repo, one writer per file.
By the numbers
Less
More
1
15 Jun
Live core
Dictation, trial and Stripe in production, about 2.5 weeks after the first commit.
git log; sync log entry for live Stripe, 15 Jun
2
19 Jun
App Attest kept off
Shipping it would have cut every Mac user’s trial from 8 minutes to 2.
Product sync log, 19 Jun
3
29 Jun
Memory v2 UX audit
15 ranked problems turned into 26 design decisions.
research/2026-06-29-memory-v2-ux-problems-and-decisions.md
4
1 Jul
Competitor research
About 80 raw ideas from 12+ products, ranked down to 60.
research/2026-07-01-competitor-reddit-60-ideas.md
5
5 Jul
Model eval
9 personas; judges preferred Qwen 13 of 18, but its Persian cards took 12–18 s against 3–5 s.
Board card, 5 Jul decision
6
1 Sep
Landing page live
The site went up two months after the engine. Distribution was the real gap.
cira-website git log
The codebase, counted
Six repos
31
days with commits, out of 96 from first to last
$ git log --all --format=%ad --date=short
896
tests: 436 macOS (410 on prod), 407 backend, 53 admin that gate every deploy
$ grep -E ‘func test|@Test’ Tests · Deno.test · admin CI
12.6k
lines of Swift in 165 files, plus 4.0k lines of tests
$ find Sources -name ‘*.swift’ | wc
17 · 30
Edge Functions · Postgres migrations
$ ls supabase/functions supabase/migrations
14
PRDs, 19 research docs and 111 board cards
# cira-product repo
Not measured
Real users, retention, edit rate, latency. The product never launched publicly and had no product analytics: /stats tracks operations only.
The first things I’d measure are post-paste edit rate, trial-to-Pro conversion, and how many dictations happen in the first session.
What I learned, and what’s next
01
Trust is the product
Every serious problem came down to Cira making an invisible decision (which tone, what it recorded, when the trial ends) and showing the signal somewhere the user wasn’t looking.
Next I’d add a persistent personalization indicator in the menubar.
02
Test the platform before the pattern
App Attest was an “obvious” fix that failed on the real platform. So was merging transcription and cleanup into one /dictate call: the two-step path returned its first token sooner, so the merged call stays behind a flag.
03
Ship the boring parts sooner
The engine was live by mid-June, but the landing page didn’t go live until 1 September and there was no public download link. Distribution was the real gap.
04
Put instrumentation in from day one
I wrote a careful privacy-safe metrics spec, but without shipping it I can’t show whether the design works.
05
Where it stands
Cira isn’t stopped. It works well for my own daily use with native macOS components, and the next step is a proper UI redesign before any public launch. I just haven’t had the time yet.
The code is intact and production is running.
