Earshot — personal audio library. Books transformed into multi-episode podcasts, streamed from R2, with per-user listening position saved across devices.
Live at https://earshot.haimdimer.com.
- Next.js 16 (App Router, Turbopack)
- NextAuth v5 — Credentials provider (email + bcrypt)
- Neon Postgres (via the
@neondatabase/serverlessHTTP driver, so cold-start friendly on Vercel) - Cloudflare R2 for audio storage, S3-compatible API, Range request support
- Tailwind v4
- Deployed on Vercel (auto-deploys from
main)
Each book is a row in books. Episodes are rows in episodes, each with a unique audio_key matching an R2 object (<audio_key>.mp3). When you play an episode, the audio player throttles timeupdate to save your position every 5 seconds (locally and to the server). pause, seeked, and pagehide (via navigator.sendBeacon) all save too. Position is keyed on (user_email, audio_key) so you resume from any device.
See CLAUDE.md for the architecture detail.
cp .env.example .env.local
# Fill in DATABASE_URL, AUTH_SECRET (openssl rand -base64 32), R2_* creds
npx tsx scripts/apply-schema.ts
npm run devSign up at /signup, then /.
- Generate the episodes with the
podcastClaude skill. - Pick a slug (e.g.
getting-to-we). Save a600x800-ish cover atpublic/covers/<slug>.jpg. - Upload each MP3 to R2:
wrangler r2 object put "library-audio/<slug>-ep1.mp3" --file ep1/ep1-podcast.mp3 --content-type audio/mpeg --remote - Write a seed script modeled on
scripts/seed-getting-to-we.ts, pointDATABASE_URLat prod, run it.
The audio player and position-tracking schema were ported from an internal audio feature I had built previously; same UX, rebuilt on a different backend.