# Deploy (Fly.io + Cloudflare R2) One Fly machine (scale-to-zero) runs Django ASGI (daphne) + ffmpeg, serving the API, the WebSocket, and the compiled SPA. SQLite lives on a Fly volume. **All media — clips, OTIO, thumbnails — lives in Cloudflare R2** (S3-compatible, free egress), served through Cloudflare's CDN. The frontend is a same-origin release build, so there's no CORS in prod. ## 1. Cloudflare R2 (do this in the dashboard) 1. **Create a bucket** (R2 → Create bucket), e.g. `lumet-media`. 2. **Public access via a custom domain**: bucket → Settings → Public access → *Connect a domain*, e.g. `media.yourdomain.com` (Cloudflare proxied). That host is `R2_PUBLIC_DOMAIN`. (This is what makes egress free + cached.) 3. **CORS policy** on the bucket (Settings → CORS) — the app `fetch()`es the OTIO JSON cross-origin, so allow your app origin: ```json [{"AllowedOrigins":["https://.fly.dev"], "AllowedMethods":["GET"],"AllowedHeaders":["*"],"MaxAgeSeconds":3600}] ``` 4. **API token**: R2 → Manage API Tokens → Create (Object Read & Write, this bucket). Note the **Access Key ID**, **Secret Access Key**, and your account's **S3 endpoint** `https://.r2.cloudflarestorage.com`. ## 2. Fly secrets ```bash fly secrets set \ DJANGO_SECRET_KEY="$(python -c 'import secrets;print(secrets.token_urlsafe(64))')" \ DJANGO_CSRF_TRUSTED="https://.fly.dev" \ R2_BUCKET="lumet-media" \ R2_ENDPOINT="https://.r2.cloudflarestorage.com" \ R2_ACCESS_KEY_ID="..." \ R2_SECRET_ACCESS_KEY="..." \ R2_PUBLIC_DOMAIN="media.yourdomain.com" ``` ## 3. Deploy ```bash fly volumes create data -r iad -n 1 -s 1 # 1 GB for SQLite (once) fly deploy fly ssh console -C "python manage.py createsuperuser" # first admin ``` `fly.toml` already sets `DJANGO_DEBUG=0`, `DJANGO_ALLOWED_HOSTS=.fly.dev`, `DJANGO_DB_PATH=/data/db.sqlite3`, and scale-to-zero. ## Notes / caveats - **Thumbnails run in a background thread.** With scale-to-zero the machine can stop right after an upload returns and kill that thread before it finishes — so thumbnails may not generate on the very first idle deploy. Options: bump `min_machines_running = 1`, or move thumbnailing to a proper queue/worker later. Core app (video, annotations, sync) is unaffected. - **Single machine only** while on SQLite + the in-memory Channels layer. To scale out: LiteFS (SQLite replication) + `channels_redis` (Upstash). - **Dev is unchanged**: `npm run dev` still serves UI on :8280 / API on :9001 with local-disk media. R2 only kicks in when the `R2_*` env vars are set.