ComposeWithMe

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 under tracks/{github-login}-{trackname}.{ext}. Uploads go straight from the browser to GitHub via Octokit (@octokit/rest), using a token fetched on demand from GET /api/github/token (looked up server-side from the Account table, never stored in the session/JWT). project.json is 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, prefixed songs/<id>/<trackname-slug>.<ext>. Uploads go through @vercel/blob/client's upload() — a client-side direct upload, with /api/blob/upload's handleUpload issuing short-lived tokens. Every upload uses addRandomSuffix: true, since Blob's put/upload reject 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.ts is the shared abstraction other code calls through: getGithubTokenForUser(), listSongTracks(), deleteSongTrack(), deleteSongObjects() — each branches on song.storageBackend so callers don't have to.

Key files

FileRole
src/lib/storage.tsBackend-agnostic abstraction used by API routes and pages
src/lib/github.tsGitHub API helpers (createSongRepo, listTracks, uploadTrack, deleteTrack)
src/lib/trackUpload.tsShared client-side upload path (branches per backend), used by manual upload, GarageBand import, and the mic recorder
src/app/api/blob/upload/route.tshandleUpload token route for client-side Blob uploads
src/app/api/github/token/route.tsReturns 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_repo OAuth 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.