Local / cloud storage
NEXORA abstracts media storage behind the Storage interface
(web/src/lib/server/storage/index.ts) with two implementations:
LocalStorage(default) โ filesystem underweb/static/;S3Storageโ S3-compatible (Cloudflare R2, AWS S3, MinIO), enabled byNEXORA_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>andvideos/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.)