Skip to main content
RapidDev - Software Development Agency
App Featurescommunication-social17 min read

How to Add Voice Note Sharing to Your App (Copy-Paste Prompts Included)

Voice note sharing needs four pieces: the browser MediaRecorder API for recording, WaveSurfer.js for waveform visualization, Supabase Storage for the audio file, and a Deepgram or Whisper Edge Function for auto-transcription. With Lovable or V0 you can ship a working feature in 6–10 hours. Running cost is $0–$5/month at 100 users, rising to $10–$30/month at 1,000 as storage and transcription costs accumulate.

4.9Clutch rating
600+Happy partners
17+Countries served
190+Team members

Feature spec

Advanced

Category

communication-social

Build with AI

6–10 hours with Lovable

Custom build

1–2 weeks custom dev

Running cost

$0–5/mo up to 100 users · $10–30/mo at 1K users

Works on

Web

Everything it takes to ship Voice Note Sharing — parts, prompts, and real costs.

TL;DR

Voice note sharing needs four pieces: the browser MediaRecorder API for recording, WaveSurfer.js for waveform visualization, Supabase Storage for the audio file, and a Deepgram or Whisper Edge Function for auto-transcription. With Lovable or V0 you can ship a working feature in 6–10 hours. Running cost is $0–$5/month at 100 users, rising to $10–$30/month at 1,000 as storage and transcription costs accumulate.

What Voice Note Sharing Actually Is

Voice note sharing lets users record short audio clips in the browser and attach them to messages, feed posts, or comments — no app download required. The browser MediaRecorder API handles recording natively, WaveSurfer.js renders the live waveform during capture and a scrubable playback bar afterward, and Supabase Storage holds the WebM audio file. The feature is advanced not because any single piece is hard, but because five layers (permissions, encoding, upload, transcription, playback) all have to work together — and each has its own failure mode on Safari, in Lovable preview, and during Next.js SSR.

What users consider table stakes in 2026

  • One-tap record button with a live animated waveform during recording so users know the mic is working
  • Max recording duration limit (2 minutes) with a visible countdown timer so recordings don't grow unbounded
  • Inline playback with a scrub bar and speed controls (0.5×, 1×, 1.5×, 2×) directly on the note card
  • Waveform thumbnail rendered on shared note cards so recipients see what they are about to play
  • Auto-transcription shown below the audio player for accessibility and searchability
  • Graceful mic permission denial handling with a clear 'microphone access required' explainer state

Anatomy of the Feature

Six components across four layers. The MediaRecorder and WaveSurfer.js pieces are where AI-generated builds most often go wrong.

Layers:UIDataBackendService

MediaRecorder API layer

UI

Requests microphone access via navigator.mediaDevices.getUserMedia() and feeds the audio stream into a MediaRecorder instance. The ondataavailable callback fires every 250ms, accumulating audio chunks that are later assembled into a single Blob for upload.

Note: Requires HTTPS — getUserMedia silently fails on http:// and inside cross-origin iframes like Lovable's preview. Safari returns MP4/AAC instead of WebM; detect with MediaRecorder.isTypeSupported() before constructing the recorder.

Waveform visualizer

UI

WaveSurfer.js (npm: wavesurfer.js) renders both the live recording waveform via AudioContext + AnalyserNode and the playback scrubber from the stored audio URL. During recording, sample the analyser every 60ms and push amplitude values into a running JSON array that becomes the stored waveform_data.

Note: WaveSurfer.js accesses document on import, causing a crash in Next.js SSR. Always load it with dynamic(() => import('wavesurfer.js'), { ssr: false }).

Audio upload handler

Backend

Takes the assembled audio Blob, encodes it cross-browser using RecordRTC (npm: recordrtc) to ensure consistent WebM output, then uploads to the Supabase Storage voice-notes bucket. Returns a signed URL (1-hour expiry) for immediate playback and inserts a voice_notes row with storage_path and duration_seconds.

Note: Chunked upload is necessary for longer recordings — a 2-minute high-quality WebM approaches 20MB which can exceed in-memory blob limits on low-RAM mobile devices.

Audio player component

UI

A custom HTML5 audio element wrapper that uses WaveSurfer.js to render the stored waveform_data array as a scrubable bar. Exposes playback rate buttons at 0.5×, 1×, 1.5×, and 2×. Fetches a fresh signed URL from Supabase before each play to avoid 403 errors on expired URLs.

Note: Do not store the signed URL permanently — it expires after 1 hour by design. Re-request it from the backend on each mount.

Transcription service

