Footage and audio are too big for git. framefields keeps them on a bucket you own and pins each file in framefields.json by its sha256, the way a lockfile pins packages: the repo stays small, every clone and every render gets exactly the same bytes, and nothing about your film’s code changes.
npx framefields assets add assets/footage # upload, pin, gitignorenpx framefields assets pull # in a fresh clone: download and verifyAny S3-compatible bucket works. Cloudflare R2 is the one we recommend: it charges nothing for downloads, so cloning, CI and cloud renders cost you storage only.
Set up a bucket
Section titled “Set up a bucket”- Create a bucket (in R2: R2 → Create bucket) and an API token that can read and write it (Manage R2 API tokens, Object Read & Write, scoped to the bucket).
- Optionally, make it public through a custom domain (R2: bucket Settings → Custom Domains), or any CDN in front of it. Then anyone can pull your assets, and renders started from the dashboard can read them, without keys.
- Add it to
framefields.json:
{ "storage": { "remotes": { "media": { "endpoint": "https://<account-id>.r2.cloudflarestorage.com", "bucket": "films", "prefix": "launch/", "publicUrl": "https://media.example.com" } } }}| Field | |
|---|---|
endpoint |
The bucket’s S3 API URL. R2: https://<account-id>.r2.cloudflarestorage.com (EU jurisdiction: …eu.r2.cloudflarestorage.com). AWS: https://s3.<region>.amazonaws.com. |
bucket |
The bucket name. |
region |
SigV4 region. Default auto, which R2 uses; AWS needs the bucket’s region. |
prefix |
Optional folder inside the bucket, so several projects can share one. |
publicUrl |
Optional public base URL: a custom domain, a CDN or the r2.dev URL. Reads use it without keys. |
Several remotes are fine, for example a public one for media you’re happy to share and a private one for raw footage. "default": "media" names the one new assets go to; --remote raw picks another.
- Give the CLI the keys through the environment, never the file:
export FRAMEFIELDS_STORAGE_MEDIA_ACCESS_KEY_ID=…export FRAMEFIELDS_STORAGE_MEDIA_SECRET_ACCESS_KEY=…The names follow the remote (raw-footage → FRAMEFIELDS_STORAGE_RAW_FOOTAGE_…). Without them, the standard AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY are used.
Commands
Section titled “Commands”| Command | |
|---|---|
npx framefields assets add <path…> |
Hash files (a folder means every file in it), upload what the bucket lacks, pin them under assets and add them to .gitignore. Adding a changed file again pins the new version. |
npx framefields assets pull |
Download missing assets and check every byte. Files you changed locally are left alone unless --overwrite. |
npx framefields assets push |
Upload pinned assets the bucket lacks, from your local files. |
npx framefields assets status |
Each asset: ok, missing or changed. --json for scripts. |
add writes entries like this; commit framefields.json and .gitignore:
"assets": { "assets/footage/intro.mp4": { "sha256": "9f2c…", "size": 48213992 }}An entry can also carry a fixed url instead of a remote, for public media you didn’t upload (stock footage, a sample): it is downloaded from there and checked against the same sha256.
Custom domains and CDNs
Section titled “Custom domains and CDNs”Objects are stored by content, at <prefix>sha256/<ab>/<sha256>, so they never change. Uploads are marked Cache-Control: public, max-age=31536000, immutable and carry their real Content-Type, so a custom domain or CDN can cache them forever and browsers can play them directly: https://media.example.com/launch/sha256/9f/9f2c….
Projects sharing a bucket and prefix store a file once, and uploading it again is skipped.
Preview from the bucket
Section titled “Preview from the bucket”npx framefields preview doesn’t need the files on disk. When a pinned asset is missing (a fresh clone, an agent working in a cloud sandbox), the preview sends the page to the bucket for it: the remote’s publicUrl, or a URL signed with the remote’s keys from the environment. The browser streams it from there, seeking with byte ranges, so nothing is downloaded into the project, and the soundtrack is mixed from the same URLs.
The engine reads media with fetch, so the bucket must allow the preview’s origin (CORS). In R2: bucket Settings → CORS policy:
[ { "AllowedOrigins": ["*"], "AllowedMethods": ["GET", "HEAD"], "AllowedHeaders": ["Range"], "ExposeHeaders": ["Content-Length", "Content-Range", "Accept-Ranges", "ETag"], "MaxAgeSeconds": 86400 }]"*" suits a public bucket: anyone can read it anyway. For a private one, list exact origins instead: the preview’s (http://127.0.0.1:<port>, printed when it starts; the port is stable per project) and your agent sandbox’s or tunnel’s. The policy covers the bucket’s custom domain and signed URLs alike. On S3 it is the same rules under Permissions → CORS.
Without CORS the page shows a fetch error for the clip. npx framefields assets pull is the fallback: with the files on disk, the preview serves them itself.
R2 signed URLs work only on the S3 API host (<account-id>.r2.cloudflarestorage.com), not a custom domain: the custom domain is for public media, and private remotes are read through signatures.
Render here or in the cloud
Section titled “Render here or in the cloud”npx framefields render <name> renders on this machine and first downloads pinned assets that are missing; npx framefields render <name> --cloud renders the same named render on framefields GPUs, to the same output file.
In cloud renders
Section titled “In cloud renders”Cloud renders download pinned assets on every GPU before rendering:
framefields render --cloudandrenderCloudfirst upload anything the bucket lacks, so a forgottenpushdoesn’t fail the render.- Assets on a remote with a
publicUrlare read from it. Assets on a private remote get read URLs signed on your machine, valid for 24 hours; your keys are never sent. - Workers cache assets by hash, so a worker that renders the project again reuses them. On R2, the downloads every GPU of a split render makes cost nothing, unlike LFS bandwidth from a git host.
- Renders started from the dashboard have no machine to sign from, so they read only remotes with a
publicUrl.
Git LFS
Section titled “Git LFS”Projects that already use Git LFS keep working: the render worker pulls LFS before it downloads pinned assets, and framefields init still writes LFS .gitattributes. LFS has two costs a bucket avoids: every clone and every render worker downloads the objects from your git host, which bills that bandwidth to the repo owner (an open-source film pays for everyone who clones it), and a clone without git lfs pull holds pointer files that decode as blank frames.
To move, npx framefields assets add the LFS-tracked files, remove their patterns from .gitattributes, and git rm --cached them.
When something fails
Section titled “When something fails”| Code | Cause | Fix |
|---|---|---|
ASSET_NOT_UPLOADED |
A pinned asset isn’t on the bucket | npx framefields assets push from a machine that has it |
ASSET_HASH_MISMATCH |
The bucket or URL serves other bytes than framefields.json pins |
Push the right file, or assets add the new version |
ASSET_PRIVATE |
A dashboard render, and the remote has no publicUrl |
Render from the CLI, or add a publicUrl |
ASSET_FORBIDDEN |
The custom domain doesn’t serve the bucket, or a signed URL expired | Check the domain is connected to the bucket; render again |
ASSET_MANIFEST_INVALID |
storage or assets is malformed |
npx framefields assets status shows the error |