Storage Decisions
Doc Status: Good | ✓ Clear summary | ✓ Easy to read | ✓ Matches code | ✓ Good structure | ✓ Professional look | ✓ Visual components
Decision Table
| Asset type | Where | Why |
|---|---|---|
| Marketing content (blog, hero, product photo) | Sanity CDN | PM/marketing edits via Studio; auto webp/resize |
| Small admin uploads (avatar, < 5MB) | Convex storage | Auth-gated, transactional with the row |
| Heavy media (instrument HD photos, video clips) | Cloudflare R2 | Bandwidth cost + CDN reach |
| UI primitives (logos, icons, brand SVGs) | public/images/{logos,icons,branding} | Cache-friendly, version-controlled |
Build artefacts (Next.js out/) | Not committed | CI builds → CF Workers Static Assets |
Sanity CDN (Marketing)
For blog hero images, product photos, instructor bios, and course content — anything marketing edits via Sanity Studio. Marketing content goes through Sanity Studio. When a PM edits an image field and publishes, the Sanity webhook fires a GitHubrepository_dispatch that rebuilds and redeploys the website.
Convex Storage (Small Admin Uploads)
For avatars, internal documents, attachments under ~5MB that need ACL tied to Convex Auth. Seedocs/convex/storage-cron for the implementation pattern.
Reference in schema: v.id('_storage').
R2 (Heavy Media)
For instrument photos at full resolution, video clips, and large PDFs — where Sanity CDN would be uneconomical.- Bucket:
r2://wds-assets/<category>/<file> - Access: signed PUT URL from Convex action (never expose secret to browser)
- Public assets: public URL via Cloudflare custom domain
action that signs a PUT URL.
CI Guardrail
.github/workflows/guard-image-storage.yml blocks PRs that add new files to content-image folders:
git diff --diff-filter=A (added files) triggers the block.
Adding New Content Images
- Recommended (PM): Open Sanity Studio → find the document → upload via image field → publish.
- Migration (dev): Use the batch upload script → get back Sanity CDN URL → reference via constant → do not commit to blocked folder.