Service

A Supabase Edge Function triggered after successful upload calls Deepgram Nova-2 (or OpenAI Whisper) with the audio file path. The transcript is written back to voice_notes.transcript. The UI shows 'Transcript loading...' until the column is populated.

Note: Use Deepgram's async transcription endpoint — synchronous calls time out for notes over ~90 seconds because Supabase Edge Functions have a 150-second limit.

Sharing logic

Data

The voice_notes table row controls access: is_public (bool) exposes the note to anyone with the link; shared_with (uuid[]) restricts to specific user IDs. Supabase RLS policies enforce both modes using auth.uid() checks and GIN index lookups on the array column.

Note: Storage bucket should be private — serve audio exclusively through signed URLs, never public bucket URLs, so RLS on the metadata table remains the single source of truth for access.

The data model

Two objects need persistence: the voice note metadata row and the audio file in Storage. Run this SQL in the Supabase SQL Editor to create the table, RLS policies, and index:

schema.sql
1create table public.voice_notes (
2 id uuid primary key default gen_random_uuid(),
3 creator_id uuid references auth.users(id) on delete cascade not null,
4 storage_path text not null,
5 public_url text,
6 duration_seconds int not null default 0,
7 waveform_data jsonb,
8 transcript text,
9 is_public bool not null default false,
10 shared_with uuid[] not null default '{}',
11 created_at timestamptz not null default now()
12);
13
14alter table public.voice_notes enable row level security;
15
16-- Creator can always read their own notes
17create policy "Creators can read own voice notes"
18 on public.voice_notes for select
19 using (auth.uid() = creator_id);
20
21-- Public notes are readable by anyone authenticated
22create policy "Authenticated users can read public voice notes"
23 on public.voice_notes for select
24 using (is_public = true and auth.uid() is not null);
25
26-- Shared-with users can read their notes
27create policy "Shared users can read voice notes"
28 on public.voice_notes for select
29 using (auth.uid() = any(shared_with));
30
31-- Only creator can insert
32create policy "Creators can insert voice notes"
33 on public.voice_notes for insert
34 with check (auth.uid() = creator_id);
35
36-- Only creator can update (e.g. transcript backfill)
37create policy "Creators can update own voice notes"
38 on public.voice_notes for update
39 using (auth.uid() = creator_id);
40
41-- Only creator can delete
42create policy "Creators can delete own voice notes"
43 on public.voice_notes for delete
44 using (auth.uid() = creator_id);
45
46create index voice_notes_creator_created_idx
47 on public.voice_notes (creator_id, created_at desc);
48
49-- GIN index for shared_with array lookups
50create index voice_notes_shared_with_gin_idx
51 on public.voice_notes using gin (shared_with);

Heads up: Create a Supabase Storage bucket named 'voice-notes' with private visibility. The GIN index on shared_with keeps per-user inbox queries fast as the notes array grows.

Build it — pick your path

Each tool fits this feature differently. Switch tabs to compare fit, time, the exact steps, and a copy-paste prompt.

Hand-built by developersFit for this feature:

Full control over audio codec selection, server-side waveform pre-computation, FFmpeg re-encoding for size optimization, and live voice broadcasting. The right choice when audio quality and scalability are non-negotiable.

Step by step

  1. 1Implement proper Opus/WebM encoding via MediaRecorder with explicit codec constraints for cross-browser consistency
  2. 2Add server-side waveform computation at upload time using audiowaveform or Web Audio API in a Node.js worker — stored JSON renders instantly client-side without WaveSurfer needing the raw audio
  3. 3Add FFmpeg-based audio normalization and compression (reducing a 2-min WebM from 20MB to 3–5MB) via a Supabase Edge Function or Cloudflare Worker
  4. 4Implement voice note expiry: a Supabase scheduled function deletes storage objects and rows older than the configured TTL (e.g. 7 days for ephemeral notes)

Where this path bites

  • Requires understanding of Web Audio API, streaming uploads, and server-side audio processing
  • FFmpeg in a serverless function requires a custom layer or Cloudflare Worker with Wasm FFmpeg — not trivial to set up

Third-party services you'll need

Recording and playback are free. Costs appear at transcription and storage scale:

ServiceWhat it doesFree tierPaid from
Supabase StorageHosts audio files; private bucket with signed URL access1GB storage included on free plan$25/mo (Pro) + $0.021/GB over 100GB (approx)
Deepgram Nova-2Speech-to-text transcription via async API after upload$200 credit on signup$0.0043/min audio (approx)
OpenAI Whisper APITranscription alternative; slightly more expensive but no signup creditNo free tier beyond initial API credit$0.006/min audio (approx)
WaveSurfer.jsWaveform rendering during recording and playbackFree (open source)$0
RecordRTCCross-browser audio encoding to consistent WebM/MP4 outputFree (open source)$0

