Storage Decisions

Doc Status: Good | ✓ Clear summary | ✓ Easy to read | ✓ Matches code | ✓ Good structure | ✓ Professional look | ✓ Visual components

Decision Table

Asset typeWhereWhy
Marketing content (blog, hero, product photo)Sanity CDNPM/marketing edits via Studio; auto webp/resize
Small admin uploads (avatar, < 5MB)Convex storageAuth-gated, transactional with the row
Heavy media (instrument HD photos, video clips)Cloudflare R2Bandwidth cost + CDN reach
UI primitives (logos, icons, brand SVGs)public/images/{logos,icons,branding}Cache-friendly, version-controlled
Build artefacts (Next.js out/)Not committedCI 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 GitHub repository_dispatch that rebuilds and redeploys the website.
Never commit local image files to content folders — use the CI guardrail below to block it.

Convex Storage (Small Admin Uploads)

For avatars, internal documents, attachments under ~5MB that need ACL tied to Convex Auth. See docs/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
R2 is provisioned via Terraform. Uploads go through a Convex action that signs a PUT URL.

CI Guardrail

.github/workflows/guard-image-storage.yml blocks PRs that add new files to content-image folders:
apps/website/public/images/courses/
apps/website/public/images/curated/
apps/website/public/images/instruments/
apps/website/public/images/why-choose-us/
apps/website/public/images/certificates/
apps/website/public/images/location/
apps/website/public/images/about/
Editing existing files is allowed. Only git diff --diff-filter=A (added files) triggers the block.

Adding New Content Images

  1. Recommended (PM): Open Sanity Studio → find the document → upload via image field → publish.
  2. Migration (dev): Use the batch upload script → get back Sanity CDN URL → reference via constant → do not commit to blocked folder.
// ✅ Reference via Sanity CDN URL
const url = urlFor(data.heroImage).width(1600).quality(85).url();
<Image src={url} />

// ✅ Typed constant for known images
import { ABOUT_IMAGES } from '@/components/about/about-images';
<Image src={ABOUT_IMAGES.eventChristmas2025} />