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.

Use the service role key on the server only

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.

schema.sql
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.

progress-subscription.ts
// 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();
Longer files

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.