Swipe the table sideways to see pricing.

What it costs to run

Drag through the tiers to see how your monthly bill scales with users — no surprises later.

Estimated monthly running cost

$3/mo

Supabase Storage free tier (1GB covers ~1,000 one-minute notes). Deepgram $200 signup credit absorbs all transcription at this scale — roughly $0.26/note at 1 minute average.

Estimates use verified 2026 pricing and assume typical usage per user. Your real bill depends on activity, storage, and third-party plans.

What breaks when AI tools build this

The failures people actually hit on their first build — the symptom, why it happens, and the exact fix.

Mic access blocked in Lovable preview

Symptom: Lovable's preview pane runs inside a sandboxed cross-origin iframe that does not pass the allow='microphone' attribute. Any call to getUserMedia() returns a NotAllowedError immediately, making it appear the feature is broken when it is actually fine.

Fix: Test voice recording exclusively on the published HTTPS URL. Click Publish → Update in Lovable and open the live domain on your phone or in a new browser tab. Never judge mic functionality from the preview iframe.

Safari records MP4 instead of WebM

Symptom: Safari's MediaRecorder implementation produces MP4/AAC audio. If your code hardcodes 'audio/webm' as the MIME type, Safari throws an error before recording starts. Meanwhile, some WaveSurfer.js configurations expect WebM and render the waveform incorrectly for MP4 streams.

Fix: Detect support before constructing the recorder: const mimeType = MediaRecorder.isTypeSupported('audio/webm') ? 'audio/webm' : 'audio/mp4'. Pass the detected mimeType to both the MediaRecorder constructor and WaveSurfer's format option.

WaveSurfer.js crashes Next.js on server render

Symptom: WaveSurfer.js accesses the browser document object on module import. Next.js attempts to import and render all components server-side during the build, which throws 'document is not defined' and crashes the build or produces a blank page.

Fix: Load WaveSurfer exclusively on the client: const WaveSurfer = dynamic(() => import('wavesurfer.js'), { ssr: false }) in every file that uses it. This is not optional — it will break every V0-generated Next.js file where WaveSurfer is imported at the top level.

Upload fails silently for 2-minute recordings

Symptom: A 2-minute voice note at typical MediaRecorder quality produces a 15–25MB Blob stored in browser memory. Low-RAM mobile devices (under 2GB RAM) may silently drop the Blob before the upload starts, or the Supabase JS client upload times out on slow connections.

Fix: Switch to chunked upload: call MediaRecorder with timeslice: 250 to receive 250ms chunks via ondataavailable, accumulate them in a chunks array, and only assemble into a final Blob after stop fires. For very long notes, stream chunks directly to Supabase Storage using multipart upload.

Transcription Edge Function times out on long audio

Symptom: Supabase Edge Functions have a hard 150-second execution limit. Synchronous Deepgram transcription for a 2-minute audio file takes 5–20 seconds plus network time, which can exceed the limit under cold-start conditions or slow network paths.

Fix: Use Deepgram's async transcription endpoint: POST the request and receive a job ID, then poll or receive a webhook callback with the transcript. Store the transcript via the webhook to voice_notes.transcript when ready. The UI shows 'Transcript loading...' with a polling useEffect until the column is populated.

Best practices

1

Always detect MIME type support before constructing MediaRecorder — never hardcode 'audio/webm' or you will break Safari

2

Store waveform amplitude data as a JSON array during recording so playback renders instantly without WaveSurfer needing to decode the full audio file

3

Use async Deepgram transcription with a webhook callback rather than synchronous calls to avoid Edge Function timeouts on longer notes

4

Generate signed URLs at playback time with a 1-hour expiry rather than storing permanent URLs — this keeps your Storage bucket private and enforces RLS at the data layer

5

Show a clear microphone permission explainer with a 'Try again' button rather than a generic browser error — permission denial is the most common first-run failure

6

Prevent sharing until storage_path is confirmed written in the database — disabling the Share button during upload avoids sharing a note that failed to persist

7

Cap recording at 2 minutes client-side with a visual countdown; longer recordings amplify every cost (storage, bandwidth, transcription) and rarely improve UX for a sharing feature

When You Need Custom Development

