Teaching Greenhouse to preview vault audio in-app
Greenhouse is a desktop app that enforces a creative-process cadence: capture an idea, let it mature, work it, touch it, eventually vault or release it. One gap kept nagging at me - when an idea or project has an audio bounce sitting in its folder, the only way to hear it is to click “Show in Finder” and go find it yourself. I wanted something closer to macOS Quick Look: hit a button, hear the sketch, decide.
Before writing any UI code I wrote a spike. Greenhouse is a Tauri v2 app, and I didn’t actually know how a Tauri webview is allowed to touch local files - I assumed it would be blocked by default, but I wanted to find out exactly how blocked, and what it would cost to open the door a crack.
The security wiring was greenfield
Turns out the app exposed zero file bytes to the webview. No asset protocol, no filesystem plugin, and a Content-Security-Policy locked to default-src 'self'. The only thing touching the filesystem from the frontend was a “open this folder in Finder” button, which just hands a path to the OS - the webview itself never sees file contents.
That’s actually a good sign for a security-conscious app, but it meant rendering audio in-app wasn’t a small tweak. I’d need to widen the CSP, which felt uncomfortable right after I’d finished a security-hardening pass on this exact app.
Proving it before building it
Rather than debate the tradeoff in the abstract, I built a throwaway proof of concept. Tauri has a built-in “asset protocol” - enable a Cargo feature, flip a config flag, and the webview can request local files through a special URL scheme instead of a network fetch. The interesting part is scope: you don’t want to hand the webview access to the whole filesystem, just the folders it actually needs.
Since Greenhouse’s vault location is chosen by the user during onboarding - it’s not a fixed path baked into the build - the usual “list allowed folders in the config file” approach doesn’t quite fit. Tauri has a runtime API for this: you grant access to a specific directory in code, right at the moment you learn what that directory is. So the vault root gets scoped into the asset protocol exactly once, the moment it’s set or loaded at startup - never broader than that.
I widened the Content-Security-Policy too, but narrowly: I only touched the two directives that load media (img-src, media-src), and left the default policy exactly as strict as it was. Small, explainable diff.
Then I actually drove the app to prove it worked - not just “it compiled,” but a script that opens the real app, captures a real idea over the same IPC layer the UI uses, drops a real WAV file into its folder from outside the webview (simulating a user saving a bounce there), and clicks through to preview it. Then I asked the live audio element what state it was in: fully loaded, correct duration, no errors, served through the asset protocol’s own URL scheme. One thing I learned only by testing on the real macOS build: the actual URL Tauri uses on macOS (asset://localhost/...) doesn’t match what most of the docs I found show, which describe the Windows/Linux form. Small thing, but it’s exactly the kind of detail you don’t get from reading documentation - you get it from running the thing.
From spike to shipped feature
With the approach de-risked, I built the real version: a “Files” panel in the project detail view, a Preview button next to anything that looks like audio, and a lightweight player that opens in an overlay. I kept “Show in Finder” as-is rather than replacing it - some people will still want the real file, not a preview.
One small, satisfying bug: my first draft had the audio auto-play the moment you opened the preview. My automated accessibility tests started hanging on that screen, and untangling why led me to a rule I already half-knew and had never quite internalized: don’t auto-play audio. It’s a real accessibility guideline, not just a testing inconvenience. Dropping autoplay fixed the test and made the feature better at the same time - the kind of coincidence that isn’t really a coincidence.
I scoped this first version to audio only, on purpose. Images and PDFs would reuse the same plumbing, but I hadn’t tested them, and Greenhouse’s whole reason for existing is protecting people’s creative process from feature creep - including my own.
Related reading
Two notes nobody ever saw
Greenhouse saved a capture note and a handoff note faithfully, and showed neither: one had no field in the wire type, the other had a working command nobody called.
The scan that offered to import itself
An import scanner that found the app's own scaffolding, then re-offered a folder it had just imported - two versions of the same missing conversation between layers.
Adding a native macOS menu to Greenhouse broke Cmd+Q, and my test harness couldn't press Cmd+N either
set_menu replaces the default menu, it doesn't extend it - taking Quit, Hide, and every Edit shortcut with it. Then the WebDriver actions endpoint turned out unable to press a key.