July 29, 202610 min read

A Private Meeting Recorder That Runs Entirely in a Browser Tab

Free spatial notes

Put this workflow on an infinite canvas instead of another linear doc.

Start in the browser with no account. Create an account only when you want to sync what you made.

The Idea

Our public demo page let visitors draw on a canvas. The welcome panel promised "record audio with transcription," which the demo could not actually do. That gap had been sitting there for a while.

The obvious fix was to wire the demo up to our production transcription backend. We did not want to do that, for two reasons. First, an unauthenticated endpoint that accepts arbitrary audio and returns transcripts is a service you are now operating for the entire internet. Second, and more importantly, it would waste the actual opportunity.

The interesting version is a recorder where the audio never leaves the browser. Not "we promise not to look." Structurally incapable of uploading, because there is nothing to upload to.

Here is what that took.

The Architecture

Three pieces, all client-side.

Capture. MediaRecorder for microphone audio. This is the boring, well-supported part — MediaRecorder has broad browser support and has for years.

Transcription. Whisper running in the browser through Transformers.js, in a worker so the canvas stays interactive. WebGPU when available, WebAssembly as the fallback. The fallback is not optional. WebGPU support is still uneven across browsers and devices, and a recorder that only works for some visitors is worse than no recorder, because the failure is invisible until someone has already recorded five minutes of audio.

Text generation. For summaries and action items, a second model — WebLLM running a roughly 0.9 GB Llama variant.

Our production app already had most of this: a Whisper worker, a persistent model cache, WebGPU/WASM routing, and cleanup logic. The correct move was to extract the reusable local-ASR pieces into a shared package rather than write a second transcription implementation for the demo. Two implementations of the same hard thing means two sets of bugs and one of them is always the stale one.

Problem One: You Cannot Hold Both Models at Once

This is the finding that cost us the most time and is the least documented anywhere.

Whisper loads into GPU memory. It transcribes. Then the user clicks "Summary," and WebLLM tries to load a 0.9 GB model into GPU memory.

If Whisper is still resident, you exceed available VRAM on machines that could comfortably run either model individually. The failure is not graceful. It surfaces as an allocation error, or a hang, or in some configurations the browser tab simply dying — and it happens on perfectly capable laptops, which makes it look random.

The fix is explicit: release Whisper from GPU memory after transcription completes, before loading the text model. Not "let garbage collection get to it eventually." An explicit disposal, awaited, before the next load begins.

This means the pipeline has a real state machine rather than two independent async loads:

  1. Load Whisper
  2. Transcribe
  3. Dispose Whisper, await confirmation
  4. Load WebLLM on first text action
  5. Run text actions, keeping the model resident for subsequent ones

Step three is the one everybody skips, and you will not catch it in testing on a workstation with plenty of VRAM.

Problem Two: The Cross-Origin Model Cache

Our marketing site and our app run on different origins. The demo lives on the marketing site. The production recorder lives in the app.

Browser storage is partitioned by origin. So the model cache does not carry over.

A visitor downloads the Whisper model on the demo, is impressed, signs up, opens the app — and downloads the same model again. Hundreds of megabytes, twice, for the same person, in the same session, on the same machine.

There is no clever workaround. This is the correct security behavior and it is not going to change. The options are real architectural choices:

  • Accept it and minimize the cost. Use Whisper Tiny in the demo. The download is small enough that duplicating it is annoying rather than disqualifying.
  • Host the demo experience on the app origin and launch or embed it from the marketing page, so the cache is shared.

We took the first option for the initial version because it ships in days rather than weeks. But if converting demo users into app users is the primary goal, the second is the right answer, and the decision should be made deliberately rather than discovered later.

The general lesson: if your product involves a large client-side model download, your origin layout is a product decision, not an infrastructure detail.

Making It Spatial Instead of a Widget

The first design put recording in a panel — a dock with a waveform, a timer, and a transcript pane. Functional, and completely beside the point.

The whole argument for OmniCanvas is that a transcript belongs on a canvas next to the diagram you drew during the meeting, not in a separate transcript inbox. A demo that puts the transcript in a side panel demonstrates the opposite of our thesis.

So the demo produces a recording card on the canvas. It appears near the visible center when recording starts. It shows live status. It fills with timestamped transcript lines that seek playback when clicked. It drags, resizes, renames, and deletes. It pans and zooms with the canvas, because it uses the same transformed HTML overlay as the production app rather than a demo-specific imitation.

Reusing the production card meant the demo inherited months of fixes for drag behavior, playback, transcript seeking, and zoom interaction. Building a lookalike would have meant rediscovering all of them.

The AI Actions, and the Ones We Hid

The demo supports summary, action items, key decisions, meeting minutes, standup recap, agenda, entity extraction, custom prompts, and mind maps generated as real canvas elements. There is a local transcript chat scoped to the recording.

We deliberately hid two production actions:

  • Identify Speakers, because we have no local acoustic diarization. Offering it would produce a confident guess dressed up as an analysis.
  • Find Related Notes, because a demo visitor has no notebook to relate anything to.

An action that technically returns output but cannot honestly work is worse than a missing action. It teaches the visitor that our AI features produce plausible nonsense, which is the exact opposite of the impression the demo exists to create.

The Privacy Design, Stated Honestly

Recording and transcription are always in-browser. For the text actions, we offer a choice on first use:

  • On this device — downloads the browser model, nothing leaves the machine, maximum privacy
  • OmniCanvas-hosted — sends the transcript, never the audio, to our own hardware, faster and higher quality

The rules we set for the hosted path, because "we host it ourselves" is a claim that has to survive contact with the implementation:

  • Cloud failover disabled. Our routing layer supports silent failover to third-party providers by default. For this route it is off, and responses marked as cloud-served are rejected server-side rather than trusted.
  • Response caching disabled. The optional cache would store transcript content in shared storage.
  • Owned nodes only, metadata-only logging.
  • Same-origin validation, size limits, rate limiting backed by the database, timeouts.
  • The client sends action IDs, not arbitrary prompts. Otherwise the demo is a free general-purpose LLM endpoint with our name on the bill.

And in the interface: a persistent badge showing which provider is active, and never silently switching between them. If the on-device model fails, the demo says so. It does not quietly send the transcript to a server while displaying a privacy promise. That single behavior is the difference between a privacy feature and a privacy claim.

What Shipped

Spatial recording cards, in-browser Whisper transcription, playback, timestamps, resize, and downloads for both audio and transcript. Nine AI actions with explicit provider choice. Mind maps as real canvas elements. Warnings before leaving with an undownloaded recording.

The clearest thing we learned: the browser is a genuinely viable runtime for this. The constraints are not CPU or model quality. They are GPU memory and origin boundaries — two things that barely come up in server-side machine learning and that will decide whether your in-browser feature works.

Try the recorder on the demo canvas — no account, no upload.

30 days free. No credit card required.

Try the Meeting Notes template in OmniCanvas

Open a local canvas with the Meeting Notes layout ready to go — no setup or account needed.

Use the Meeting Notes Template

Start locally, then sign up only to sync — or explore the interactive demo first.