Family Storytelling Platform

Client

Fable Together

Category

Product + Front-End

Year

2025

Fable Together storytelling interface
Fable Together storytelling interface

Overview

Fable Together started with an early interface direction and a long list of features. My job was to turn that into a working web application. I built out the front end, connected Supabase, wrote the API routes, integrated ElevenLabs for narration and voice cloning, and kept the dashboard responsive as new features were added.

Role & Scope

I handled product/UI implementation, front-end development, database and API work, and a lot of the technical glue between the interface and the services behind it. I also built accessibility into the application from the start rather than treating it as a final pass.

What I Built

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

Implementation

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

Voice cloning touched most of the stack. The request checked the signed-in user, sent the recording to ElevenLabs, saved the returned voice to Supabase, and then updated the quest/credit system.

src/app/api/voices/clone/route.ts

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)

Project State

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