AllMeetingsDecisionsProduct

The recording bug we fixed three times

The timer moved. The meter reacted. Finish worked. For six days after launch, not one recording completed the path to a transcript.

Leon studies a monitor while Mira arranges three white cards on a table during an evening work session.

Scripta shipped browser recording on August 13, 2026. It completed zero recordings until August 19.

The recorder itself looked healthy. It obtained audio, displayed a timer, moved the level meter, and let the person press Finish. Then three independent faults stopped the recording at successive boundaries. Each fix exposed the next one, so “we fixed the recording bug” was wrong twice before it was true.

The visible feature was only the first stage

A browser recorder can succeed while the feature fails.

The full path was:

  1. capture audio in the browser;
  2. upload it to private storage;
  3. register an authorized recording row;
  4. read the stored audio for transcription;
  5. send one batch request to xAI Grok STT;
  6. save a usable transcript.

The launch checks proved the first step. The product promise depended on all six.

Fault one: the container and bucket disagreed

The browser selected audio/webm;codecs=opus. The private storage bucket allowed audio/webm.

Those describe the same base media type, but the bucket used an exact-string allowlist. The codec parameter made the captured file fail after the meeting had already ended.

The interface translated the failure into a generic connection message. That sent everyone toward Wi-Fi even though the network was not the problem.

The durable correction was to normalize the media type at the storage boundary while preserving the recorder's actual type where it remained useful. The boundary needed the base type; the diagnostic record could retain the detail.

Fault two: valid SQL named the wrong column

After the MIME fix, storage authorization failed.

An RLS policy used an unqualified name inside an EXISTS expression. SQL resolved it to the wrong relation instead of storage.objects.name. The policy parsed and deployed. It simply denied the intended operation.

Qualification can look like style until the query crosses a security boundary. There, it is correctness. The policy needed to say which relation owned the path being checked.

Fixing that revealed the final fault.

Fault three: two provider fields belonged together

The audio now reached xAI, which refused the transcription request. The request included format without language; the provider required the pair together.

This was not a problem a retry could solve. The request builder had allowed an invalid combination.

The correction was structural: construct the related parameters together so call sites cannot separate them. When two fields form an invariant, the code should make the incomplete state difficult to express.

One generic sentence hid three systems

All three failures looked like a connection problem to the person recording. That message was meant to be friendly. During a new integration, it destroyed the only clue about which boundary had failed.

A storage allowlist, an RLS policy, and a provider request do not have the same remedy. Flattening them into one sentence makes support calmer and debugging slower.

The better launch posture is to surface the upstream error safely and preserve it on the recording. A person should know whether their audio is saved and whether transcription can be retried. Engineers should know which system refused the operation.

Monitor outcomes, not healthy components

The missing metric was simple: how many recordings reached a completed transcript?

Route health would not answer it. Recorder initialization would not answer it. Upload attempts would not answer it. A feature that crossed browser, storage, database policy, and an external model needed one end-to-end success definition.

That lesson applies beyond recording:

  • test the exact media type a real browser emits;
  • exercise policies against the deployed relation names;
  • test external requests with valid parameter combinations;
  • keep failed recordings recoverable;
  • monitor the user outcome, not just each component's availability.

Unit tests remain valuable, but the deployed path must run at least once before a multi-system feature can be called working.

The record begins after capture succeeds

Once transcription completes, the transcript still arrives as material for review. A person promotes it to the meeting. The minutes assistant drafts from it, and consequential extractions point back to quotes verified in the transcript.

That downstream care is irrelevant if the source never survives the pipeline. Meeting notes can only show their work after the recording system has produced evidence worth reviewing.

The six-day failure changed the engineering definition of “recording works” from a moving meter to a completed, recoverable transcript. Nothing of Scripta's ever joined the call; the failures lived in the path that began after browser capture.

FAQ

Why did the recorder appear to work?
Browser capture succeeded, so the visible controls behaved normally. The failures occurred later at upload authorization and transcription boundaries.
Why not upload every MediaRecorder chunk separately?
Later chunks are not necessarily independently decodable. Sending chunks is not a substitute for a streaming transcription protocol.
Did a Scripta bot join the meeting?
No. The person recorded in their own browser; nothing of Scripta's joined the call.
What was the most important diagnostic change?
Judge success by a completed, usable transcript and surface boundary-specific failures instead of translating everything into a connection problem.