Family Storytelling Platform

Client

Fable Together

Category

Product + Front-End

Year

2025

Fable Together storytelling interface
Fable Together storytelling interface

Fable Together started with an early interface direction and a long feature list. I turned that into a working responsive web application, handling product/UI implementation, front-end development, Supabase and API integration, and the technical connections between the interface and its services. I also integrated ElevenLabs for narration and instant voice cloning, while building accessibility into the application from the start rather than treating it as a final pass.

The MVP included a Story Library, multilingual narration, a Voice Library and instant voice cloning, user-specific data and profiles, search, quests and credits, all within a responsive dashboard. The goal at this stage was a functional product for testing rather than a finished public release.

Next.js · React · TypeScript · Supabase · ElevenLabs · Vercel

Voice cloning touched most of the stack: the request verified the signed-in user, sent the recording to ElevenLabs, saved the resulting voice to Supabase, and updated the user’s quest and credit state.


Voice cloning API route

const supabase = await createSupabaseServerClient()
const { data: { user } } = await supabase.auth.getUser()

if (!user) {
  return NextResponse.json(
    { error: "Unauthorized" },
    { status: 401 }
  )
}

const clonedVoice = await cloneVoice({
  name: sanitizedName,
  audioFiles: audioFile,
  removeBackgroundNoise,
  labels: { created_by: user.id },
})

const { data: voiceRecord } = await supabase
  .from("voices")
  .insert({
    owner_id: user.id,
    voice_id: clonedVoice.voice_id,
    label: sanitizedName,
    provider: "elevenlabs",
    category: "narrator",
  })
  .select()
  .single()

completeQuestServerSide("clone-voice", user.id)
const supabase = await createSupabaseServerClient()
const { data: { user } } = await supabase.auth.getUser()

if (!user) {
  return NextResponse.json(
    { error: "Unauthorized" },
    { status: 401 }
  )
}

const clonedVoice = await cloneVoice({
  name: sanitizedName,
  audioFiles: audioFile,
  removeBackgroundNoise,
  labels: { created_by: user.id },
})

const { data: voiceRecord } = await supabase
  .from("voices")
  .insert({
    owner_id: user.id,
    voice_id: clonedVoice.voice_id,
    label: sanitizedName,
    provider: "elevenlabs",
    category: "narrator",
  })
  .select()
  .single()

completeQuestServerSide("clone-voice", user.id)
const supabase = await createSupabaseServerClient()
const { data: { user } } = await supabase.auth.getUser()

if (!user) {
  return NextResponse.json(
    { error: "Unauthorized" },
    { status: 401 }
  )
}

const clonedVoice = await cloneVoice({
  name: sanitizedName,
  audioFiles: audioFile,
  removeBackgroundNoise,
  labels: { created_by: user.id },
})

const { data: voiceRecord } = await supabase
  .from("voices")
  .insert({
    owner_id: user.id,
    voice_id: clonedVoice.voice_id,
    label: sanitizedName,
    provider: "elevenlabs",
    category: "narrator",
  })
  .select()
  .single()

completeQuestServerSide("clone-voice", user.id)

By the end of the MVP phase, the team had a working test build rather than a collection of disconnected screens. Some audio and responsive work was still being refined, but the core product flows and technical structure were in place.