Storage Backends
Songs pick GitHub (self-hosted) or hosted Vercel Blob storage at creation
Overview
Each song picks a storage backend at creation time and keeps it for life — there's no migrating a song between backends later. GITHUB is a self-hosted option: a public GitHub repo owned by the song's creator, opt-in and requires a linked GitHub account. BLOB (Vercel Blob) is the default for everyone else — no per-user account or setup needed.
Regardless of backend, Song.name/bpm/createdAt/ownerId live in Postgres as the single index of every song. There's deliberately no Track table — track listing is always a live call to the backend's native listing API, so the storage backend stays the source of truth for what tracks actually exist (no caching to go stale).
How it works
- GITHUB: repo named
cwm-<song-name>, tracks live undertracks/{github-login}-{trackname}.{ext}. Uploads go straight from the browser to GitHub via Octokit (@octokit/rest), using a token fetched on demand fromGET /api/github/token(looked up server-side from theAccounttable, never stored in the session/JWT).project.jsonis written into the repo at creation for self-describing-ness if the repo is ever disconnected, but the app never reads it back. - BLOB: objects live under
songs/<id>/, public access, prefixedsongs/<id>/<trackname-slug>.<ext>. Uploads go through@vercel/blob/client'supload()— a client-side direct upload, with/api/blob/upload'shandleUploadissuing short-lived tokens. Every upload usesaddRandomSuffix: true, since Blob'sput/uploadreject same-pathname overwrites by default — each upload is additive, never an in-place replace. - Both paths exist to route around Vercel's 4.5MB function body limit — uploads never pass through a Vercel function body, only short-lived auth tokens do.
src/lib/storage.tsis the shared abstraction other code calls through:getGithubTokenForUser(),listSongTracks(),deleteSongTrack(),deleteSongObjects()— each branches onsong.storageBackendso callers don't have to.
Key files
| File | Role |
|---|---|
src/lib/storage.ts | Backend-agnostic abstraction used by API routes and pages |
src/lib/github.ts | GitHub API helpers (createSongRepo, listTracks, uploadTrack, deleteTrack) |
src/lib/trackUpload.ts | Shared client-side upload path (branches per backend), used by manual upload, GarageBand import, and the mic recorder |
src/app/api/blob/upload/route.ts | handleUpload token route for client-side Blob uploads |
src/app/api/github/token/route.ts | Returns the caller's own GitHub token, for the client-side Octokit path |
Limitations / notes
- GitHub Contents API tops out around ~100MB per file — MP3 over WAV is recommended to stay well under.
- Deleting a GITHUB-backed song only removes its Postgres row — the GitHub repo and its files are left untouched, since removing them would need the
delete_repoOAuth scope this app doesn't request. Deleting a BLOB-backed song does clean up every object under its prefix (deleteSongObjects). - No collaborator/ACL model for either backend — only the song's owner can upload.