illogicalproject

Detent Coffee

Scan any bag. Brew it right. Built solo with Claude — designed, coded, tested, and shipped to the App Store.

Recipe screen — grind 6.5, 196°F, 22g dose, 1:15 ratio
Download on the App Store — Free
20+
Grinders supported
6
Brew methods
2
Platforms shipped

01 — Design & Code

Claude didn’t assist.
Claude built.

Detent is a solo project. One founder, one vision, and Claude as a full-stack collaborator — from first wireframe to App Store approval. The entire web frontend lives in a single file (~1,700 lines). The design system, component architecture, color tokens, and SwiftUI views were all shaped through conversation with Claude in Cowork.

The workflow was iterative and honest. Claude would propose an implementation, I’d push back on a design decision or flag a constraint, and we’d work through it together. It never felt like prompting. It felt like pairing with a senior engineer who happened to know both Next.js and SwiftUI.

The design language — Scandi-Zen minimalism, kintsugi gold accents, the Daruma red mark, Newsreader italic for headlines — started as a brief and evolved into a full design system with strict token rules, accessibility requirements, and documented hard constraints.

Claude SonnetCoworkNext.js 16SwiftUISupabase

“The CLAUDE.md files in each repo aren’t documentation. They’re institutional memory. Every constraint, every incident, every hard-won architectural decision lives there — and Claude reads them before touching anything.”

Detent home screen — Dial in today's cup

02 — Web First

Ship the web version.
Then build the app.

Before writing a single line of Swift, Detent ran as a full web application. This wasn’t a prototype. It was the real thing — same Supabase backend, same Claude API routes for scanning and grind recommendations, same Fellow integration.

Web is faster to iterate. React state is easier to inspect than SwiftUI’s. Auth bugs surface in a browser before they cost you a TestFlight cycle. By the time the iOS app was being written, the cross-user privacy leak in the database view, the silent no-op delete bug, the recipe loading state that could hang forever, and a handful of API timeout edge cases were already fixed. The native app inherited a hardened API contract — not a second first attempt.

1

Next.js web app

Full product on web: auth, scanning, cabinet, Fellow sync. Real users, real feedback.

2

Backend hardening

Privacy bugs, timeout handling, delete guards, bounded fetch wrappers — all found in the browser.

3

SwiftUI native app

Built against the locked API. Ported the validated logic. Focused on native feel — not rethinking the product.


03 — Image Recognition

On-device first.
Cloud when it counts.

Scanning a coffee bag sounds straightforward. It isn’t. Labels vary wildly — kraft paper, matte black, dense text, reflective foil, minimal type set in a 9pt serif. Getting reliable structured data from an image requires a layered approach.

Before sending anything to the cloud, Detent evaluates the image on-device using Apple’s Neural Engine. iOS Vision and Core ML extract text, assess orientation, and run a confidence pass — all without a network round trip. Blurry captures get flagged before they waste an API call. The scan flow stays fast even on a slow connection.

When the image clears the on-device threshold, it goes to Claude Vision via the scan API. The prompt returns a typed result — roaster, origin, process, roast level, flavor notes, body, acidity, finish scores. The response shape is locked so both web and iOS parse it the same way. Low-confidence scans surface a retry panel rather than failing silently.

Apple VisionCore MLNeural EngineClaude VisionAVFoundation
Scan result — Dark Jawn with flavor notes and descriptorsBrew method selection

04 — Testing

Real beans.
Real edge cases.

Unit tests covered the logic. Real-world testing covered everything else.

The cabinet delete that “worked” but didn’t — caught when a coffee came back after a page refresh. The recipe loading state that froze forever — caught by tapping back mid-fetch. The cross-user data leak in the database view — caught by JWT impersonation in the Supabase SQL editor, checking whether one user’s session could see another’s cabinet. That one would have been bad in production.

Every API route has a bounded timeout and a typed error state. Every Supabase write chains .select() so a zero-row response can’t masquerade as success. For iOS, testing meant physical hardware — the simulator won’t show you an icon that looks wrong under the squircle mask, or OLED smear on a near-black surface.

“The most useful test: impersonate one user’s JWT, query the cabinet view, verify you see zero rows from another user’s account. One SQL block. Caught the privacy leak before it ever shipped.”

Cabinet — real coffees scanned during testing

05 — Fellow Integration

Bluetooth was a
beautiful dead end.

Fellow makes an excellent coffee machine. It also, in theory, supports Bluetooth connectivity. Connecting to it from a third-party app sounded like the obvious path — scan a bag, generate a brew profile, push it directly to the machine via BLE. Clean.

It was not clean. Web Bluetooth on Safari is restricted. The Fellow BLE protocol isn’t documented. Weeks of investigation into navigator.bluetooth, custom UUIDs, and BLE debugging tooling got close but never reliable. The architecture kept drifting toward a fragile hack.

The pivot: Fellow’s cloud API. A GitHub open-source library surfaced a cleaner path — authenticate as the user, call their cloud endpoint, let Fellow’s own infrastructure push the profile to the machine. More reliable, no BLE dependency, works the same way on the couch or at the office. A GCP Cloud Function bridges the gap. The machine receives the profile within ~15 seconds. All the BLE code is gone.

Fellow Cloud APIGCP Cloud FunctionsOpen SourcePython
Ready when you are — brew profile sent to Fellow

06 — Recommendation Engine

Learning to recommend.
Not just calculate.

The grind and brew recommendation isn’t a lookup table. It’s a structured prompt with Claude Sonnet behind it. Getting it right required understanding two things that weren’t known going in: how recommendation systems actually work, and how Fellow machines actually behave.

Recommendation engines are preference inference problems. You have a set of attributes — roast level, process, origin, elevation, acidity score — and you need to map them to settings a person will find good. The challenge is that “good” is contextual. A light Ethiopian pour-over brewed at the same ratio as a dark Indonesian espresso is going to be bad. The model needs to know the rules of the domain before it can reason about exceptions.

Building the prompts meant learning the Fellow Aiden’s actual parameter space: temperature range, pre-infusion behavior, how the machine interprets ratio inputs, the difference between a bloom step and a full infuse, where it has real flexibility versus where it doesn’t. That knowledge came from documentation, community sources, and a lot of test brews.

“The model knows what it doesn’t know. A bag with no origin or process data returns a sensible conservative profile — not a confident wrong answer.”

Brew method selection — Drip, Pour Over, Espresso, AeroPress, Cold Brew, French PressRecipe screen — grind 6.5, 196°F, 22g dose, 1:15 ratio, 3:30 brew time

Resulting stability

Detent shipped to the App Store. Free. Scan any bag and get a grind setting calibrated to your equipment and a full brew recipe for your method. Drip, pour over, AeroPress, French press, espresso, cold brew. 20+ grinders supported. Fellow Aiden integration live.

The web build preceded the iOS app by months. Shipping it first meant the scan pipeline, the grind model, and the Fellow integration were all battle-tested before a single line of Swift was written.

The build produced more than an app. The web-first discipline, the Claude-as-memory pattern, the layered on-device-then-cloud OCR approach, the open-source pivot on the Fellow connection — each one is a repeatable method, not a one-time hack.

Download

vol. 4 · ix · brian fenn · pdx · finis