Edge Functions

Streaming Speech with ElevenLabs

Generate and stream speech through Supabase Edge Functions. Store speech in Supabase Storage and cache responses via built-in CDN.


Introduction

In this tutorial you will learn how to build an edge API to generate, stream, store, and cache speech using Supabase Edge Functions, Supabase Storage, and ElevenLabs text to speech API.

Requirements

Setup

Create a Supabase project locally

After installing the Supabase CLI, run the following command to create a new Supabase project locally:


_10
supabase init

Configure the storage bucket

You can configure the Supabase CLI to automatically generate a storage bucket by adding this configuration in the config.toml file:

./supabase/config.toml

_10
[storage.buckets.audio]
_10
public = false
_10
file_size_limit = "50MiB"
_10
allowed_mime_types = ["audio/mp3"]
_10
objects_path = "./audio"

Configure background tasks for Supabase Edge Functions

To use background tasks in Supabase Edge Functions when developing locally, you need to add the following configuration in the config.toml file:

./supabase/config.toml

_10
[edge_runtime]
_10
policy = "per_worker"

Create a Supabase Edge Function for speech generation

Create a new Edge Function by running the following command:


_10
supabase functions new text-to-speech

If you're using VS Code or Cursor, select y when the CLI prompts "Generate VS Code settings for Deno? [y/N]"!

Set up the environment variables

Within the supabase/functions directory, create a new .env file and add the following variables:

supabase/functions/.env

_10
# Find / create an API key at https://elevenlabs.io/app/settings/api-keys
_10
ELEVENLABS_API_KEY=your_api_key

Dependencies

The project uses a couple of dependencies:

Since Supabase Edge Function uses the Deno runtime, you don't need to install the dependencies, rather you can import them via the npm: prefix.

Code the Supabase Edge Function

In your newly created supabase/functions/text-to-speech/index.ts file, add the following code:

supabase/functions/text-to-speech/index.ts

_91
// Setup type definitions for built-in Supabase Runtime APIs
_91
import 'jsr:@supabase/functions-js/edge-runtime.d.ts'
_91
import { createClient } from 'jsr:@supabase/supabase-js@2'
_91
import { ElevenLabsClient } from 'npm:elevenlabs@1.52.0'
_91
import * as hash from 'npm:object-hash'
_91
_91
const supabase = createClient(
_91
Deno.env.get('SUPABASE_URL')!,
_91
Deno.env.get('SUPABASE_SERVICE_ROLE_KEY')!
_91
)
_91
_91
const client = new ElevenLabsClient({
_91
apiKey: Deno.env.get('ELEVENLABS_API_KEY'),
_91
})
_91
_91
// Upload audio to Supabase Storage in a background task
_91
async function uploadAudioToStorage(stream: ReadableStream, requestHash: string) {
_91
const { data, error } = await supabase.storage
_91
.from('audio')
_91
.upload(`${requestHash}.mp3`, stream, {
_91
contentType: 'audio/mp3',
_91
})
_91
_91
console.log('Storage upload result', { data, error })
_91
}
_91
_91
Deno.serve(async (req) => {
_91
// To secure your function for production, you can for example validate the request origin,
_91
// or append a user access token and validate it with Supabase Auth.
_91
console.log('Request origin', req.headers.get('host'))
_91
const url = new URL(req.url)
_91
const params = new URLSearchParams(url.search)
_91
const text = params.get('text')
_91
const voiceId = params.get('voiceId') ?? 'JBFqnCBsd6RMkjVDRZzb'
_91
_91
const requestHash = hash.MD5({ text, voiceId })
_91
console.log('Request hash', requestHash)
_91
_91
// Check storage for existing audio file
_91
const { data } = await supabase.storage.from('audio').createSignedUrl(`${requestHash}.mp3`, 60)
_91
_91
if (data) {
_91
console.log('Audio file found in storage', data)
_91
const storageRes = await fetch(data.signedUrl)
_91
if (storageRes.ok) return storageRes
_91
}
_91
_91
if (!text) {
_91
return new Response(JSON.stringify({ error: 'Text parameter is required' }), {
_91
status: 400,
_91
headers: { 'Content-Type': 'application/json' },
_91
})
_91
}
_91
_91
try {
_91
console.log('ElevenLabs API call')
_91
const response = await client.textToSpeech.convertAsStream(voiceId, {
_91
output_format: 'mp3_44100_128',
_91
model_id: 'eleven_multilingual_v2',
_91
text,
_91
})
_91
_91
const stream = new ReadableStream({
_91
async start(controller) {
_91
for await (const chunk of response) {
_91
controller.enqueue(chunk)
_91
}
_91
controller.close()
_91
},
_91
})
_91
_91
// Branch stream to Supabase Storage
_91
const [browserStream, storageStream] = stream.tee()
_91
_91
// Upload to Supabase Storage in the background
_91
EdgeRuntime.waitUntil(uploadAudioToStorage(storageStream, requestHash))
_91
_91
// Return the streaming response immediately
_91
return new Response(browserStream, {
_91
headers: {
_91
'Content-Type': 'audio/mpeg',
_91
},
_91
})
_91
} catch (error) {
_91
console.log('error', { error })
_91
return new Response(JSON.stringify({ error: error.message }), {
_91
status: 500,
_91
headers: { 'Content-Type': 'application/json' },
_91
})
_91
}
_91
})

Run locally

To run the function locally, run the following commands:


_10
supabase start

Once the local Supabase stack is up and running, run the following command to start the function and observe the logs:


_10
supabase functions serve

Try it out

Navigate to http://127.0.0.1:54321/functions/v1/text-to-speech?text=hello%20world to hear the function in action.

Afterwards, navigate to http://127.0.0.1:54323/project/default/storage/buckets/audio to see the audio file in your local Supabase Storage bucket.

Deploy to Supabase

If you haven't already, create a new Supabase account at database.new and link the local project to your Supabase account:


_10
supabase link

Once done, run the following command to deploy the function:


_10
supabase functions deploy

Set the function secrets

Now that you have all your secrets set locally, you can run the following command to set the secrets in your Supabase project:


_10
supabase secrets set --env-file supabase/functions/.env

Test the function

The function is designed in a way that it can be used directly as a source for an <audio> element.


_10
<audio
_10
src="https://${SUPABASE_PROJECT_REF}.supabase.co/functions/v1/text-to-speech?text=Hello%2C%20world!&voiceId=JBFqnCBsd6RMkjVDRZzb"
_10
controls
_10
/>

You can find an example frontend implementation in the complete code example on GitHub.