Vibecode Frame.io
track this build5 phases, 15 steps, beginner friendly0%The review loop itself is not the hard part: a player, timecoded comments, version stacking, and an unlisted share link are a weekend of work for one editor and a few clients. What you cannot rebuild is the rest of the product · Camera to Cloud from set, the Premiere and Resolve panels that put comments on your timeline as markers, transcoding and CDN delivery that hold up when a client scrubs a 4K cut from a hotel wifi, and the security paperwork studios ask for. Solo and small-shop review is genuinely vibecodeable. Delivery infrastructure is not.
You are building a lean indie version of Frame.io. Create the following project files first, then implement the application by following them. Keep the files updated as decisions change. Do not collapse this into a single README or prompt. ===== README.md ===== # Frame.io · indie build A watch folder that transcodes a review proxy, an unlisted page where reviewers scrub and comment on an exact frame, versions stacked so a client can compare, and an export of every note as timecode your editing application understands. Estimated effort: **weekend**. Work `BUILD_PLAN.md` top to bottom · every phase ends in a check that has to pass before the next one starts. ## Stack | Part | Choice | Why | | --- | --- | --- | | Transcode | ffmpeg producing a review proxy with a fixed keyframe interval | frequent keyframes make scrubbing responsive, which is what reviewers actually judge the tool on | | Runtime | Node 22 with SQLite | a handful of assets and a few hundred comments | | Player | A player driven by frame number, not seconds | the comment anchor is a frame and every seek and display derives from it | | Storage | Local disk or S3-compatible storage behind a reverse proxy | review proxies are large and originals should not leave your machine unless you decide | | Watching | A folder watcher with a settle check | an export still being written must not be transcoded | ## Before you start Have every one of these ready. The plan assumes them from step one. - [ ] **Node.js 22 or newer** · free - Why: Runs the watcher, the server and the share pages. - Get it: nodejs.org, or nvm install 22. - Verify: node --version prints v22 or higher - [ ] **ffmpeg and ffprobe** · free - Why: Probing for the exact frame rate and transcoding proxies both run through them. - Get it: brew install ffmpeg or your package manager. - Verify: ffprobe reports a rational frame rate for a test file - [ ] **Two source files, one at a non-integer frame rate and one over an hour** · free - Why: The timecode work is only testable against a real 29.97 file, and rounding errors only become visible at length. - Get it: Use your own footage. If you have a drop-frame file, use that; otherwise generate one at 30000 over 1001. - Verify: Both are available and one reports a non-integer rational rate - [ ] **A server with real disk and bandwidth** · 10 to 40 US dollars a month depending on storage and transfer - Why: Proxies are large and clients stream them. A small instance will transcode slowly and serve worse. - Get it: A virtual server with enough disk for your active projects and bandwidth you have checked the price of. - Verify: A proxy transcodes and streams smoothly to another network - [ ] **An editing application to verify marker import** · free if you already have one - Why: Phase 5's export is only correct if the markers land where the comments were, and only the editing application can tell you. - Get it: Whichever you already use. The marker import format differs, so target the one you actually work in. - Verify: Exported markers import and land on the intended frames ## Quick start ```sh ffprobe -v error -select_streams v:0 -show_entries stream=r_frame_rate,nb_frames -show_entries format_tags=timecode -of json input.mov ``` Then copy `.env.example` to `.env` and fill in the values it documents. ## Honest limits This build deliberately does not replace: - Camera to cloud ingest. - Review panels inside editing applications. - A transcode farm and a global delivery network. - The reliability clients already expect from a service. - Camera to Cloud ingest straight off set - Premiere, After Effects, and Resolve review panels that sync comments to timeline markers - a transcode farm and global CDN, so client playback is only as good as your box - forensic watermarking, SSO, and the security posture studios require - the iPad and mobile review apps clients actually use - storage, transfer tooling, and uptime bundled into the seat price If one of those is essential to you, that is the reason to keep paying for Frame.io, and the README should say so rather than pretend. ===== BRIEF.md ===== # Build brief · Frame.io The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me a review link where clients leave comments pinned to an exact frame, and you get them back as timecode to replace Frame.io. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Rule zero Frame accuracy is the whole product, and it is where this gets subtle. Video frame rates are often not integers: what everyone calls 30 frames per second is usually 30000 divided by 1001, or about 29.97. A comment stored as a time in seconds and converted back with the wrong rate lands on the wrong frame, and a note that says the wrong frame is worse than no note. So store the frame number, carry the exact rational frame rate with every asset, and know whether the timecode is drop-frame before you display any. Get that right in Phase 1 and the rest is straightforward. ### Stack (fixed, do not substitute) - Transcode: ffmpeg producing a review proxy with a fixed keyframe interval · frequent keyframes make scrubbing responsive, which is what reviewers actually judge the tool on - Runtime: Node 22 with SQLite · a handful of assets and a few hundred comments - Player: A player driven by frame number, not seconds · the comment anchor is a frame and every seek and display derives from it - Storage: Local disk or S3-compatible storage behind a reverse proxy · review proxies are large and originals should not leave your machine unless you decide - Watching: A folder watcher with a settle check · an export still being written must not be transcoded ### Data model (create this before Phase 1) - assets(id, source_path, title, frame_rate_num, frame_rate_den, drop_frame, duration_frames, timecode_start_frames, proxy_key, state) - versions(id, asset_group_id, asset_id, ordinal, label, uploaded_at) · a group is one shot reviewed repeatedly - shares(id, asset_group_id, token, password_hash, expires_at, allow_download, created_at) - comments(id, version_id, frame, author_name, body, created_at, resolved_at, parent_id) - annotations(comment_id, shape_json) · a drawing over the frame, stored in normalised coordinates - The frame rate is stored as a numerator and a denominator. Storing it as a decimal is the bug that produces off-by-one frames an hour into a long file. ### Phase 1 · Frame rates and timecode, before anything else Build: Probe a source file for its exact frame rate as a rational, its duration in frames, its starting timecode and whether it is drop-frame. Write conversions between frame number and timecode both ways, and test them against a long file. Trap: Storing 29.97 rather than 30000 over 1001 accumulates error, and after an hour the computed frame is wrong. Keep the rate rational throughout. Trap: Drop-frame timecode skips frame numbers rather than frames: it omits numbers 0 and 1 at the start of each minute except every tenth minute, so wall clock and timecode stay close. Converting a drop-frame timecode with non-drop arithmetic is wrong by several seconds an hour, and the error grows. Done when: Frame rate is stored as a rational and reported exactly. Converting frame to timecode and back returns the original frame for every frame in a one hour file, tested exhaustively. A drop-frame file's timecode at one hour matches the expected value. A file with a non-zero starting timecode reports frames relative to it correctly. Do not build yet: No transcoding, sharing or comments. ### Phase 2 · A proxy that scrubs well Build: Watch a folder, wait for files to settle, and transcode a review proxy with a short keyframe interval. Keep the exact frame rate. Generate a poster. Never modify the source file. Trap: A proxy with a long keyframe interval forces the player to decode from the last keyframe on every seek, which makes scrubbing feel broken even though playback is fine. Use a short interval, accept the larger file, and remember that reviewers judge this tool on scrubbing rather than on playback. Done when: Scrubbing the proxy updates frames without a visible delay. The proxy's frame rate and duration in frames match the source exactly. A file still being copied is not transcoded until the copy completes. The source file checksum is unchanged. Do not build yet: No sharing or comments yet. ### Phase 3 · A review link that plays for a client Build: An unlisted share page with an unguessable token, optional password and expiry. The player displays the current timecode and steps a frame at a time with arrow keys. Serve the proxy behind a signature so the file is not reachable by guessing. Trap: An unguessable page URL with a predictable video file URL means the video circulates without the page, which for unreleased client work is exactly what the review link was supposed to prevent. Sign the media requests and keep the file private. Done when: A share link plays for someone with no account. A direct request to the proxy without a signature is refused. Arrow keys step exactly one frame and the displayed timecode advances by one. An expired share refuses rather than showing an empty page. Do not build yet: No comments yet. ### Phase 4 · Comments pinned to a frame Build: Comments anchored to a frame number, with optional drawing over the frame stored in normalised coordinates. Markers on the timeline. Clicking a comment seeks to its frame and shows its drawing. Trap: Storing an annotation in pixel coordinates makes it land in the wrong place on any other display size or if the proxy resolution changes. Normalise to the frame's dimensions, so the drawing scales with whatever the reviewer is watching on. Done when: A comment created while paused on a frame reopens on exactly that frame. A drawing appears in the same place on a different screen size, verified by opening the link on a phone. Timeline markers correspond to comment frames. A comment on a version does not appear on another version. Do not build yet: No versions or export yet. ### Phase 5 · Versions and the notes going back Build: Stack versions of the same shot with labels. Comments belong to a version. Export all notes as timecode in a format your editing application can import as markers, using the exact frame rate and drop-frame flag. Trap: Exporting notes as timecode computed with a rounded frame rate produces markers that drift from the comment position, and the drift grows through the file so the last notes are the most wrong. Use the stored rational rate and the drop-frame flag for every conversion in the export. Done when: A second version stacks under the same share and comments stay with their version. Exported markers land on the intended frames when imported into an editing application, verified for a note near the end of a long file. The export names the frame rate and drop-frame status. Comparing two versions is possible from the share page. Do not build yet: Operations and cleanup come last. ### Phase 6 · Storage, bandwidth and cleanup Build: Retention removing proxies past an age unless the project is active, with a warning first. Bandwidth and storage reporting. Backups of the comment database, which is the part that is not regenerable. Archive a project as notes plus a manifest. Trap: Proxies are regenerable from the source and comments are not, so the backup priority is inverted from what the file sizes suggest. Back up the small database religiously and treat the large proxies as a cache. Done when: Retention removes aged proxies after a warning and keeps active projects. Deleting a proxy leaves the comments intact and the share page explains rather than breaking. Storage and bandwidth per project are reported. A restore of the database reproduces every comment with correct frames. Do not build yet: Nothing after this. ### Out of scope (and why) - Camera to cloud ingest straight off set, which is a hardware and network integration rather than software. - Review panels inside editing applications that sync comments to timeline markers, which are per-application extensions and are most of the daily convenience. - A transcode farm and a global delivery network, so a client on another continent gets whatever your server can send. - The reliability of a service clients already trust, which matters when the review is with someone paying you. ### README must contain - That frame rate is stored as a rational and why 29.97 as a decimal is wrong - That drop-frame timecode skips numbers rather than frames, and how that is handled - That the proxy uses a short keyframe interval because reviewers judge scrubbing - That annotations are stored in normalised coordinates so they scale - That proxies are a regenerable cache and the comment database is the thing to back up ===== AGENTS.md ===== # Agent instructions · Frame.io indie build - Read `README.md` and `BUILD_PLAN.md` before writing code. The stack is fixed: ffmpeg producing a review proxy with a fixed keyframe interval, Node 22 with SQLite, A player driven by frame number, not seconds, Local disk or S3-compatible storage behind a reverse proxy, A folder watcher with a settle check. Do not substitute. - Work one phase at a time, in order. Do not start a phase until every "Done when" item of the previous one passes. - Prefer the fewest moving parts that satisfy the step. No frameworks, services or dependencies the plan does not name. - Secrets live in `.env`, never in source or logs. Keep `.env.example` current when a variable is introduced. - Do not invent cryptography, security guarantees, APIs or compliance claims. - Add a focused test for every destructive, security-sensitive or data-loss path the plan names. - Run the project checks before declaring a phase complete, and record any deliberate shortcut in the README under "Tradeoffs". ## Known traps - Storing 29.97 rather than 30000 over 1001 accumulates error, and after an hour the computed frame is wrong. Keep the rate rational throughout. - Drop-frame timecode skips frame numbers rather than frames: it omits numbers 0 and 1 at the start of each minute except every tenth minute, so wall clock and timecode stay close. Converting a drop-frame timecode with non-drop arithmetic is wrong by several seconds an hour, and the error grows. - A proxy with a long keyframe interval forces the player to decode from the last keyframe on every seek, which makes scrubbing feel broken even though playback is fine. Use a short interval, accept the larger file, and remember that reviewers judge this tool on scrubbing rather than on playback. - An unguessable page URL with a predictable video file URL means the video circulates without the page, which for unreleased client work is exactly what the review link was supposed to prevent. Sign the media requests and keep the file private. - Storing an annotation in pixel coordinates makes it land in the wrong place on any other display size or if the proxy resolution changes. Normalise to the frame's dimensions, so the drawing scales with whatever the reviewer is watching on. - Exporting notes as timecode computed with a rounded frame rate produces markers that drift from the comment position, and the drift grows through the file so the last notes are the most wrong. Use the stored rational rate and the drop-frame flag for every conversion in the export. ===== BUILD_PLAN.md ===== # Build plan · Frame.io A watch folder that transcodes a review proxy, an unlisted page where reviewers scrub and comment on an exact frame, versions stacked so a client can compare, and an export of every note as timecode your editing application understands. Phases are in dependency order. Each ends in a "Done when" list; treat an unticked item as a blocker, not a note. ## Phase 1 · Frame rates and timecode, before anything else You can convert between frame number and timecode correctly for a non-integer rate. ### Steps 1. Probe for the exact rate Read the rational frame rate rather than the rounded one, plus duration and start timecode. Files: `src/probe.js` ```sh ffprobe -v error -select_streams v:0 -show_entries stream=r_frame_rate,nb_frames -show_entries format_tags=timecode -of json input.mov ``` 2. Implement both conversions Frame to timecode and back, handling drop-frame and a non-zero start. Files: `src/timecode.js` 3. Test exhaustively over an hour Every frame of a one hour file, both directions. This is cheap and catches every rounding mistake. Files: `test/timecode.test.js` ### Done when - [ ] Frame rate is stored and reported as a rational - [ ] Round trip is exact for every frame in a one hour file - [ ] Drop-frame timecode at one hour matches the expected value - [ ] A non-zero start timecode is handled correctly ### Watch out - Storing 29.97 rather than 30000 over 1001 accumulates error, and after an hour the computed frame is wrong. Keep the rate rational throughout. - Drop-frame timecode skips frame numbers rather than frames: it omits numbers 0 and 1 at the start of each minute except every tenth minute, so wall clock and timecode stay close. Converting a drop-frame timecode with non-drop arithmetic is wrong by several seconds an hour, and the error grows. ## Phase 2 · A proxy that scrubs well A reviewer can drag the playhead and see frames update immediately. ### Steps 1. Watch with a settle check Size stable across two checks before touching a file. An export in progress is the common case here. Files: `src/watch.js` 2. Transcode with frequent keyframes Short interval for scrubbing, matching the source frame rate exactly rather than normalising it. ```sh ffmpeg -i source.mov -c:v libx264 -g 12 -keyint_min 12 -sc_threshold 0 -crf 23 -c:a aac proxy.mp4 ``` 3. Verify frame counts match Source and proxy must have the same duration in frames, or every comment is offset. Files: `src/verify.js` ### Done when - [ ] Scrubbing updates frames without visible delay - [ ] Proxy frame rate and frame count match the source - [ ] A copying file is not transcoded early - [ ] The source checksum is unchanged ### Watch out - A proxy with a long keyframe interval forces the player to decode from the last keyframe on every seek, which makes scrubbing feel broken even though playback is fine. Use a short interval, accept the larger file, and remember that reviewers judge this tool on scrubbing rather than on playback. ## Phase 3 · A review link that plays for a client Someone with the link can watch and scrub without an account. ### Steps 1. Serve an unlisted share page Unguessable token, optional password, expiry. No public listing anywhere. Files: `src/share.js` 2. Sign media requests The proxy is private and reachable only through a short-lived signature issued after the gate. Files: `src/media.js` 3. Drive the player by frame Arrow keys step one frame, the display shows timecode, and every seek is computed from a frame number. Files: `public/player.js` ### Done when - [ ] A share link plays for someone with no account - [ ] An unsigned proxy request is refused - [ ] Arrow keys step exactly one frame - [ ] An expired share refuses access ### Watch out - An unguessable page URL with a predictable video file URL means the video circulates without the page, which for unreleased client work is exactly what the review link was supposed to prevent. Sign the media requests and keep the file private. ## Phase 4 · Comments pinned to a frame A comment lands on the exact frame the reviewer was looking at. ### Steps 1. Anchor comments to frame numbers Not seconds. The frame is what the reviewer saw and what the editor needs. Files: `src/comments.js` 2. Normalise annotation coordinates Relative to frame dimensions, so drawings scale with the display. Files: `public/annotate.js` 3. Verify across screen sizes Open the same link on a phone and a desktop. The drawing must land in the same place. ### Done when - [ ] A comment reopens on exactly its frame - [ ] A drawing lands correctly on a different screen size - [ ] Timeline markers match comment frames - [ ] Comments do not leak between versions ### Watch out - Storing an annotation in pixel coordinates makes it land in the wrong place on any other display size or if the proxy resolution changes. Normalise to the frame's dimensions, so the drawing scales with whatever the reviewer is watching on. ## Phase 5 · Versions and the notes going back A client can compare versions, and you get every note as timecode. ### Steps 1. Stack versions in a group One share, several versions, comments per version, with a visible current version. Files: `src/versions.js` 2. Export markers with exact conversion Rational rate and drop-frame flag applied. Verify a note near the end rather than the start. Files: `src/export.js` 3. Check in a real editing application Import the markers and confirm placement. This is the step that reveals a rounding error. ### Done when - [ ] Versions stack and comments stay with their version - [ ] Exported markers land correctly, checked near the end of a long file - [ ] The export names frame rate and drop-frame status - [ ] Two versions can be compared from the share page ### Watch out - Exporting notes as timecode computed with a rounded frame rate produces markers that drift from the comment position, and the drift grows through the file so the last notes are the most wrong. Use the stored rational rate and the drop-frame flag for every conversion in the export. ## Not in this build - Camera to cloud ingest. - Review panels inside editing applications. - A transcode farm and a global delivery network. - The reliability clients already expect from a service. ## After v1, if you want it - Side by side version comparison with synchronised playheads - Comment status and approval, so a version can be signed off - Automatic proxy regeneration when an archived project is reopened ===== .env.example ===== # Copy to .env and fill in. Never commit .env; this file documents it. # Required. Folder watched for new exports. Files here are never modified. WATCH_DIR=./incoming # Required. Where review proxies are written. Treat as a cache. PROXY_DIR=./proxies # Required. Keyframe interval in frames. Short for scrubbing, at the cost of file size. PROXY_KEYFRAME_FRAMES=12 # Required. Quality setting for the proxy. Lower is better and larger. PROXY_CRF=23 # Required. How long a signed media URL lasts. Long enough for a review session. SIGNED_MEDIA_TTL_SECONDS=7200 # Required. Age at which an inactive project's proxies are removed, after a warning. PROXY_RETENTION_DAYS=120 # Required. Used in share links. Must be HTTPS. PUBLIC_URL=https://review.example.com
You are building a lean indie version of Frame.io. Create the following project files first, then implement the application by following them. Keep the files updated as decisions change. Do not collapse this into a single README or prompt. ===== README.md ===== # Frame.io · indie build A watch folder that transcodes a review proxy, an unlisted page where reviewers scrub and comment on an exact frame, versions stacked so a client can compare, and an export of every note as timecode your editing application understands. Estimated effort: **weekend**. Work `BUILD_PLAN.md` top to bottom · every phase ends in a check that has to pass before the next one starts. ## Stack | Part | Choice | Why | | --- | --- | --- | | Transcode | ffmpeg producing a review proxy with a fixed keyframe interval | frequent keyframes make scrubbing responsive, which is what reviewers actually judge the tool on | | Runtime | Node 22 with SQLite | a handful of assets and a few hundred comments | | Player | A player driven by frame number, not seconds | the comment anchor is a frame and every seek and display derives from it | | Storage | Local disk or S3-compatible storage behind a reverse proxy | review proxies are large and originals should not leave your machine unless you decide | | Watching | A folder watcher with a settle check | an export still being written must not be transcoded | ## Before you start Have every one of these ready. The plan assumes them from step one. - [ ] **Node.js 22 or newer** · free - Why: Runs the watcher, the server and the share pages. - Get it: nodejs.org, or nvm install 22. - Verify: node --version prints v22 or higher - [ ] **ffmpeg and ffprobe** · free - Why: Probing for the exact frame rate and transcoding proxies both run through them. - Get it: brew install ffmpeg or your package manager. - Verify: ffprobe reports a rational frame rate for a test file - [ ] **Two source files, one at a non-integer frame rate and one over an hour** · free - Why: The timecode work is only testable against a real 29.97 file, and rounding errors only become visible at length. - Get it: Use your own footage. If you have a drop-frame file, use that; otherwise generate one at 30000 over 1001. - Verify: Both are available and one reports a non-integer rational rate - [ ] **A server with real disk and bandwidth** · 10 to 40 US dollars a month depending on storage and transfer - Why: Proxies are large and clients stream them. A small instance will transcode slowly and serve worse. - Get it: A virtual server with enough disk for your active projects and bandwidth you have checked the price of. - Verify: A proxy transcodes and streams smoothly to another network - [ ] **An editing application to verify marker import** · free if you already have one - Why: Phase 5's export is only correct if the markers land where the comments were, and only the editing application can tell you. - Get it: Whichever you already use. The marker import format differs, so target the one you actually work in. - Verify: Exported markers import and land on the intended frames ## Quick start ```sh ffprobe -v error -select_streams v:0 -show_entries stream=r_frame_rate,nb_frames -show_entries format_tags=timecode -of json input.mov ``` Then copy `.env.example` to `.env` and fill in the values it documents. ## Honest limits This build deliberately does not replace: - Camera to cloud ingest. - Review panels inside editing applications. - A transcode farm and a global delivery network. - The reliability clients already expect from a service. - Camera to Cloud ingest straight off set - Premiere, After Effects, and Resolve review panels that sync comments to timeline markers - a transcode farm and global CDN, so client playback is only as good as your box - forensic watermarking, SSO, and the security posture studios require - the iPad and mobile review apps clients actually use - storage, transfer tooling, and uptime bundled into the seat price If one of those is essential to you, that is the reason to keep paying for Frame.io, and the README should say so rather than pretend. ===== BRIEF.md ===== # Build brief · Frame.io The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me a review link where clients leave comments pinned to an exact frame, and you get them back as timecode to replace Frame.io. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Rule zero Frame accuracy is the whole product, and it is where this gets subtle. Video frame rates are often not integers: what everyone calls 30 frames per second is usually 30000 divided by 1001, or about 29.97. A comment stored as a time in seconds and converted back with the wrong rate lands on the wrong frame, and a note that says the wrong frame is worse than no note. So store the frame number, carry the exact rational frame rate with every asset, and know whether the timecode is drop-frame before you display any. Get that right in Phase 1 and the rest is straightforward. ### Stack (fixed, do not substitute) - Transcode: ffmpeg producing a review proxy with a fixed keyframe interval · frequent keyframes make scrubbing responsive, which is what reviewers actually judge the tool on - Runtime: Node 22 with SQLite · a handful of assets and a few hundred comments - Player: A player driven by frame number, not seconds · the comment anchor is a frame and every seek and display derives from it - Storage: Local disk or S3-compatible storage behind a reverse proxy · review proxies are large and originals should not leave your machine unless you decide - Watching: A folder watcher with a settle check · an export still being written must not be transcoded ### Data model (create this before Phase 1) - assets(id, source_path, title, frame_rate_num, frame_rate_den, drop_frame, duration_frames, timecode_start_frames, proxy_key, state) - versions(id, asset_group_id, asset_id, ordinal, label, uploaded_at) · a group is one shot reviewed repeatedly - shares(id, asset_group_id, token, password_hash, expires_at, allow_download, created_at) - comments(id, version_id, frame, author_name, body, created_at, resolved_at, parent_id) - annotations(comment_id, shape_json) · a drawing over the frame, stored in normalised coordinates - The frame rate is stored as a numerator and a denominator. Storing it as a decimal is the bug that produces off-by-one frames an hour into a long file. ### Phase 1 · Frame rates and timecode, before anything else Build: Probe a source file for its exact frame rate as a rational, its duration in frames, its starting timecode and whether it is drop-frame. Write conversions between frame number and timecode both ways, and test them against a long file. Trap: Storing 29.97 rather than 30000 over 1001 accumulates error, and after an hour the computed frame is wrong. Keep the rate rational throughout. Trap: Drop-frame timecode skips frame numbers rather than frames: it omits numbers 0 and 1 at the start of each minute except every tenth minute, so wall clock and timecode stay close. Converting a drop-frame timecode with non-drop arithmetic is wrong by several seconds an hour, and the error grows. Done when: Frame rate is stored as a rational and reported exactly. Converting frame to timecode and back returns the original frame for every frame in a one hour file, tested exhaustively. A drop-frame file's timecode at one hour matches the expected value. A file with a non-zero starting timecode reports frames relative to it correctly. Do not build yet: No transcoding, sharing or comments. ### Phase 2 · A proxy that scrubs well Build: Watch a folder, wait for files to settle, and transcode a review proxy with a short keyframe interval. Keep the exact frame rate. Generate a poster. Never modify the source file. Trap: A proxy with a long keyframe interval forces the player to decode from the last keyframe on every seek, which makes scrubbing feel broken even though playback is fine. Use a short interval, accept the larger file, and remember that reviewers judge this tool on scrubbing rather than on playback. Done when: Scrubbing the proxy updates frames without a visible delay. The proxy's frame rate and duration in frames match the source exactly. A file still being copied is not transcoded until the copy completes. The source file checksum is unchanged. Do not build yet: No sharing or comments yet. ### Phase 3 · A review link that plays for a client Build: An unlisted share page with an unguessable token, optional password and expiry. The player displays the current timecode and steps a frame at a time with arrow keys. Serve the proxy behind a signature so the file is not reachable by guessing. Trap: An unguessable page URL with a predictable video file URL means the video circulates without the page, which for unreleased client work is exactly what the review link was supposed to prevent. Sign the media requests and keep the file private. Done when: A share link plays for someone with no account. A direct request to the proxy without a signature is refused. Arrow keys step exactly one frame and the displayed timecode advances by one. An expired share refuses rather than showing an empty page. Do not build yet: No comments yet. ### Phase 4 · Comments pinned to a frame Build: Comments anchored to a frame number, with optional drawing over the frame stored in normalised coordinates. Markers on the timeline. Clicking a comment seeks to its frame and shows its drawing. Trap: Storing an annotation in pixel coordinates makes it land in the wrong place on any other display size or if the proxy resolution changes. Normalise to the frame's dimensions, so the drawing scales with whatever the reviewer is watching on. Done when: A comment created while paused on a frame reopens on exactly that frame. A drawing appears in the same place on a different screen size, verified by opening the link on a phone. Timeline markers correspond to comment frames. A comment on a version does not appear on another version. Do not build yet: No versions or export yet. ### Phase 5 · Versions and the notes going back Build: Stack versions of the same shot with labels. Comments belong to a version. Export all notes as timecode in a format your editing application can import as markers, using the exact frame rate and drop-frame flag. Trap: Exporting notes as timecode computed with a rounded frame rate produces markers that drift from the comment position, and the drift grows through the file so the last notes are the most wrong. Use the stored rational rate and the drop-frame flag for every conversion in the export. Done when: A second version stacks under the same share and comments stay with their version. Exported markers land on the intended frames when imported into an editing application, verified for a note near the end of a long file. The export names the frame rate and drop-frame status. Comparing two versions is possible from the share page. Do not build yet: Operations and cleanup come last. ### Phase 6 · Storage, bandwidth and cleanup Build: Retention removing proxies past an age unless the project is active, with a warning first. Bandwidth and storage reporting. Backups of the comment database, which is the part that is not regenerable. Archive a project as notes plus a manifest. Trap: Proxies are regenerable from the source and comments are not, so the backup priority is inverted from what the file sizes suggest. Back up the small database religiously and treat the large proxies as a cache. Done when: Retention removes aged proxies after a warning and keeps active projects. Deleting a proxy leaves the comments intact and the share page explains rather than breaking. Storage and bandwidth per project are reported. A restore of the database reproduces every comment with correct frames. Do not build yet: Nothing after this. ### Out of scope (and why) - Camera to cloud ingest straight off set, which is a hardware and network integration rather than software. - Review panels inside editing applications that sync comments to timeline markers, which are per-application extensions and are most of the daily convenience. - A transcode farm and a global delivery network, so a client on another continent gets whatever your server can send. - The reliability of a service clients already trust, which matters when the review is with someone paying you. ### README must contain - That frame rate is stored as a rational and why 29.97 as a decimal is wrong - That drop-frame timecode skips numbers rather than frames, and how that is handled - That the proxy uses a short keyframe interval because reviewers judge scrubbing - That annotations are stored in normalised coordinates so they scale - That proxies are a regenerable cache and the comment database is the thing to back up ===== AGENTS.md ===== # Agent instructions · Frame.io indie build - Read `README.md` and `BUILD_PLAN.md` before writing code. The stack is fixed: ffmpeg producing a review proxy with a fixed keyframe interval, Node 22 with SQLite, A player driven by frame number, not seconds, Local disk or S3-compatible storage behind a reverse proxy, A folder watcher with a settle check. Do not substitute. - Work one phase at a time, in order. Do not start a phase until every "Done when" item of the previous one passes. - Prefer the fewest moving parts that satisfy the step. No frameworks, services or dependencies the plan does not name. - Secrets live in `.env`, never in source or logs. Keep `.env.example` current when a variable is introduced. - Do not invent cryptography, security guarantees, APIs or compliance claims. - Add a focused test for every destructive, security-sensitive or data-loss path the plan names. - Run the project checks before declaring a phase complete, and record any deliberate shortcut in the README under "Tradeoffs". ## Known traps - Storing 29.97 rather than 30000 over 1001 accumulates error, and after an hour the computed frame is wrong. Keep the rate rational throughout. - Drop-frame timecode skips frame numbers rather than frames: it omits numbers 0 and 1 at the start of each minute except every tenth minute, so wall clock and timecode stay close. Converting a drop-frame timecode with non-drop arithmetic is wrong by several seconds an hour, and the error grows. - A proxy with a long keyframe interval forces the player to decode from the last keyframe on every seek, which makes scrubbing feel broken even though playback is fine. Use a short interval, accept the larger file, and remember that reviewers judge this tool on scrubbing rather than on playback. - An unguessable page URL with a predictable video file URL means the video circulates without the page, which for unreleased client work is exactly what the review link was supposed to prevent. Sign the media requests and keep the file private. - Storing an annotation in pixel coordinates makes it land in the wrong place on any other display size or if the proxy resolution changes. Normalise to the frame's dimensions, so the drawing scales with whatever the reviewer is watching on. - Exporting notes as timecode computed with a rounded frame rate produces markers that drift from the comment position, and the drift grows through the file so the last notes are the most wrong. Use the stored rational rate and the drop-frame flag for every conversion in the export. ===== BUILD_PLAN.md ===== # Build plan · Frame.io A watch folder that transcodes a review proxy, an unlisted page where reviewers scrub and comment on an exact frame, versions stacked so a client can compare, and an export of every note as timecode your editing application understands. Phases are in dependency order. Each ends in a "Done when" list; treat an unticked item as a blocker, not a note. ## Phase 1 · Frame rates and timecode, before anything else You can convert between frame number and timecode correctly for a non-integer rate. ### Steps 1. Probe for the exact rate Read the rational frame rate rather than the rounded one, plus duration and start timecode. Files: `src/probe.js` ```sh ffprobe -v error -select_streams v:0 -show_entries stream=r_frame_rate,nb_frames -show_entries format_tags=timecode -of json input.mov ``` 2. Implement both conversions Frame to timecode and back, handling drop-frame and a non-zero start. Files: `src/timecode.js` 3. Test exhaustively over an hour Every frame of a one hour file, both directions. This is cheap and catches every rounding mistake. Files: `test/timecode.test.js` ### Done when - [ ] Frame rate is stored and reported as a rational - [ ] Round trip is exact for every frame in a one hour file - [ ] Drop-frame timecode at one hour matches the expected value - [ ] A non-zero start timecode is handled correctly ### Watch out - Storing 29.97 rather than 30000 over 1001 accumulates error, and after an hour the computed frame is wrong. Keep the rate rational throughout. - Drop-frame timecode skips frame numbers rather than frames: it omits numbers 0 and 1 at the start of each minute except every tenth minute, so wall clock and timecode stay close. Converting a drop-frame timecode with non-drop arithmetic is wrong by several seconds an hour, and the error grows. ## Phase 2 · A proxy that scrubs well A reviewer can drag the playhead and see frames update immediately. ### Steps 1. Watch with a settle check Size stable across two checks before touching a file. An export in progress is the common case here. Files: `src/watch.js` 2. Transcode with frequent keyframes Short interval for scrubbing, matching the source frame rate exactly rather than normalising it. ```sh ffmpeg -i source.mov -c:v libx264 -g 12 -keyint_min 12 -sc_threshold 0 -crf 23 -c:a aac proxy.mp4 ``` 3. Verify frame counts match Source and proxy must have the same duration in frames, or every comment is offset. Files: `src/verify.js` ### Done when - [ ] Scrubbing updates frames without visible delay - [ ] Proxy frame rate and frame count match the source - [ ] A copying file is not transcoded early - [ ] The source checksum is unchanged ### Watch out - A proxy with a long keyframe interval forces the player to decode from the last keyframe on every seek, which makes scrubbing feel broken even though playback is fine. Use a short interval, accept the larger file, and remember that reviewers judge this tool on scrubbing rather than on playback. ## Phase 3 · A review link that plays for a client Someone with the link can watch and scrub without an account. ### Steps 1. Serve an unlisted share page Unguessable token, optional password, expiry. No public listing anywhere. Files: `src/share.js` 2. Sign media requests The proxy is private and reachable only through a short-lived signature issued after the gate. Files: `src/media.js` 3. Drive the player by frame Arrow keys step one frame, the display shows timecode, and every seek is computed from a frame number. Files: `public/player.js` ### Done when - [ ] A share link plays for someone with no account - [ ] An unsigned proxy request is refused - [ ] Arrow keys step exactly one frame - [ ] An expired share refuses access ### Watch out - An unguessable page URL with a predictable video file URL means the video circulates without the page, which for unreleased client work is exactly what the review link was supposed to prevent. Sign the media requests and keep the file private. ## Phase 4 · Comments pinned to a frame A comment lands on the exact frame the reviewer was looking at. ### Steps 1. Anchor comments to frame numbers Not seconds. The frame is what the reviewer saw and what the editor needs. Files: `src/comments.js` 2. Normalise annotation coordinates Relative to frame dimensions, so drawings scale with the display. Files: `public/annotate.js` 3. Verify across screen sizes Open the same link on a phone and a desktop. The drawing must land in the same place. ### Done when - [ ] A comment reopens on exactly its frame - [ ] A drawing lands correctly on a different screen size - [ ] Timeline markers match comment frames - [ ] Comments do not leak between versions ### Watch out - Storing an annotation in pixel coordinates makes it land in the wrong place on any other display size or if the proxy resolution changes. Normalise to the frame's dimensions, so the drawing scales with whatever the reviewer is watching on. ## Phase 5 · Versions and the notes going back A client can compare versions, and you get every note as timecode. ### Steps 1. Stack versions in a group One share, several versions, comments per version, with a visible current version. Files: `src/versions.js` 2. Export markers with exact conversion Rational rate and drop-frame flag applied. Verify a note near the end rather than the start. Files: `src/export.js` 3. Check in a real editing application Import the markers and confirm placement. This is the step that reveals a rounding error. ### Done when - [ ] Versions stack and comments stay with their version - [ ] Exported markers land correctly, checked near the end of a long file - [ ] The export names frame rate and drop-frame status - [ ] Two versions can be compared from the share page ### Watch out - Exporting notes as timecode computed with a rounded frame rate produces markers that drift from the comment position, and the drift grows through the file so the last notes are the most wrong. Use the stored rational rate and the drop-frame flag for every conversion in the export. ## Not in this build - Camera to cloud ingest. - Review panels inside editing applications. - A transcode farm and a global delivery network. - The reliability clients already expect from a service. ## After v1, if you want it - Side by side version comparison with synchronised playheads - Comment status and approval, so a version can be signed off - Automatic proxy regeneration when an archived project is reopened ===== .env.example ===== # Copy to .env and fill in. Never commit .env; this file documents it. # Required. Folder watched for new exports. Files here are never modified. WATCH_DIR=./incoming # Required. Where review proxies are written. Treat as a cache. PROXY_DIR=./proxies # Required. Keyframe interval in frames. Short for scrubbing, at the cost of file size. PROXY_KEYFRAME_FRAMES=12 # Required. Quality setting for the proxy. Lower is better and larger. PROXY_CRF=23 # Required. How long a signed media URL lasts. Long enough for a review session. SIGNED_MEDIA_TTL_SECONDS=7200 # Required. Age at which an inactive project's proxies are removed, after a warning. PROXY_RETENTION_DAYS=120 # Required. Used in share links. Must be HTTPS. PUBLIC_URL=https://review.example.com
You are building a production product version of Frame.io. Create the following project files first, then implement the application by following them. Keep the files updated as decisions change. Do not collapse this into a single README or prompt. ===== PRODUCT.md ===== # Frame.io · product brief ## Problem The review loop itself is not the hard part: a player, timecoded comments, version stacking, and an unlisted share link are a weekend of work for one editor and a few clients. What you cannot rebuild is the rest of the product · Camera to Cloud from set, the Premiere and Resolve panels that put comments on your timeline as markers, transcoding and CDN delivery that hold up when a client scrubs a 4K cut from a hotel wifi, and the security paperwork studios ask for. Solo and small-shop review is genuinely vibecodeable. Delivery infrastructure is not. ## Product outcome A review link where a client's comment lands on the exact frame, drawings scale to any screen, versions stack for comparison, and every note comes back as timecode that imports correctly into your timeline. ## Target user A builder who needs a maintainable product foundation, not a one-off demo. ## Required capabilities - ffmpeg - a VPS with real disk and bandwidth - a domain plus reverse proxy with TLS - chokidar or another folder watcher - a storage and egress budget you now pay yourself ## Explicit non-goals for v1 - Camera to cloud ingest. - Review panels inside editing applications. - A transcode farm and a global delivery network. - The reliability clients already expect from a service. - Camera to Cloud ingest straight off set - Premiere, After Effects, and Resolve review panels that sync comments to timeline markers - a transcode farm and global CDN, so client playback is only as good as your box - forensic watermarking, SSO, and the security posture studios require - the iPad and mobile review apps clients actually use - storage, transfer tooling, and uptime bundled into the seat price ## Success criteria - Frame to timecode round trip is exact for every frame in a one hour file - Drop-frame conversion matches expected values at one hour - An unsigned proxy request is confirmed refused - Exported markers are confirmed correct near the end of a long file - A database restore reproduces comments on correct frames ===== BRIEF.md ===== # Build brief · Frame.io The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me a review link where clients leave comments pinned to an exact frame, and you get them back as timecode to replace Frame.io. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Rule zero Frame accuracy is the whole product, and it is where this gets subtle. Video frame rates are often not integers: what everyone calls 30 frames per second is usually 30000 divided by 1001, or about 29.97. A comment stored as a time in seconds and converted back with the wrong rate lands on the wrong frame, and a note that says the wrong frame is worse than no note. So store the frame number, carry the exact rational frame rate with every asset, and know whether the timecode is drop-frame before you display any. Get that right in Phase 1 and the rest is straightforward. ### Stack (fixed, do not substitute) - Transcode: ffmpeg producing a review proxy with a fixed keyframe interval · frequent keyframes make scrubbing responsive, which is what reviewers actually judge the tool on - Runtime: Node 22 with SQLite · a handful of assets and a few hundred comments - Player: A player driven by frame number, not seconds · the comment anchor is a frame and every seek and display derives from it - Storage: Local disk or S3-compatible storage behind a reverse proxy · review proxies are large and originals should not leave your machine unless you decide - Watching: A folder watcher with a settle check · an export still being written must not be transcoded ### Data model (create this before Phase 1) - assets(id, source_path, title, frame_rate_num, frame_rate_den, drop_frame, duration_frames, timecode_start_frames, proxy_key, state) - versions(id, asset_group_id, asset_id, ordinal, label, uploaded_at) · a group is one shot reviewed repeatedly - shares(id, asset_group_id, token, password_hash, expires_at, allow_download, created_at) - comments(id, version_id, frame, author_name, body, created_at, resolved_at, parent_id) - annotations(comment_id, shape_json) · a drawing over the frame, stored in normalised coordinates - The frame rate is stored as a numerator and a denominator. Storing it as a decimal is the bug that produces off-by-one frames an hour into a long file. ### Phase 1 · Frame rates and timecode, before anything else Build: Probe a source file for its exact frame rate as a rational, its duration in frames, its starting timecode and whether it is drop-frame. Write conversions between frame number and timecode both ways, and test them against a long file. Trap: Storing 29.97 rather than 30000 over 1001 accumulates error, and after an hour the computed frame is wrong. Keep the rate rational throughout. Trap: Drop-frame timecode skips frame numbers rather than frames: it omits numbers 0 and 1 at the start of each minute except every tenth minute, so wall clock and timecode stay close. Converting a drop-frame timecode with non-drop arithmetic is wrong by several seconds an hour, and the error grows. Done when: Frame rate is stored as a rational and reported exactly. Converting frame to timecode and back returns the original frame for every frame in a one hour file, tested exhaustively. A drop-frame file's timecode at one hour matches the expected value. A file with a non-zero starting timecode reports frames relative to it correctly. Do not build yet: No transcoding, sharing or comments. ### Phase 2 · A proxy that scrubs well Build: Watch a folder, wait for files to settle, and transcode a review proxy with a short keyframe interval. Keep the exact frame rate. Generate a poster. Never modify the source file. Trap: A proxy with a long keyframe interval forces the player to decode from the last keyframe on every seek, which makes scrubbing feel broken even though playback is fine. Use a short interval, accept the larger file, and remember that reviewers judge this tool on scrubbing rather than on playback. Done when: Scrubbing the proxy updates frames without a visible delay. The proxy's frame rate and duration in frames match the source exactly. A file still being copied is not transcoded until the copy completes. The source file checksum is unchanged. Do not build yet: No sharing or comments yet. ### Phase 3 · A review link that plays for a client Build: An unlisted share page with an unguessable token, optional password and expiry. The player displays the current timecode and steps a frame at a time with arrow keys. Serve the proxy behind a signature so the file is not reachable by guessing. Trap: An unguessable page URL with a predictable video file URL means the video circulates without the page, which for unreleased client work is exactly what the review link was supposed to prevent. Sign the media requests and keep the file private. Done when: A share link plays for someone with no account. A direct request to the proxy without a signature is refused. Arrow keys step exactly one frame and the displayed timecode advances by one. An expired share refuses rather than showing an empty page. Do not build yet: No comments yet. ### Phase 4 · Comments pinned to a frame Build: Comments anchored to a frame number, with optional drawing over the frame stored in normalised coordinates. Markers on the timeline. Clicking a comment seeks to its frame and shows its drawing. Trap: Storing an annotation in pixel coordinates makes it land in the wrong place on any other display size or if the proxy resolution changes. Normalise to the frame's dimensions, so the drawing scales with whatever the reviewer is watching on. Done when: A comment created while paused on a frame reopens on exactly that frame. A drawing appears in the same place on a different screen size, verified by opening the link on a phone. Timeline markers correspond to comment frames. A comment on a version does not appear on another version. Do not build yet: No versions or export yet. ### Phase 5 · Versions and the notes going back Build: Stack versions of the same shot with labels. Comments belong to a version. Export all notes as timecode in a format your editing application can import as markers, using the exact frame rate and drop-frame flag. Trap: Exporting notes as timecode computed with a rounded frame rate produces markers that drift from the comment position, and the drift grows through the file so the last notes are the most wrong. Use the stored rational rate and the drop-frame flag for every conversion in the export. Done when: A second version stacks under the same share and comments stay with their version. Exported markers land on the intended frames when imported into an editing application, verified for a note near the end of a long file. The export names the frame rate and drop-frame status. Comparing two versions is possible from the share page. Do not build yet: Operations and cleanup come last. ### Phase 6 · Storage, bandwidth and cleanup Build: Retention removing proxies past an age unless the project is active, with a warning first. Bandwidth and storage reporting. Backups of the comment database, which is the part that is not regenerable. Archive a project as notes plus a manifest. Trap: Proxies are regenerable from the source and comments are not, so the backup priority is inverted from what the file sizes suggest. Back up the small database religiously and treat the large proxies as a cache. Done when: Retention removes aged proxies after a warning and keeps active projects. Deleting a proxy leaves the comments intact and the share page explains rather than breaking. Storage and bandwidth per project are reported. A restore of the database reproduces every comment with correct frames. Do not build yet: Nothing after this. ### Out of scope (and why) - Camera to cloud ingest straight off set, which is a hardware and network integration rather than software. - Review panels inside editing applications that sync comments to timeline markers, which are per-application extensions and are most of the daily convenience. - A transcode farm and a global delivery network, so a client on another continent gets whatever your server can send. - The reliability of a service clients already trust, which matters when the review is with someone paying you. ### README must contain - That frame rate is stored as a rational and why 29.97 as a decimal is wrong - That drop-frame timecode skips numbers rather than frames, and how that is handled - That the proxy uses a short keyframe interval because reviewers judge scrubbing - That annotations are stored in normalised coordinates so they scale - That proxies are a regenerable cache and the comment database is the thing to back up ===== ARCHITECTURE.md ===== # Architecture · Frame.io ## Stack | Part | Choice | Why | | --- | --- | --- | | Transcode | ffmpeg producing a review proxy with a fixed keyframe interval | frequent keyframes make scrubbing responsive, which is what reviewers actually judge the tool on | | Runtime | Node 22 with SQLite | a handful of assets and a few hundred comments | | Player | A player driven by frame number, not seconds | the comment anchor is a frame and every seek and display derives from it | | Storage | Local disk or S3-compatible storage behind a reverse proxy | review proxies are large and originals should not leave your machine unless you decide | | Watching | A folder watcher with a settle check | an export still being written must not be transcoded | ## Modules Each module has one owner concern and a documented way to replace it. | Module | Owns | How to replace it | | --- | --- | --- | | Timecode | Rational frame rates and drop-frame conversion. | Not swappable casually. Every comment, marker and export depends on it being exact. | | Transcode | Proxies optimised for scrubbing. | ffmpeg is the tool. The short keyframe interval is the decision reviewers feel. | | Share | Unlisted access and signed media. | Any gate, provided the media itself is not reachable without one. | | Comments | Frame anchors and normalised annotations. | The frame anchor and normalised coordinates are correctness properties rather than implementation details. | | Export | Markers for an editing application. | Formats differ per application. The exact conversion does not. | ## Configuration Every runtime setting is an environment variable documented in `.env.example`, validated at startup, with a safe local default wherever one exists. - `WATCH_DIR` · required · Folder watched for new exports. Files here are never modified. - `PROXY_DIR` · required · Where review proxies are written. Treat as a cache. - `PROXY_KEYFRAME_FRAMES` · required · Keyframe interval in frames. Short for scrubbing, at the cost of file size. - `PROXY_CRF` · required · Quality setting for the proxy. Lower is better and larger. - `SIGNED_MEDIA_TTL_SECONDS` · required · How long a signed media URL lasts. Long enough for a review session. - `PROXY_RETENTION_DAYS` · required · Age at which an inactive project's proxies are removed, after a warning. - `PUBLIC_URL` · required · Used in share links. Must be HTTPS. ## Production baseline - Security: least privilege, input validation at every boundary, secret redaction in logs, rate limits on abuse-prone paths, no invented security primitives. - Data: explicit schema and migrations, transactional writes where integrity matters, backup and restore procedures that have been exercised. - Integrations: adapters around third-party providers, idempotent webhook or job processing, bounded retries, timeouts. - Observability: structured logs with request or operation ids, an error-tracking hook, and health and readiness checks where a server exists. - Quality: unit tests for domain rules, integration tests at module boundaries, one end-to-end test of the critical path. ## Decision records For each dependency in the stack table, keep a short note: why it was chosen, its failure mode, and how it is replaced. Do not add infrastructure until a requirement in `PRODUCT.md` justifies it. ===== AGENTS.md ===== # Agent instructions · Frame.io product build - Read `PRODUCT.md` and `ARCHITECTURE.md` before changing code. The stack is fixed: ffmpeg producing a review proxy with a fixed keyframe interval, Node 22 with SQLite, A player driven by frame number, not seconds, Local disk or S3-compatible storage behind a reverse proxy, A folder watcher with a settle check. - Implement milestone by milestone from `MILESTONES.md`; keep each change reviewable and leave the application runnable at every commit. - Treat authentication, payments, encryption, imports, webhooks and destructive actions as high-risk boundaries when present. - Never invent cryptography or silently weaken a requirement to make a check pass. - Put every external service behind an interface with a deterministic fake for tests. - Add migrations and rollback or recovery notes for every persistent data change. - Log useful operational context without credentials, tokens, passwords or personal data. - Update documentation and run every check before completing a milestone. ## Known traps - Storing 29.97 rather than 30000 over 1001 accumulates error, and after an hour the computed frame is wrong. Keep the rate rational throughout. - Drop-frame timecode skips frame numbers rather than frames: it omits numbers 0 and 1 at the start of each minute except every tenth minute, so wall clock and timecode stay close. Converting a drop-frame timecode with non-drop arithmetic is wrong by several seconds an hour, and the error grows. - A proxy with a long keyframe interval forces the player to decode from the last keyframe on every seek, which makes scrubbing feel broken even though playback is fine. Use a short interval, accept the larger file, and remember that reviewers judge this tool on scrubbing rather than on playback. - An unguessable page URL with a predictable video file URL means the video circulates without the page, which for unreleased client work is exactly what the review link was supposed to prevent. Sign the media requests and keep the file private. - Storing an annotation in pixel coordinates makes it land in the wrong place on any other display size or if the proxy resolution changes. Normalise to the frame's dimensions, so the drawing scales with whatever the reviewer is watching on. - Exporting notes as timecode computed with a rounded frame rate produces markers that drift from the comment position, and the drift grows through the file so the last notes are the most wrong. Use the stored rational rate and the drop-frame flag for every conversion in the export. - Proxies are regenerable from the source and comments are not, so the backup priority is inverted from what the file sizes suggest. Back up the small database religiously and treat the large proxies as a cache. ===== MILESTONES.md ===== # Delivery milestones · Frame.io Estimated effort: **weekend** for the indie phases; the production-only milestones add the trust and operability layer. ## M1 · Frame rates and timecode, before anything else You can convert between frame number and timecode correctly for a non-integer rate. ### Steps 1. Probe for the exact rate Read the rational frame rate rather than the rounded one, plus duration and start timecode. Files: `src/probe.js` ```sh ffprobe -v error -select_streams v:0 -show_entries stream=r_frame_rate,nb_frames -show_entries format_tags=timecode -of json input.mov ``` 2. Implement both conversions Frame to timecode and back, handling drop-frame and a non-zero start. Files: `src/timecode.js` 3. Test exhaustively over an hour Every frame of a one hour file, both directions. This is cheap and catches every rounding mistake. Files: `test/timecode.test.js` ### Done when - [ ] Frame rate is stored and reported as a rational - [ ] Round trip is exact for every frame in a one hour file - [ ] Drop-frame timecode at one hour matches the expected value - [ ] A non-zero start timecode is handled correctly ### Watch out - Storing 29.97 rather than 30000 over 1001 accumulates error, and after an hour the computed frame is wrong. Keep the rate rational throughout. - Drop-frame timecode skips frame numbers rather than frames: it omits numbers 0 and 1 at the start of each minute except every tenth minute, so wall clock and timecode stay close. Converting a drop-frame timecode with non-drop arithmetic is wrong by several seconds an hour, and the error grows. ## M2 · A proxy that scrubs well A reviewer can drag the playhead and see frames update immediately. ### Steps 1. Watch with a settle check Size stable across two checks before touching a file. An export in progress is the common case here. Files: `src/watch.js` 2. Transcode with frequent keyframes Short interval for scrubbing, matching the source frame rate exactly rather than normalising it. ```sh ffmpeg -i source.mov -c:v libx264 -g 12 -keyint_min 12 -sc_threshold 0 -crf 23 -c:a aac proxy.mp4 ``` 3. Verify frame counts match Source and proxy must have the same duration in frames, or every comment is offset. Files: `src/verify.js` ### Done when - [ ] Scrubbing updates frames without visible delay - [ ] Proxy frame rate and frame count match the source - [ ] A copying file is not transcoded early - [ ] The source checksum is unchanged ### Watch out - A proxy with a long keyframe interval forces the player to decode from the last keyframe on every seek, which makes scrubbing feel broken even though playback is fine. Use a short interval, accept the larger file, and remember that reviewers judge this tool on scrubbing rather than on playback. ## M3 · A review link that plays for a client Someone with the link can watch and scrub without an account. ### Steps 1. Serve an unlisted share page Unguessable token, optional password, expiry. No public listing anywhere. Files: `src/share.js` 2. Sign media requests The proxy is private and reachable only through a short-lived signature issued after the gate. Files: `src/media.js` 3. Drive the player by frame Arrow keys step one frame, the display shows timecode, and every seek is computed from a frame number. Files: `public/player.js` ### Done when - [ ] A share link plays for someone with no account - [ ] An unsigned proxy request is refused - [ ] Arrow keys step exactly one frame - [ ] An expired share refuses access ### Watch out - An unguessable page URL with a predictable video file URL means the video circulates without the page, which for unreleased client work is exactly what the review link was supposed to prevent. Sign the media requests and keep the file private. ## M4 · Comments pinned to a frame A comment lands on the exact frame the reviewer was looking at. ### Steps 1. Anchor comments to frame numbers Not seconds. The frame is what the reviewer saw and what the editor needs. Files: `src/comments.js` 2. Normalise annotation coordinates Relative to frame dimensions, so drawings scale with the display. Files: `public/annotate.js` 3. Verify across screen sizes Open the same link on a phone and a desktop. The drawing must land in the same place. ### Done when - [ ] A comment reopens on exactly its frame - [ ] A drawing lands correctly on a different screen size - [ ] Timeline markers match comment frames - [ ] Comments do not leak between versions ### Watch out - Storing an annotation in pixel coordinates makes it land in the wrong place on any other display size or if the proxy resolution changes. Normalise to the frame's dimensions, so the drawing scales with whatever the reviewer is watching on. ## M5 · Versions and the notes going back A client can compare versions, and you get every note as timecode. ### Steps 1. Stack versions in a group One share, several versions, comments per version, with a visible current version. Files: `src/versions.js` 2. Export markers with exact conversion Rational rate and drop-frame flag applied. Verify a note near the end rather than the start. Files: `src/export.js` 3. Check in a real editing application Import the markers and confirm placement. This is the step that reveals a rounding error. ### Done when - [ ] Versions stack and comments stay with their version - [ ] Exported markers land correctly, checked near the end of a long file - [ ] The export names frame rate and drop-frame status - [ ] Two versions can be compared from the share page ### Watch out - Exporting notes as timecode computed with a rounded frame rate produces markers that drift from the comment position, and the drift grows through the file so the last notes are the most wrong. Use the stored rational rate and the drop-frame flag for every conversion in the export. ## M6 · Storage, bandwidth and cleanup (production only) Proxies do not fill the disk and a finished project can be archived. ### Steps 1. Retire proxies, keep comments Proxies are a cache. Regenerate from source if a project reopens. Files: `src/retention.js` 2. Report storage and bandwidth Per project. Client review of a long cut moves real bandwidth. Files: `src/usage.js` 3. Back up the database and archive projects Notes plus a manifest, readable without this application. Files: `scripts/backup.sh`, `src/archive.js` ### Done when - [ ] Retention removes aged proxies after a warning - [ ] A removed proxy leaves comments intact with an explanatory page - [ ] Storage and bandwidth per project are reported - [ ] A database restore reproduces comments with correct frames ### Watch out - Proxies are regenerable from the source and comments are not, so the backup priority is inverted from what the file sizes suggest. Back up the small database religiously and treat the large proxies as a cache. ===== OPERATIONS.md ===== # Operations · Frame.io ## Backup The comment database is small and irreplaceable; proxies are large and regenerable from sources. Back up the first religiously and treat the second as a cache, which is the opposite of what the file sizes suggest. ## Restore Restore the database, regenerate proxies from sources as projects reopen, and confirm a comment lands on the frame it names. Do a restore drill before the first real user, and write the date here when it passes. ## Monitoring Disk use, outbound bandwidth per project and transcode failures. Bandwidth is the cost that surprises people when a client reviews a long cut several times. ## Incident checklist If comments appear on the wrong frames, the frame rate handling is the first suspect, and the exhaustive round trip test from Phase 1 is where to look. If a proxy is missing, the share page should explain rather than break, which is what makes retention safe. 1. Contain the issue without destroying evidence or user data. 2. Record the timeline and affected scope. 3. Rotate exposed secrets and revoke compromised sessions or credentials. 4. Restore from a verified backup when needed. 5. Document the root cause, the remediation and the regression test. ## Release gate - [ ] Frame to timecode round trip is exact for every frame in a one hour file - [ ] Drop-frame conversion matches expected values at one hour - [ ] An unsigned proxy request is confirmed refused - [ ] Exported markers are confirmed correct near the end of a long file - [ ] A database restore reproduces comments on correct frames ## Launch constraint Do not market omitted Frame.io capabilities as implemented. The non-goals in `PRODUCT.md` remain user-visible limitations until they are deliberately delivered. ===== .env.example ===== # Copy to .env and fill in. Never commit .env; this file documents it. # Required. Folder watched for new exports. Files here are never modified. WATCH_DIR=./incoming # Required. Where review proxies are written. Treat as a cache. PROXY_DIR=./proxies # Required. Keyframe interval in frames. Short for scrubbing, at the cost of file size. PROXY_KEYFRAME_FRAMES=12 # Required. Quality setting for the proxy. Lower is better and larger. PROXY_CRF=23 # Required. How long a signed media URL lasts. Long enough for a review session. SIGNED_MEDIA_TTL_SECONDS=7200 # Required. Age at which an inactive project's proxies are removed, after a warning. PROXY_RETENTION_DAYS=120 # Required. Used in share links. Must be HTTPS. PUBLIC_URL=https://review.example.com
# Frame.io · indie build A watch folder that transcodes a review proxy, an unlisted page where reviewers scrub and comment on an exact frame, versions stacked so a client can compare, and an export of every note as timecode your editing application understands. Estimated effort: **weekend**. Work `BUILD_PLAN.md` top to bottom · every phase ends in a check that has to pass before the next one starts. ## Stack | Part | Choice | Why | | --- | --- | --- | | Transcode | ffmpeg producing a review proxy with a fixed keyframe interval | frequent keyframes make scrubbing responsive, which is what reviewers actually judge the tool on | | Runtime | Node 22 with SQLite | a handful of assets and a few hundred comments | | Player | A player driven by frame number, not seconds | the comment anchor is a frame and every seek and display derives from it | | Storage | Local disk or S3-compatible storage behind a reverse proxy | review proxies are large and originals should not leave your machine unless you decide | | Watching | A folder watcher with a settle check | an export still being written must not be transcoded | ## Before you start Have every one of these ready. The plan assumes them from step one. - [ ] **Node.js 22 or newer** · free - Why: Runs the watcher, the server and the share pages. - Get it: nodejs.org, or nvm install 22. - Verify: node --version prints v22 or higher - [ ] **ffmpeg and ffprobe** · free - Why: Probing for the exact frame rate and transcoding proxies both run through them. - Get it: brew install ffmpeg or your package manager. - Verify: ffprobe reports a rational frame rate for a test file - [ ] **Two source files, one at a non-integer frame rate and one over an hour** · free - Why: The timecode work is only testable against a real 29.97 file, and rounding errors only become visible at length. - Get it: Use your own footage. If you have a drop-frame file, use that; otherwise generate one at 30000 over 1001. - Verify: Both are available and one reports a non-integer rational rate - [ ] **A server with real disk and bandwidth** · 10 to 40 US dollars a month depending on storage and transfer - Why: Proxies are large and clients stream them. A small instance will transcode slowly and serve worse. - Get it: A virtual server with enough disk for your active projects and bandwidth you have checked the price of. - Verify: A proxy transcodes and streams smoothly to another network - [ ] **An editing application to verify marker import** · free if you already have one - Why: Phase 5's export is only correct if the markers land where the comments were, and only the editing application can tell you. - Get it: Whichever you already use. The marker import format differs, so target the one you actually work in. - Verify: Exported markers import and land on the intended frames ## Quick start ```sh ffprobe -v error -select_streams v:0 -show_entries stream=r_frame_rate,nb_frames -show_entries format_tags=timecode -of json input.mov ``` Then copy `.env.example` to `.env` and fill in the values it documents. ## Honest limits This build deliberately does not replace: - Camera to cloud ingest. - Review panels inside editing applications. - A transcode farm and a global delivery network. - The reliability clients already expect from a service. - Camera to Cloud ingest straight off set - Premiere, After Effects, and Resolve review panels that sync comments to timeline markers - a transcode farm and global CDN, so client playback is only as good as your box - forensic watermarking, SSO, and the security posture studios require - the iPad and mobile review apps clients actually use - storage, transfer tooling, and uptime bundled into the seat price If one of those is essential to you, that is the reason to keep paying for Frame.io, and the README should say so rather than pretend.
# Build brief · Frame.io The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me a review link where clients leave comments pinned to an exact frame, and you get them back as timecode to replace Frame.io. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Rule zero Frame accuracy is the whole product, and it is where this gets subtle. Video frame rates are often not integers: what everyone calls 30 frames per second is usually 30000 divided by 1001, or about 29.97. A comment stored as a time in seconds and converted back with the wrong rate lands on the wrong frame, and a note that says the wrong frame is worse than no note. So store the frame number, carry the exact rational frame rate with every asset, and know whether the timecode is drop-frame before you display any. Get that right in Phase 1 and the rest is straightforward. ### Stack (fixed, do not substitute) - Transcode: ffmpeg producing a review proxy with a fixed keyframe interval · frequent keyframes make scrubbing responsive, which is what reviewers actually judge the tool on - Runtime: Node 22 with SQLite · a handful of assets and a few hundred comments - Player: A player driven by frame number, not seconds · the comment anchor is a frame and every seek and display derives from it - Storage: Local disk or S3-compatible storage behind a reverse proxy · review proxies are large and originals should not leave your machine unless you decide - Watching: A folder watcher with a settle check · an export still being written must not be transcoded ### Data model (create this before Phase 1) - assets(id, source_path, title, frame_rate_num, frame_rate_den, drop_frame, duration_frames, timecode_start_frames, proxy_key, state) - versions(id, asset_group_id, asset_id, ordinal, label, uploaded_at) · a group is one shot reviewed repeatedly - shares(id, asset_group_id, token, password_hash, expires_at, allow_download, created_at) - comments(id, version_id, frame, author_name, body, created_at, resolved_at, parent_id) - annotations(comment_id, shape_json) · a drawing over the frame, stored in normalised coordinates - The frame rate is stored as a numerator and a denominator. Storing it as a decimal is the bug that produces off-by-one frames an hour into a long file. ### Phase 1 · Frame rates and timecode, before anything else Build: Probe a source file for its exact frame rate as a rational, its duration in frames, its starting timecode and whether it is drop-frame. Write conversions between frame number and timecode both ways, and test them against a long file. Trap: Storing 29.97 rather than 30000 over 1001 accumulates error, and after an hour the computed frame is wrong. Keep the rate rational throughout. Trap: Drop-frame timecode skips frame numbers rather than frames: it omits numbers 0 and 1 at the start of each minute except every tenth minute, so wall clock and timecode stay close. Converting a drop-frame timecode with non-drop arithmetic is wrong by several seconds an hour, and the error grows. Done when: Frame rate is stored as a rational and reported exactly. Converting frame to timecode and back returns the original frame for every frame in a one hour file, tested exhaustively. A drop-frame file's timecode at one hour matches the expected value. A file with a non-zero starting timecode reports frames relative to it correctly. Do not build yet: No transcoding, sharing or comments. ### Phase 2 · A proxy that scrubs well Build: Watch a folder, wait for files to settle, and transcode a review proxy with a short keyframe interval. Keep the exact frame rate. Generate a poster. Never modify the source file. Trap: A proxy with a long keyframe interval forces the player to decode from the last keyframe on every seek, which makes scrubbing feel broken even though playback is fine. Use a short interval, accept the larger file, and remember that reviewers judge this tool on scrubbing rather than on playback. Done when: Scrubbing the proxy updates frames without a visible delay. The proxy's frame rate and duration in frames match the source exactly. A file still being copied is not transcoded until the copy completes. The source file checksum is unchanged. Do not build yet: No sharing or comments yet. ### Phase 3 · A review link that plays for a client Build: An unlisted share page with an unguessable token, optional password and expiry. The player displays the current timecode and steps a frame at a time with arrow keys. Serve the proxy behind a signature so the file is not reachable by guessing. Trap: An unguessable page URL with a predictable video file URL means the video circulates without the page, which for unreleased client work is exactly what the review link was supposed to prevent. Sign the media requests and keep the file private. Done when: A share link plays for someone with no account. A direct request to the proxy without a signature is refused. Arrow keys step exactly one frame and the displayed timecode advances by one. An expired share refuses rather than showing an empty page. Do not build yet: No comments yet. ### Phase 4 · Comments pinned to a frame Build: Comments anchored to a frame number, with optional drawing over the frame stored in normalised coordinates. Markers on the timeline. Clicking a comment seeks to its frame and shows its drawing. Trap: Storing an annotation in pixel coordinates makes it land in the wrong place on any other display size or if the proxy resolution changes. Normalise to the frame's dimensions, so the drawing scales with whatever the reviewer is watching on. Done when: A comment created while paused on a frame reopens on exactly that frame. A drawing appears in the same place on a different screen size, verified by opening the link on a phone. Timeline markers correspond to comment frames. A comment on a version does not appear on another version. Do not build yet: No versions or export yet. ### Phase 5 · Versions and the notes going back Build: Stack versions of the same shot with labels. Comments belong to a version. Export all notes as timecode in a format your editing application can import as markers, using the exact frame rate and drop-frame flag. Trap: Exporting notes as timecode computed with a rounded frame rate produces markers that drift from the comment position, and the drift grows through the file so the last notes are the most wrong. Use the stored rational rate and the drop-frame flag for every conversion in the export. Done when: A second version stacks under the same share and comments stay with their version. Exported markers land on the intended frames when imported into an editing application, verified for a note near the end of a long file. The export names the frame rate and drop-frame status. Comparing two versions is possible from the share page. Do not build yet: Operations and cleanup come last. ### Phase 6 · Storage, bandwidth and cleanup Build: Retention removing proxies past an age unless the project is active, with a warning first. Bandwidth and storage reporting. Backups of the comment database, which is the part that is not regenerable. Archive a project as notes plus a manifest. Trap: Proxies are regenerable from the source and comments are not, so the backup priority is inverted from what the file sizes suggest. Back up the small database religiously and treat the large proxies as a cache. Done when: Retention removes aged proxies after a warning and keeps active projects. Deleting a proxy leaves the comments intact and the share page explains rather than breaking. Storage and bandwidth per project are reported. A restore of the database reproduces every comment with correct frames. Do not build yet: Nothing after this. ### Out of scope (and why) - Camera to cloud ingest straight off set, which is a hardware and network integration rather than software. - Review panels inside editing applications that sync comments to timeline markers, which are per-application extensions and are most of the daily convenience. - A transcode farm and a global delivery network, so a client on another continent gets whatever your server can send. - The reliability of a service clients already trust, which matters when the review is with someone paying you. ### README must contain - That frame rate is stored as a rational and why 29.97 as a decimal is wrong - That drop-frame timecode skips numbers rather than frames, and how that is handled - That the proxy uses a short keyframe interval because reviewers judge scrubbing - That annotations are stored in normalised coordinates so they scale - That proxies are a regenerable cache and the comment database is the thing to back up
# Agent instructions · Frame.io indie build - Read `README.md` and `BUILD_PLAN.md` before writing code. The stack is fixed: ffmpeg producing a review proxy with a fixed keyframe interval, Node 22 with SQLite, A player driven by frame number, not seconds, Local disk or S3-compatible storage behind a reverse proxy, A folder watcher with a settle check. Do not substitute. - Work one phase at a time, in order. Do not start a phase until every "Done when" item of the previous one passes. - Prefer the fewest moving parts that satisfy the step. No frameworks, services or dependencies the plan does not name. - Secrets live in `.env`, never in source or logs. Keep `.env.example` current when a variable is introduced. - Do not invent cryptography, security guarantees, APIs or compliance claims. - Add a focused test for every destructive, security-sensitive or data-loss path the plan names. - Run the project checks before declaring a phase complete, and record any deliberate shortcut in the README under "Tradeoffs". ## Known traps - Storing 29.97 rather than 30000 over 1001 accumulates error, and after an hour the computed frame is wrong. Keep the rate rational throughout. - Drop-frame timecode skips frame numbers rather than frames: it omits numbers 0 and 1 at the start of each minute except every tenth minute, so wall clock and timecode stay close. Converting a drop-frame timecode with non-drop arithmetic is wrong by several seconds an hour, and the error grows. - A proxy with a long keyframe interval forces the player to decode from the last keyframe on every seek, which makes scrubbing feel broken even though playback is fine. Use a short interval, accept the larger file, and remember that reviewers judge this tool on scrubbing rather than on playback. - An unguessable page URL with a predictable video file URL means the video circulates without the page, which for unreleased client work is exactly what the review link was supposed to prevent. Sign the media requests and keep the file private. - Storing an annotation in pixel coordinates makes it land in the wrong place on any other display size or if the proxy resolution changes. Normalise to the frame's dimensions, so the drawing scales with whatever the reviewer is watching on. - Exporting notes as timecode computed with a rounded frame rate produces markers that drift from the comment position, and the drift grows through the file so the last notes are the most wrong. Use the stored rational rate and the drop-frame flag for every conversion in the export.
# Build plan · Frame.io A watch folder that transcodes a review proxy, an unlisted page where reviewers scrub and comment on an exact frame, versions stacked so a client can compare, and an export of every note as timecode your editing application understands. Phases are in dependency order. Each ends in a "Done when" list; treat an unticked item as a blocker, not a note. ## Phase 1 · Frame rates and timecode, before anything else You can convert between frame number and timecode correctly for a non-integer rate. ### Steps 1. Probe for the exact rate Read the rational frame rate rather than the rounded one, plus duration and start timecode. Files: `src/probe.js` ```sh ffprobe -v error -select_streams v:0 -show_entries stream=r_frame_rate,nb_frames -show_entries format_tags=timecode -of json input.mov ``` 2. Implement both conversions Frame to timecode and back, handling drop-frame and a non-zero start. Files: `src/timecode.js` 3. Test exhaustively over an hour Every frame of a one hour file, both directions. This is cheap and catches every rounding mistake. Files: `test/timecode.test.js` ### Done when - [ ] Frame rate is stored and reported as a rational - [ ] Round trip is exact for every frame in a one hour file - [ ] Drop-frame timecode at one hour matches the expected value - [ ] A non-zero start timecode is handled correctly ### Watch out - Storing 29.97 rather than 30000 over 1001 accumulates error, and after an hour the computed frame is wrong. Keep the rate rational throughout. - Drop-frame timecode skips frame numbers rather than frames: it omits numbers 0 and 1 at the start of each minute except every tenth minute, so wall clock and timecode stay close. Converting a drop-frame timecode with non-drop arithmetic is wrong by several seconds an hour, and the error grows. ## Phase 2 · A proxy that scrubs well A reviewer can drag the playhead and see frames update immediately. ### Steps 1. Watch with a settle check Size stable across two checks before touching a file. An export in progress is the common case here. Files: `src/watch.js` 2. Transcode with frequent keyframes Short interval for scrubbing, matching the source frame rate exactly rather than normalising it. ```sh ffmpeg -i source.mov -c:v libx264 -g 12 -keyint_min 12 -sc_threshold 0 -crf 23 -c:a aac proxy.mp4 ``` 3. Verify frame counts match Source and proxy must have the same duration in frames, or every comment is offset. Files: `src/verify.js` ### Done when - [ ] Scrubbing updates frames without visible delay - [ ] Proxy frame rate and frame count match the source - [ ] A copying file is not transcoded early - [ ] The source checksum is unchanged ### Watch out - A proxy with a long keyframe interval forces the player to decode from the last keyframe on every seek, which makes scrubbing feel broken even though playback is fine. Use a short interval, accept the larger file, and remember that reviewers judge this tool on scrubbing rather than on playback. ## Phase 3 · A review link that plays for a client Someone with the link can watch and scrub without an account. ### Steps 1. Serve an unlisted share page Unguessable token, optional password, expiry. No public listing anywhere. Files: `src/share.js` 2. Sign media requests The proxy is private and reachable only through a short-lived signature issued after the gate. Files: `src/media.js` 3. Drive the player by frame Arrow keys step one frame, the display shows timecode, and every seek is computed from a frame number. Files: `public/player.js` ### Done when - [ ] A share link plays for someone with no account - [ ] An unsigned proxy request is refused - [ ] Arrow keys step exactly one frame - [ ] An expired share refuses access ### Watch out - An unguessable page URL with a predictable video file URL means the video circulates without the page, which for unreleased client work is exactly what the review link was supposed to prevent. Sign the media requests and keep the file private. ## Phase 4 · Comments pinned to a frame A comment lands on the exact frame the reviewer was looking at. ### Steps 1. Anchor comments to frame numbers Not seconds. The frame is what the reviewer saw and what the editor needs. Files: `src/comments.js` 2. Normalise annotation coordinates Relative to frame dimensions, so drawings scale with the display. Files: `public/annotate.js` 3. Verify across screen sizes Open the same link on a phone and a desktop. The drawing must land in the same place. ### Done when - [ ] A comment reopens on exactly its frame - [ ] A drawing lands correctly on a different screen size - [ ] Timeline markers match comment frames - [ ] Comments do not leak between versions ### Watch out - Storing an annotation in pixel coordinates makes it land in the wrong place on any other display size or if the proxy resolution changes. Normalise to the frame's dimensions, so the drawing scales with whatever the reviewer is watching on. ## Phase 5 · Versions and the notes going back A client can compare versions, and you get every note as timecode. ### Steps 1. Stack versions in a group One share, several versions, comments per version, with a visible current version. Files: `src/versions.js` 2. Export markers with exact conversion Rational rate and drop-frame flag applied. Verify a note near the end rather than the start. Files: `src/export.js` 3. Check in a real editing application Import the markers and confirm placement. This is the step that reveals a rounding error. ### Done when - [ ] Versions stack and comments stay with their version - [ ] Exported markers land correctly, checked near the end of a long file - [ ] The export names frame rate and drop-frame status - [ ] Two versions can be compared from the share page ### Watch out - Exporting notes as timecode computed with a rounded frame rate produces markers that drift from the comment position, and the drift grows through the file so the last notes are the most wrong. Use the stored rational rate and the drop-frame flag for every conversion in the export. ## Not in this build - Camera to cloud ingest. - Review panels inside editing applications. - A transcode farm and a global delivery network. - The reliability clients already expect from a service. ## After v1, if you want it - Side by side version comparison with synchronised playheads - Comment status and approval, so a version can be signed off - Automatic proxy regeneration when an archived project is reopened
# Copy to .env and fill in. Never commit .env; this file documents it. # Required. Folder watched for new exports. Files here are never modified. WATCH_DIR=./incoming # Required. Where review proxies are written. Treat as a cache. PROXY_DIR=./proxies # Required. Keyframe interval in frames. Short for scrubbing, at the cost of file size. PROXY_KEYFRAME_FRAMES=12 # Required. Quality setting for the proxy. Lower is better and larger. PROXY_CRF=23 # Required. How long a signed media URL lasts. Long enough for a review session. SIGNED_MEDIA_TTL_SECONDS=7200 # Required. Age at which an inactive project's proxies are removed, after a warning. PROXY_RETENTION_DAYS=120 # Required. Used in share links. Must be HTTPS. PUBLIC_URL=https://review.example.com
# Frame.io · product brief ## Problem The review loop itself is not the hard part: a player, timecoded comments, version stacking, and an unlisted share link are a weekend of work for one editor and a few clients. What you cannot rebuild is the rest of the product · Camera to Cloud from set, the Premiere and Resolve panels that put comments on your timeline as markers, transcoding and CDN delivery that hold up when a client scrubs a 4K cut from a hotel wifi, and the security paperwork studios ask for. Solo and small-shop review is genuinely vibecodeable. Delivery infrastructure is not. ## Product outcome A review link where a client's comment lands on the exact frame, drawings scale to any screen, versions stack for comparison, and every note comes back as timecode that imports correctly into your timeline. ## Target user A builder who needs a maintainable product foundation, not a one-off demo. ## Required capabilities - ffmpeg - a VPS with real disk and bandwidth - a domain plus reverse proxy with TLS - chokidar or another folder watcher - a storage and egress budget you now pay yourself ## Explicit non-goals for v1 - Camera to cloud ingest. - Review panels inside editing applications. - A transcode farm and a global delivery network. - The reliability clients already expect from a service. - Camera to Cloud ingest straight off set - Premiere, After Effects, and Resolve review panels that sync comments to timeline markers - a transcode farm and global CDN, so client playback is only as good as your box - forensic watermarking, SSO, and the security posture studios require - the iPad and mobile review apps clients actually use - storage, transfer tooling, and uptime bundled into the seat price ## Success criteria - Frame to timecode round trip is exact for every frame in a one hour file - Drop-frame conversion matches expected values at one hour - An unsigned proxy request is confirmed refused - Exported markers are confirmed correct near the end of a long file - A database restore reproduces comments on correct frames
# Build brief · Frame.io The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me a review link where clients leave comments pinned to an exact frame, and you get them back as timecode to replace Frame.io. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Rule zero Frame accuracy is the whole product, and it is where this gets subtle. Video frame rates are often not integers: what everyone calls 30 frames per second is usually 30000 divided by 1001, or about 29.97. A comment stored as a time in seconds and converted back with the wrong rate lands on the wrong frame, and a note that says the wrong frame is worse than no note. So store the frame number, carry the exact rational frame rate with every asset, and know whether the timecode is drop-frame before you display any. Get that right in Phase 1 and the rest is straightforward. ### Stack (fixed, do not substitute) - Transcode: ffmpeg producing a review proxy with a fixed keyframe interval · frequent keyframes make scrubbing responsive, which is what reviewers actually judge the tool on - Runtime: Node 22 with SQLite · a handful of assets and a few hundred comments - Player: A player driven by frame number, not seconds · the comment anchor is a frame and every seek and display derives from it - Storage: Local disk or S3-compatible storage behind a reverse proxy · review proxies are large and originals should not leave your machine unless you decide - Watching: A folder watcher with a settle check · an export still being written must not be transcoded ### Data model (create this before Phase 1) - assets(id, source_path, title, frame_rate_num, frame_rate_den, drop_frame, duration_frames, timecode_start_frames, proxy_key, state) - versions(id, asset_group_id, asset_id, ordinal, label, uploaded_at) · a group is one shot reviewed repeatedly - shares(id, asset_group_id, token, password_hash, expires_at, allow_download, created_at) - comments(id, version_id, frame, author_name, body, created_at, resolved_at, parent_id) - annotations(comment_id, shape_json) · a drawing over the frame, stored in normalised coordinates - The frame rate is stored as a numerator and a denominator. Storing it as a decimal is the bug that produces off-by-one frames an hour into a long file. ### Phase 1 · Frame rates and timecode, before anything else Build: Probe a source file for its exact frame rate as a rational, its duration in frames, its starting timecode and whether it is drop-frame. Write conversions between frame number and timecode both ways, and test them against a long file. Trap: Storing 29.97 rather than 30000 over 1001 accumulates error, and after an hour the computed frame is wrong. Keep the rate rational throughout. Trap: Drop-frame timecode skips frame numbers rather than frames: it omits numbers 0 and 1 at the start of each minute except every tenth minute, so wall clock and timecode stay close. Converting a drop-frame timecode with non-drop arithmetic is wrong by several seconds an hour, and the error grows. Done when: Frame rate is stored as a rational and reported exactly. Converting frame to timecode and back returns the original frame for every frame in a one hour file, tested exhaustively. A drop-frame file's timecode at one hour matches the expected value. A file with a non-zero starting timecode reports frames relative to it correctly. Do not build yet: No transcoding, sharing or comments. ### Phase 2 · A proxy that scrubs well Build: Watch a folder, wait for files to settle, and transcode a review proxy with a short keyframe interval. Keep the exact frame rate. Generate a poster. Never modify the source file. Trap: A proxy with a long keyframe interval forces the player to decode from the last keyframe on every seek, which makes scrubbing feel broken even though playback is fine. Use a short interval, accept the larger file, and remember that reviewers judge this tool on scrubbing rather than on playback. Done when: Scrubbing the proxy updates frames without a visible delay. The proxy's frame rate and duration in frames match the source exactly. A file still being copied is not transcoded until the copy completes. The source file checksum is unchanged. Do not build yet: No sharing or comments yet. ### Phase 3 · A review link that plays for a client Build: An unlisted share page with an unguessable token, optional password and expiry. The player displays the current timecode and steps a frame at a time with arrow keys. Serve the proxy behind a signature so the file is not reachable by guessing. Trap: An unguessable page URL with a predictable video file URL means the video circulates without the page, which for unreleased client work is exactly what the review link was supposed to prevent. Sign the media requests and keep the file private. Done when: A share link plays for someone with no account. A direct request to the proxy without a signature is refused. Arrow keys step exactly one frame and the displayed timecode advances by one. An expired share refuses rather than showing an empty page. Do not build yet: No comments yet. ### Phase 4 · Comments pinned to a frame Build: Comments anchored to a frame number, with optional drawing over the frame stored in normalised coordinates. Markers on the timeline. Clicking a comment seeks to its frame and shows its drawing. Trap: Storing an annotation in pixel coordinates makes it land in the wrong place on any other display size or if the proxy resolution changes. Normalise to the frame's dimensions, so the drawing scales with whatever the reviewer is watching on. Done when: A comment created while paused on a frame reopens on exactly that frame. A drawing appears in the same place on a different screen size, verified by opening the link on a phone. Timeline markers correspond to comment frames. A comment on a version does not appear on another version. Do not build yet: No versions or export yet. ### Phase 5 · Versions and the notes going back Build: Stack versions of the same shot with labels. Comments belong to a version. Export all notes as timecode in a format your editing application can import as markers, using the exact frame rate and drop-frame flag. Trap: Exporting notes as timecode computed with a rounded frame rate produces markers that drift from the comment position, and the drift grows through the file so the last notes are the most wrong. Use the stored rational rate and the drop-frame flag for every conversion in the export. Done when: A second version stacks under the same share and comments stay with their version. Exported markers land on the intended frames when imported into an editing application, verified for a note near the end of a long file. The export names the frame rate and drop-frame status. Comparing two versions is possible from the share page. Do not build yet: Operations and cleanup come last. ### Phase 6 · Storage, bandwidth and cleanup Build: Retention removing proxies past an age unless the project is active, with a warning first. Bandwidth and storage reporting. Backups of the comment database, which is the part that is not regenerable. Archive a project as notes plus a manifest. Trap: Proxies are regenerable from the source and comments are not, so the backup priority is inverted from what the file sizes suggest. Back up the small database religiously and treat the large proxies as a cache. Done when: Retention removes aged proxies after a warning and keeps active projects. Deleting a proxy leaves the comments intact and the share page explains rather than breaking. Storage and bandwidth per project are reported. A restore of the database reproduces every comment with correct frames. Do not build yet: Nothing after this. ### Out of scope (and why) - Camera to cloud ingest straight off set, which is a hardware and network integration rather than software. - Review panels inside editing applications that sync comments to timeline markers, which are per-application extensions and are most of the daily convenience. - A transcode farm and a global delivery network, so a client on another continent gets whatever your server can send. - The reliability of a service clients already trust, which matters when the review is with someone paying you. ### README must contain - That frame rate is stored as a rational and why 29.97 as a decimal is wrong - That drop-frame timecode skips numbers rather than frames, and how that is handled - That the proxy uses a short keyframe interval because reviewers judge scrubbing - That annotations are stored in normalised coordinates so they scale - That proxies are a regenerable cache and the comment database is the thing to back up
# Architecture · Frame.io ## Stack | Part | Choice | Why | | --- | --- | --- | | Transcode | ffmpeg producing a review proxy with a fixed keyframe interval | frequent keyframes make scrubbing responsive, which is what reviewers actually judge the tool on | | Runtime | Node 22 with SQLite | a handful of assets and a few hundred comments | | Player | A player driven by frame number, not seconds | the comment anchor is a frame and every seek and display derives from it | | Storage | Local disk or S3-compatible storage behind a reverse proxy | review proxies are large and originals should not leave your machine unless you decide | | Watching | A folder watcher with a settle check | an export still being written must not be transcoded | ## Modules Each module has one owner concern and a documented way to replace it. | Module | Owns | How to replace it | | --- | --- | --- | | Timecode | Rational frame rates and drop-frame conversion. | Not swappable casually. Every comment, marker and export depends on it being exact. | | Transcode | Proxies optimised for scrubbing. | ffmpeg is the tool. The short keyframe interval is the decision reviewers feel. | | Share | Unlisted access and signed media. | Any gate, provided the media itself is not reachable without one. | | Comments | Frame anchors and normalised annotations. | The frame anchor and normalised coordinates are correctness properties rather than implementation details. | | Export | Markers for an editing application. | Formats differ per application. The exact conversion does not. | ## Configuration Every runtime setting is an environment variable documented in `.env.example`, validated at startup, with a safe local default wherever one exists. - `WATCH_DIR` · required · Folder watched for new exports. Files here are never modified. - `PROXY_DIR` · required · Where review proxies are written. Treat as a cache. - `PROXY_KEYFRAME_FRAMES` · required · Keyframe interval in frames. Short for scrubbing, at the cost of file size. - `PROXY_CRF` · required · Quality setting for the proxy. Lower is better and larger. - `SIGNED_MEDIA_TTL_SECONDS` · required · How long a signed media URL lasts. Long enough for a review session. - `PROXY_RETENTION_DAYS` · required · Age at which an inactive project's proxies are removed, after a warning. - `PUBLIC_URL` · required · Used in share links. Must be HTTPS. ## Production baseline - Security: least privilege, input validation at every boundary, secret redaction in logs, rate limits on abuse-prone paths, no invented security primitives. - Data: explicit schema and migrations, transactional writes where integrity matters, backup and restore procedures that have been exercised. - Integrations: adapters around third-party providers, idempotent webhook or job processing, bounded retries, timeouts. - Observability: structured logs with request or operation ids, an error-tracking hook, and health and readiness checks where a server exists. - Quality: unit tests for domain rules, integration tests at module boundaries, one end-to-end test of the critical path. ## Decision records For each dependency in the stack table, keep a short note: why it was chosen, its failure mode, and how it is replaced. Do not add infrastructure until a requirement in `PRODUCT.md` justifies it.
# Agent instructions · Frame.io product build - Read `PRODUCT.md` and `ARCHITECTURE.md` before changing code. The stack is fixed: ffmpeg producing a review proxy with a fixed keyframe interval, Node 22 with SQLite, A player driven by frame number, not seconds, Local disk or S3-compatible storage behind a reverse proxy, A folder watcher with a settle check. - Implement milestone by milestone from `MILESTONES.md`; keep each change reviewable and leave the application runnable at every commit. - Treat authentication, payments, encryption, imports, webhooks and destructive actions as high-risk boundaries when present. - Never invent cryptography or silently weaken a requirement to make a check pass. - Put every external service behind an interface with a deterministic fake for tests. - Add migrations and rollback or recovery notes for every persistent data change. - Log useful operational context without credentials, tokens, passwords or personal data. - Update documentation and run every check before completing a milestone. ## Known traps - Storing 29.97 rather than 30000 over 1001 accumulates error, and after an hour the computed frame is wrong. Keep the rate rational throughout. - Drop-frame timecode skips frame numbers rather than frames: it omits numbers 0 and 1 at the start of each minute except every tenth minute, so wall clock and timecode stay close. Converting a drop-frame timecode with non-drop arithmetic is wrong by several seconds an hour, and the error grows. - A proxy with a long keyframe interval forces the player to decode from the last keyframe on every seek, which makes scrubbing feel broken even though playback is fine. Use a short interval, accept the larger file, and remember that reviewers judge this tool on scrubbing rather than on playback. - An unguessable page URL with a predictable video file URL means the video circulates without the page, which for unreleased client work is exactly what the review link was supposed to prevent. Sign the media requests and keep the file private. - Storing an annotation in pixel coordinates makes it land in the wrong place on any other display size or if the proxy resolution changes. Normalise to the frame's dimensions, so the drawing scales with whatever the reviewer is watching on. - Exporting notes as timecode computed with a rounded frame rate produces markers that drift from the comment position, and the drift grows through the file so the last notes are the most wrong. Use the stored rational rate and the drop-frame flag for every conversion in the export. - Proxies are regenerable from the source and comments are not, so the backup priority is inverted from what the file sizes suggest. Back up the small database religiously and treat the large proxies as a cache.
# Delivery milestones · Frame.io Estimated effort: **weekend** for the indie phases; the production-only milestones add the trust and operability layer. ## M1 · Frame rates and timecode, before anything else You can convert between frame number and timecode correctly for a non-integer rate. ### Steps 1. Probe for the exact rate Read the rational frame rate rather than the rounded one, plus duration and start timecode. Files: `src/probe.js` ```sh ffprobe -v error -select_streams v:0 -show_entries stream=r_frame_rate,nb_frames -show_entries format_tags=timecode -of json input.mov ``` 2. Implement both conversions Frame to timecode and back, handling drop-frame and a non-zero start. Files: `src/timecode.js` 3. Test exhaustively over an hour Every frame of a one hour file, both directions. This is cheap and catches every rounding mistake. Files: `test/timecode.test.js` ### Done when - [ ] Frame rate is stored and reported as a rational - [ ] Round trip is exact for every frame in a one hour file - [ ] Drop-frame timecode at one hour matches the expected value - [ ] A non-zero start timecode is handled correctly ### Watch out - Storing 29.97 rather than 30000 over 1001 accumulates error, and after an hour the computed frame is wrong. Keep the rate rational throughout. - Drop-frame timecode skips frame numbers rather than frames: it omits numbers 0 and 1 at the start of each minute except every tenth minute, so wall clock and timecode stay close. Converting a drop-frame timecode with non-drop arithmetic is wrong by several seconds an hour, and the error grows. ## M2 · A proxy that scrubs well A reviewer can drag the playhead and see frames update immediately. ### Steps 1. Watch with a settle check Size stable across two checks before touching a file. An export in progress is the common case here. Files: `src/watch.js` 2. Transcode with frequent keyframes Short interval for scrubbing, matching the source frame rate exactly rather than normalising it. ```sh ffmpeg -i source.mov -c:v libx264 -g 12 -keyint_min 12 -sc_threshold 0 -crf 23 -c:a aac proxy.mp4 ``` 3. Verify frame counts match Source and proxy must have the same duration in frames, or every comment is offset. Files: `src/verify.js` ### Done when - [ ] Scrubbing updates frames without visible delay - [ ] Proxy frame rate and frame count match the source - [ ] A copying file is not transcoded early - [ ] The source checksum is unchanged ### Watch out - A proxy with a long keyframe interval forces the player to decode from the last keyframe on every seek, which makes scrubbing feel broken even though playback is fine. Use a short interval, accept the larger file, and remember that reviewers judge this tool on scrubbing rather than on playback. ## M3 · A review link that plays for a client Someone with the link can watch and scrub without an account. ### Steps 1. Serve an unlisted share page Unguessable token, optional password, expiry. No public listing anywhere. Files: `src/share.js` 2. Sign media requests The proxy is private and reachable only through a short-lived signature issued after the gate. Files: `src/media.js` 3. Drive the player by frame Arrow keys step one frame, the display shows timecode, and every seek is computed from a frame number. Files: `public/player.js` ### Done when - [ ] A share link plays for someone with no account - [ ] An unsigned proxy request is refused - [ ] Arrow keys step exactly one frame - [ ] An expired share refuses access ### Watch out - An unguessable page URL with a predictable video file URL means the video circulates without the page, which for unreleased client work is exactly what the review link was supposed to prevent. Sign the media requests and keep the file private. ## M4 · Comments pinned to a frame A comment lands on the exact frame the reviewer was looking at. ### Steps 1. Anchor comments to frame numbers Not seconds. The frame is what the reviewer saw and what the editor needs. Files: `src/comments.js` 2. Normalise annotation coordinates Relative to frame dimensions, so drawings scale with the display. Files: `public/annotate.js` 3. Verify across screen sizes Open the same link on a phone and a desktop. The drawing must land in the same place. ### Done when - [ ] A comment reopens on exactly its frame - [ ] A drawing lands correctly on a different screen size - [ ] Timeline markers match comment frames - [ ] Comments do not leak between versions ### Watch out - Storing an annotation in pixel coordinates makes it land in the wrong place on any other display size or if the proxy resolution changes. Normalise to the frame's dimensions, so the drawing scales with whatever the reviewer is watching on. ## M5 · Versions and the notes going back A client can compare versions, and you get every note as timecode. ### Steps 1. Stack versions in a group One share, several versions, comments per version, with a visible current version. Files: `src/versions.js` 2. Export markers with exact conversion Rational rate and drop-frame flag applied. Verify a note near the end rather than the start. Files: `src/export.js` 3. Check in a real editing application Import the markers and confirm placement. This is the step that reveals a rounding error. ### Done when - [ ] Versions stack and comments stay with their version - [ ] Exported markers land correctly, checked near the end of a long file - [ ] The export names frame rate and drop-frame status - [ ] Two versions can be compared from the share page ### Watch out - Exporting notes as timecode computed with a rounded frame rate produces markers that drift from the comment position, and the drift grows through the file so the last notes are the most wrong. Use the stored rational rate and the drop-frame flag for every conversion in the export. ## M6 · Storage, bandwidth and cleanup (production only) Proxies do not fill the disk and a finished project can be archived. ### Steps 1. Retire proxies, keep comments Proxies are a cache. Regenerate from source if a project reopens. Files: `src/retention.js` 2. Report storage and bandwidth Per project. Client review of a long cut moves real bandwidth. Files: `src/usage.js` 3. Back up the database and archive projects Notes plus a manifest, readable without this application. Files: `scripts/backup.sh`, `src/archive.js` ### Done when - [ ] Retention removes aged proxies after a warning - [ ] A removed proxy leaves comments intact with an explanatory page - [ ] Storage and bandwidth per project are reported - [ ] A database restore reproduces comments with correct frames ### Watch out - Proxies are regenerable from the source and comments are not, so the backup priority is inverted from what the file sizes suggest. Back up the small database religiously and treat the large proxies as a cache.
# Operations · Frame.io ## Backup The comment database is small and irreplaceable; proxies are large and regenerable from sources. Back up the first religiously and treat the second as a cache, which is the opposite of what the file sizes suggest. ## Restore Restore the database, regenerate proxies from sources as projects reopen, and confirm a comment lands on the frame it names. Do a restore drill before the first real user, and write the date here when it passes. ## Monitoring Disk use, outbound bandwidth per project and transcode failures. Bandwidth is the cost that surprises people when a client reviews a long cut several times. ## Incident checklist If comments appear on the wrong frames, the frame rate handling is the first suspect, and the exhaustive round trip test from Phase 1 is where to look. If a proxy is missing, the share page should explain rather than break, which is what makes retention safe. 1. Contain the issue without destroying evidence or user data. 2. Record the timeline and affected scope. 3. Rotate exposed secrets and revoke compromised sessions or credentials. 4. Restore from a verified backup when needed. 5. Document the root cause, the remediation and the regression test. ## Release gate - [ ] Frame to timecode round trip is exact for every frame in a one hour file - [ ] Drop-frame conversion matches expected values at one hour - [ ] An unsigned proxy request is confirmed refused - [ ] Exported markers are confirmed correct near the end of a long file - [ ] A database restore reproduces comments on correct frames ## Launch constraint Do not market omitted Frame.io capabilities as implemented. The non-goals in `PRODUCT.md` remain user-visible limitations until they are deliberately delivered.
# Copy to .env and fill in. Never commit .env; this file documents it. # Required. Folder watched for new exports. Files here are never modified. WATCH_DIR=./incoming # Required. Where review proxies are written. Treat as a cache. PROXY_DIR=./proxies # Required. Keyframe interval in frames. Short for scrubbing, at the cost of file size. PROXY_KEYFRAME_FRAMES=12 # Required. Quality setting for the proxy. Lower is better and larger. PROXY_CRF=23 # Required. How long a signed media URL lasts. Long enough for a review session. SIGNED_MEDIA_TTL_SECONDS=7200 # Required. Age at which an inactive project's proxies are removed, after a warning. PROXY_RETENTION_DAYS=120 # Required. Used in share links. Must be HTTPS. PUBLIC_URL=https://review.example.com
$ choose a build depth, inspect the files, then open the complete pack in your agent
They pay so the client link never buffers and the note never gets lost. A director opens a Frame.io link on a phone in a car and it plays; the editor opens the Premiere panel and the same notes land on the timeline as markers. That round trip, plus a transcode and delivery layer nobody has to think about, is the product. The comment box was never the expensive part.
xCamera to Cloud ingest straight off set
xPremiere, After Effects, and Resolve review panels that sync comments to timeline markers
xa transcode farm and global CDN, so client playback is only as good as your box
xforensic watermarking, SSO, and the security posture studios require
xthe iPad and mobile review apps clients actually use
xstorage, transfer tooling, and uptime bundled into the seat price
Don't feel like building it? These folks already made it free.
KKitsuFrame-by-frame reviews, version comparisons and approvals; the VFX production tracker comes along for the ride.open source↗no votes, no pay-to-list · just what's real
Frame.io pricing
| plan | monthly | annual (per mo) | what you get |
|---|---|---|---|
| free | $0/workspace | $0/workspace | 2 members; 2 GB storage; 2 GB mounted storage; 2 projects |
| pro | $15/user | $13.05/user | Up to 5 members; 2 TB included storage plus 2 TB per added member; 250 GB mounted storage; unlimited projects |
| team | $25/user | $21.75/user | Up to 15 members; 3 TB included storage plus 2 TB per added member; 500 GB mounted storage; unlimited projects |
| enterprise | custom | — | Custom members and storage; multiple workspaces; enterprise security and support |
free tier2 members, 2 GB storage, 2 GB mounted storage and 2 projects
billingmonthly + annual (13% lower); annual billed upfront and auto-renews; applicable tax extra
hidden costsStorage and member caps require a higher plan; some Enterprise/Frame.io Select capabilities are add-ons rather than included.
verified 2026-08-14 · source ↗
Vibecode Frame.io
Kinda. The core of Frame.io is buildable in a weekend with the prompt on this page, but there are real gaps: Camera to Cloud ingest straight off set, Premiere, After Effects, and Resolve review panels that sync comments to timeline markers. Read the honest list above before committing.
How much does Frame.io cost?
Frame.io costs about $25/month (Team, checked 2026-07-30), which is $300 per year.
What do I lose by replacing Frame.io?
Honestly: Camera to Cloud ingest straight off set; Premiere, After Effects, and Resolve review panels that sync comments to timeline markers; a transcode farm and global CDN, so client playback is only as good as your box; forensic watermarking, SSO, and the security posture studios require; the iPad and mobile review apps clients actually use; storage, transfer tooling, and uptime bundled into the seat price. If any of those are load-bearing for you, keep paying.
Is there an open-source alternative to Frame.io?
Yes: Kitsu (Frame-by-frame reviews, version comparisons and approvals; the VFX production tracker comes along for the ride.) Clapshot (Frame-accurate comments, drawings and version stacks without Adobe; you still run the server.) The prompt is for when you want it exactly your way.