Using Speech Revolutions with Supabase
If your users upload audio to Supabase Storage, you can transcribe it without downloading it: create a signed URL for the object, pass it to Speech Revolutions, and write the transcript to a Postgres table. You can also stream progress to the browser over Supabase Realtime. The Speech Revolutions API key and the Supabase service role key stay on the server.
Creating signed URLs and privileged table writes use the Supabase service role key. Keep it, and SPEECHREVOLUTIONS_API_KEY, on your server. The browser uses only the anon key and reads rows through Row Level Security.
The table
Create one row per job with the storage path, a status, a 0–100 progress value, and the transcript text when the job completes.
create table transcriptions (
id uuid primary key default gen_random_uuid(),
storage_path text not null,
job_id text,
status text not null default 'processing', -- processing|completed|failed
percent real not null default 0, -- overall 0-100
text text,
created_at timestamptz not null default now()
);
-- so the browser can subscribe to its own rows over Realtime
alter publication supabase_realtime add table transcriptions;Transcribe a file in Supabase Storage
Create a signed URL for the uploaded object, with an expiry longer than the transcription time, and pass it to transcribe(). The SDK detects the URL and Speech Revolutions fetches the audio directly from Supabase. Then write the transcript to the table.
import { createClient } from "@supabase/supabase-js";
import { SpeechRevolutions } from "speechrevolutions";
// Server-side: service role key, never shipped to the browser.
const supabase = createClient(
process.env.SUPABASE_URL!,
process.env.SUPABASE_SERVICE_ROLE_KEY!,
);
const stt = new SpeechRevolutions(); // SPEECHREVOLUTIONS_API_KEY
export async function transcribeFromStorage(id: string, storagePath: string) {
// 1. Signed URL so Speech Revolutions can read the private object.
const { data, error } = await supabase.storage
.from("audio")
.createSignedUrl(storagePath, 3600); // seconds — outlast the transcription
if (error) throw error;
// 2. Pass the URL straight to Speech Revolutions (auto-detected as a URL).
const result = await stt.transcribe(data.signedUrl, {
speakerLabels: true,
onProgress: (e) => {
// A Supabase query only runs once it is awaited or then'd.
void supabase
.from("transcriptions")
.update({ percent: e.percent ?? 0 })
.eq("id", id) // drives Realtime updates (see below)
.then(({ error }) => error && console.error(error));
},
});
// 3. Store the transcript in Postgres.
await supabase
.from("transcriptions")
.update({ status: "completed", percent: 100, text: result.text })
.eq("id", id);
}Optional: live progress over Realtime
The on_progress/onProgress callback writes percent to the row, so the browser can subscribe to that row over Supabase Realtime and update a progress bar without polling.
// Browser: anon key + RLS. No Speech Revolutions key here.
const channel = supabase
.channel("job")
.on(
"postgres_changes",
{
event: "UPDATE",
schema: "public",
table: "transcriptions",
filter: `id=eq.${rowId}`,
},
(payload) => {
const { percent, status, text } = payload.new;
setPercent(percent); // 0-100 from the SDK callback
if (status === "completed") setText(text);
},
)
.subscribe();For long recordings, use submit() with a callback_url instead of blocking on transcribe(), and update the row from your webhook handler after verifying X-SR-Signature with your signing secret (see Webhooks). The Next.js and FastAPI guides show complete receivers, and Live progress for web apps explains how to weight the callbacks into one progress bar.