AI tools handle the record-upload-transcribe loop well. These use cases exceed what a first-build prompt session can produce reliably:

  • Audio compression is required server-side (FFmpeg re-encoding to Opus reduces file size 4–6× over raw WebM, cutting storage and bandwidth costs significantly at scale)
  • Waveform data must be pre-computed at upload time on the server for instant rendering without shipping raw audio to every viewer
  • Voice notes need automatic expiry (delete after 7 days) via a scheduled Supabase function with Storage object cleanup
  • Live voice broadcasting to multiple simultaneous listeners is required — this needs a WebRTC SFU (Selective Forwarding Unit) like LiveKit, which is a fundamentally different architecture from recorded note sharing

RapidDev builds these features for production

Real auth, database, and payments — fixed price, delivered in weeks. You own 100% of the code.

Get a fixed-price quote

Frequently asked questions

How do I record audio in the browser without a library?

The browser MediaRecorder API is built into every modern browser — no library needed to record. Call navigator.mediaDevices.getUserMedia({ audio: true }) to get a mic stream, pass it to new MediaRecorder(stream), and listen to ondataavailable for audio chunks. A library like RecordRTC is only needed if you want consistent cross-browser codec output (it normalizes WebM vs MP4 differences).

How do I show a live waveform while recording?

Connect an AudioContext and AnalyserNode to the getUserMedia stream before passing it to MediaRecorder. Call analyser.getByteTimeDomainData(dataArray) every 60ms in a requestAnimationFrame loop and draw the amplitude values to a Canvas element. WaveSurfer.js wraps this pattern but adds SSR complexity in Next.js — you can also implement the Canvas drawing loop directly if you want to avoid the dynamic import requirement.

How do I transcribe voice notes automatically?

After the audio file is uploaded to Supabase Storage, invoke a Supabase Edge Function that sends the file URL to Deepgram's Nova-2 API or OpenAI Whisper. For notes under 60 seconds, synchronous Deepgram calls work fine. For notes approaching 2 minutes, use Deepgram's async endpoint: submit the job and receive a webhook when the transcript is ready, then write it to voice_notes.transcript.

Why does mic recording work on localhost but not my deployed app?

getUserMedia requires a secure context — HTTPS or localhost. Any http:// deployment blocks it silently. Additionally, Lovable's preview pane and V0's sandbox both run inside cross-origin iframes that don't grant microphone permissions regardless of HTTPS status. Always test on the actual deployed HTTPS URL, not inside any tool's preview.

How do I handle Safari's different audio format?

Safari's MediaRecorder produces MP4/AAC instead of WebM. Check at runtime with MediaRecorder.isTypeSupported('audio/webm') before constructing the recorder. If it returns false (Safari), use 'audio/mp4'. Store the detected MIME type in your voice_notes row so the player can inform WaveSurfer of the correct format. Deepgram accepts both formats, so transcription is unaffected.

How do I add playback speed controls to an audio player?

If you're using a raw HTML audio element, set audioElement.playbackRate to 0.5, 1, 1.5, or 2. If you're using WaveSurfer.js, call wavesurfer.setPlaybackRate(rate). Render four buttons and update state on click. Speed controls are supported in all modern browsers with no additional library.

How much does audio transcription cost at scale?

Deepgram Nova-2 costs $0.0043/minute of audio (approx). At 1,000 users each sending 10 one-minute voice notes per month, that is 10,000 minutes at roughly $43/month. OpenAI Whisper costs $0.006/minute (approx) — about 40% more. Both offer batch processing discounts at high volume. Caching transcripts in your database means each note is only transcribed once regardless of how many times it is played.

Can I delete voice notes automatically after a set number of days?

Yes. Create a Supabase scheduled function (pg_cron) that runs nightly: SELECT id, storage_path FROM voice_notes WHERE created_at < NOW() - INTERVAL '7 days'. For each row, call the Supabase Storage API to delete the file, then delete the row. You can also set a Supabase Storage lifecycle rule at the bucket level for automatic object expiry without custom code.

RapidDev

Need this feature production-ready?

RapidDev builds voice note sharing into real apps — auth, database, payments — at $13K–$25K.

Book a free consultation

30-min call. No commitment.

Want this built for you?

We ship production apps at a fixed price — $13K–$25K, 6–10 weeks, source code yours. You've seen what it takes; we do it every week.

Get a fixed-price quote

We put the rapid in RapidDev

Need a dedicated strategic tech and growth partner? Discover what RapidDev can do for your business! Book a call with our team to schedule a free, no-obligation consultation. We'll discuss your project and provide a custom quote at no cost.