Skip to content

Local / cloud storage

NEXORA abstracts media storage behind the Storage interface (web/src/lib/server/storage/index.ts) with two implementations:

  • LocalStorage (default) โ€” filesystem under web/static/;
  • S3Storage โ€” S3-compatible (Cloudflare R2, AWS S3, MinIO), enabled by NEXORA_STORAGE_PROVIDER=s3.
flowchart LR
   APP["Web app"] --> S["Storage interface<br/>put / get / delete / exists / url"]
   S -->|local| FS[("web/static/<br/>videos/ ยท uploads/")]
   S -->|s3| R2[("R2 bucket<br/>videos/original ยท annotations")]

Backend choice (NEXORA_STORAGE_PROVIDER)

local (default) s3 (R2)
Videos web/static/videos/original/<name> videos/original/<name>
Student photos web/static/uploads/students/ uploads/students/
Streaming HTTP Range (seek) reads the whole object (no Range outside public URL)
Intended use dev / demo multi-instance prod

Object keys

  • Videos/ โ†’ videos/<name>; transcode: videos/original/<name> and videos/proxy/<name> (720p @ 5 fps proxy, preferred for streaming if it exists).
  • Photos: uploads/students/<filename>.
  • R2: videos/original/<name>.mp4 + annotations/<name>.csv.

Streaming route

GET /videos/<name> (web/src/routes/videos/[name]/+server.ts): 1. authentication required (any logged-in role); 2. looks up proxy โ†’ original โ†’ videos/<name>; 3. local backend โ†’ direct stream with Range; otherwise reads the whole object.

Switching to R2

# root .env
NEXORA_STORAGE_PROVIDER=s3
NEXORA_S3_ENDPOINT=https://<account-id>.r2.cloudflarestorage.com
NEXORA_S3_REGION=auto
NEXORA_S3_BUCKET=nexora-media
NEXORA_S3_ACCESS_KEY=...
NEXORA_S3_SECRET_KEY=...
NEXORA_S3_PUBLIC_URL=https://media.<domain>   # optional

Streaming upload (machine without dashboard R2 access):

cd web
node ../scripts/upload-to-r2.mjs --video ../static/videos/original/full_class_merged.mp4 \
     --csv ../processing/data/march_labels_clean.csv
# keys created: videos/original/<name> ยท annotations/<name>.csv

Idempotent (only uploads what is missing):

node scripts/upload-assets.mjs              # upload missing assets
node scripts/upload-assets.mjs --clear      # force full re-upload

R2 lifecycle (cost)

Rule Prefix Action When
Archive originals videos/original/ Infrequent Access after 7 days
Long-term archive videos/original/ Glacier after 90 days
Delete chunks videos/chunks/ Delete after 30 days
Delete proxies videos/proxy/ Delete after 180 days

(Lifecycle rules to configure in the Cloudflare R2 dashboard.)