Local screen recording is easy, but instant hosting, permissions, viewer analytics, comments, trimming, and team libraries push it beyond one sitting.
You are building a lean indie version of Loom. 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 ===== # Loom · indie build A recorder that captures screen and camera together, encodes locally, uploads to storage you own, generates a transcript, and serves a share page with a working link, viewing permissions and an expiry, so the whole loop from recording to a link takes under a minute. 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 | | --- | --- | --- | | Capture | The browser's display and camera capture APIs | no installation for the recorder, which is most of why the original is used | | Encoding | MediaRecorder in the browser, with ffmpeg server side for the final file | record locally, fix the container and generate a poster afterwards | | Storage | S3-compatible object storage behind a delivery network | video is large and viewers are elsewhere | | Pages | A small server for upload, share pages and permissions | the link is the product and it has to keep working | | Transcript | whisper.cpp server side, after upload | searchable and accessible, without sending anything to a third party | ## 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 server, the upload endpoint and the processing pipeline. - Get it: nodejs.org, or nvm install 22. - Verify: node --version prints v22 or higher - [ ] **ffmpeg** · free - Why: Remuxing for seekability and poster generation both go through it. - Get it: brew install ffmpeg, your package manager, or a static build. - Verify: ffmpeg -version prints a banner - [ ] **An HTTPS host** · 5 to 10 USD a month plus a domain - Why: Display capture and camera capture both require a secure context, so plain HTTP cannot record at all. - Get it: A small server with a domain and automatic certificates, or a tunnel for local development. - Verify: The recorder page can request display capture - [ ] **An S3-compatible bucket with a delivery network** · storage is cheap; egress varies by an order of magnitude between providers - Why: Recordings are large and viewers are elsewhere. Egress from a bucket without a network in front is usually the expensive path. - Get it: Create a private bucket and credentials, and put a delivery network in front. Compare egress pricing, since it dominates the cost here. - Verify: An object uploads, is refused directly, and serves through the network with a signature - [ ] **whisper.cpp with a model** (optional) · free, but it uses server time per recording - Why: Phase 4 only. Sharing works without it and the transcript arrives afterwards. - Get it: Build whisper.cpp and download a model sized for your server. - Verify: It transcribes a short file ## Quick start ```sh ffmpeg -i in.webm -c copy -movflags +faststart out.mp4 ffmpeg -i out.mp4 -ss 2 -vframes 1 poster.jpg ``` Then copy `.env.example` to `.env` and fill in the values it documents. ## Honest limits This build deliberately does not replace: - Reliable hosting at scale, which is what the subscription sells. - Workspace permissions and team libraries. - Editing beyond trimming. - A commitment that links keep working for years without your attention. - instant reliable hosting - permissions - comments - analytics - workspace libraries - browser extensions If one of those is essential to you, that is the reason to keep paying for Loom, and the README should say so rather than pretend. ===== BRIEF.md ===== # Build brief · Loom 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 record-and-share tool: capture screen and camera, upload, and hand someone a link that plays to replace Loom. 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. ### Stack (fixed, do not substitute) - Capture: The browser's display and camera capture APIs · no installation for the recorder, which is most of why the original is used - Encoding: MediaRecorder in the browser, with ffmpeg server side for the final file · record locally, fix the container and generate a poster afterwards - Storage: S3-compatible object storage behind a delivery network · video is large and viewers are elsewhere - Pages: A small server for upload, share pages and permissions · the link is the product and it has to keep working - Transcript: whisper.cpp server side, after upload · searchable and accessible, without sending anything to a third party ### Data model (create this before Phase 1) - recordings(id, owner_id, title, duration_ms, width, height, state, created_at, expires_at) - assets(recording_id, kind, key, bytes) · kind is video, poster, transcript or captions - shares(id, recording_id, token, visibility, password_hash, expires_at, created_at) · visibility is link, password or account - views(recording_id, share_id, at, watched_ms, viewer_hash) - comments(id, recording_id, at_ms, author, body, created_at) · anchored to a timestamp in the video - The share token is separate from the recording identifier, so a link can be revoked without destroying the recording. ### Phase 1 · Recording screen and camera together Build: Request display capture and camera capture, compose them onto a canvas with the camera as an inset, capture the canvas stream with microphone audio, and record with MediaRecorder. Handle the user stopping the share from the browser's own control. Trap: The browser's own stop sharing control ends the display track without telling your application through any of the obvious paths, and a recorder that does not listen for the track ending produces a truncated or corrupt file. Listen for the track's end event and finalise the recording properly. Done when: A recording contains screen, camera inset and microphone audio, and plays back in the browser. Stopping via the browser's own control finalises the recording rather than corrupting it. Denying camera access still records the screen with audio and says what is missing. Do not build yet: No upload, no sharing, no transcript. ### Phase 2 · Upload that survives a bad connection Build: Upload in chunks with resume. Start uploading while recording continues rather than waiting for the end. Show real progress. On completion, run a server-side pass to fix the container and generate a poster frame. Trap: Browser-recorded files often have unseekable metadata, because the duration is not known until recording ends and the container is written progressively. A viewer cannot scrub such a file. Run a server-side remux to move the metadata to the front, which is fast and makes the file seekable. Done when: A ten minute recording uploads with visible progress. Interrupting the network mid-upload and reconnecting resumes rather than restarting. The processed file is seekable, verified by scrubbing to the middle immediately after load. A poster frame is generated and shown before playback. Do not build yet: No sharing or permissions yet. ### Phase 3 · The link, and who may open it Build: A share page with an unguessable token. Visibility settings of anyone with the link, password protected, or signed-in accounts only. An expiry date. Revoking a share without deleting the recording. Trap: An unguessable link is obscurity, not access control, and for anything sensitive the video object itself must require a signature. Otherwise the share page's permissions are decoration and the object URL circulates instead. Done when: A share link plays for someone with no account. A password protected share refuses without the password, and the video object is refused without a signature, tested by requesting it directly. Revoking a share breaks the link and leaves the recording intact. An expired share refuses rather than showing an empty page. Do not build yet: No transcript or comments yet. ### Phase 4 · Transcript and captions Build: Transcribe server side after upload. Serve captions as a track and the transcript as text alongside the player, with clicking a line seeking the video. Mark automatic transcripts as automatic. Trap: An automatic transcript presented as captions without review produces confidently wrong text for the viewers who most depend on it. Label it as automatic, make correcting it easy, and treat a reviewed transcript as a different state from a generated one. Done when: A transcript is generated after upload without blocking the share link. Captions display in the player and can be turned off. Clicking a transcript line seeks the video there. Automatic transcripts are visibly labelled and can be edited, and editing marks them reviewed. Do not build yet: No comments or analytics yet. ### Phase 5 · Comments and knowing it was watched Build: Comments anchored to a timestamp, shown on the timeline. View records with watch time, derived from player events. Notify the owner of a new comment. Keep viewer records coarse rather than identifying. Trap: Detailed per-viewer watch tracking on a link you sent to a colleague is more surveillance than most people expect from a video message. Record coarse aggregates and be explicit in the interface about what the sender can see, because the recipient cannot see your privacy policy. Done when: A comment anchored at a timestamp appears on the timeline and seeks there when clicked. The owner is notified of a new comment. Watch time is recorded and shown in aggregate. The share page states what the sender can see about viewing. Do not build yet: Retention and operations come last. ### Phase 6 · Storage cost, retention and backups Build: Retention that deletes recordings past an age unless kept, with a warning first. Storage and egress cost reporting. Backups of the database and, selectively, of recordings. A restore that produces working links. Trap: Deleting a recording breaks every link to it that anyone has ever shared, including in documents and messages you cannot see. Warn before deletion, keep the share page as a clear expired notice rather than a broken link, and make retention opt-in per recording rather than silent. Done when: Retention warns before deleting and a kept recording is exempt. A deleted recording's link shows an expired notice rather than an error. Storage and egress cost per month is reported. A restore reproduces recordings and their share links work, and the date is recorded. Do not build yet: Nothing after this. ### Out of scope (and why) - Instant reliable hosting at any scale, which is what the subscription actually sells and which becomes your operational problem. - The workspace permissions and team libraries that make it usable inside a company. - Editing beyond trimming, since a recording tool that becomes an editor is a different product. - The reliability of a link that has to keep working for years. That is now a commitment you are making personally. ### README must contain - That the browser's own stop control must be handled or recordings truncate - That uploaded files are remuxed so viewers can scrub, and why browser recordings are not seekable - That share tokens are separate from recordings, so revoking does not delete - That video objects require a signature and the share page is not the only gate - What the sender can see about viewing, and that the share page says so ===== AGENTS.md ===== # Agent instructions · Loom indie build - Read `README.md` and `BUILD_PLAN.md` before writing code. The stack is fixed: The browser's display and camera capture APIs, MediaRecorder in the browser, with ffmpeg server side for the final file, S3-compatible object storage behind a delivery network, A small server for upload, share pages and permissions, whisper.cpp server side, after upload. 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 - The browser's own stop sharing control ends the display track without telling your application through any of the obvious paths, and a recorder that does not listen for the track ending produces a truncated or corrupt file. Listen for the track's end event and finalise the recording properly. - Browser-recorded files often have unseekable metadata, because the duration is not known until recording ends and the container is written progressively. A viewer cannot scrub such a file. Run a server-side remux to move the metadata to the front, which is fast and makes the file seekable. - An unguessable link is obscurity, not access control, and for anything sensitive the video object itself must require a signature. Otherwise the share page's permissions are decoration and the object URL circulates instead. - An automatic transcript presented as captions without review produces confidently wrong text for the viewers who most depend on it. Label it as automatic, make correcting it easy, and treat a reviewed transcript as a different state from a generated one. - Detailed per-viewer watch tracking on a link you sent to a colleague is more surveillance than most people expect from a video message. Record coarse aggregates and be explicit in the interface about what the sender can see, because the recipient cannot see your privacy policy. ===== BUILD_PLAN.md ===== # Build plan · Loom A recorder that captures screen and camera together, encodes locally, uploads to storage you own, generates a transcript, and serves a share page with a working link, viewing permissions and an expiry, so the whole loop from recording to a link takes under a minute. Phases are in dependency order. Each ends in a "Done when" list; treat an unticked item as a blocker, not a note. ## Phase 1 · Recording screen and camera together One recording contains the screen and a camera inset, with audio, and it plays. ### Steps 1. Capture both sources Display capture and camera capture as separate streams, composed onto a canvas each frame. Files: `src/capture.ts` 2. Record the composed stream MediaRecorder over the canvas stream plus microphone audio. Choose a container the browser actually supports. Files: `src/record.ts` 3. Handle the browser's stop control Listen for the display track ending and finalise. This is the case that produces corrupt files. Files: `src/record.ts` ### Done when - [ ] A recording contains screen, camera and audio - [ ] The browser's stop control finalises rather than corrupts - [ ] Denying camera still records screen and audio - [ ] The recorded file plays back in the browser ### Watch out - The browser's own stop sharing control ends the display track without telling your application through any of the obvious paths, and a recorder that does not listen for the track ending produces a truncated or corrupt file. Listen for the track's end event and finalise the recording properly. ## Phase 2 · Upload that survives a bad connection A ten minute recording uploads reliably, and an interruption does not mean starting again. ### Steps 1. Upload in resumable chunks Multipart upload with recorded offsets so a reconnection continues. Files: `src/upload.ts` 2. Start uploading during recording Chunks go as they are produced, so the wait after stopping is short. This is most of the perceived speed. Files: `src/upload.ts` 3. Remux and generate a poster Move metadata to the front so the file is seekable, and extract a frame for the poster. ```sh ffmpeg -i in.webm -c copy -movflags +faststart out.mp4 ffmpeg -i out.mp4 -ss 2 -vframes 1 poster.jpg ``` ### Done when - [ ] A ten minute recording uploads with visible progress - [ ] An interrupted upload resumes rather than restarting - [ ] The processed file is seekable immediately - [ ] A poster frame is generated and displayed ### Watch out - Browser-recorded files often have unseekable metadata, because the duration is not known until recording ends and the container is written progressively. A viewer cannot scrub such a file. Run a server-side remux to move the metadata to the front, which is fast and makes the file seekable. ## Phase 3 · The link, and who may open it You get a link the moment the upload finishes, and you control who can use it. ### Steps 1. Generate share tokens separately Separate from the recording identifier, so revocation does not destroy anything. Files: `src/shares.ts` 2. Sign object access The share page issues short-lived signed URLs after checking the visibility rule. Objects stay private. Files: `src/access.ts` 3. Test the bypass paths Request the object directly, use a revoked token, and open an expired share. All three must fail. ### Done when - [ ] A share link plays for someone with no account - [ ] A direct object request without a signature is refused - [ ] Revoking breaks the link and keeps the recording - [ ] An expired share refuses access ### Watch out - An unguessable link is obscurity, not access control, and for anything sensitive the video object itself must require a signature. Otherwise the share page's permissions are decoration and the object URL circulates instead. ## Phase 4 · Transcript and captions Every recording is searchable and watchable without sound. ### Steps 1. Transcribe asynchronously After upload, in the background. The share link must not wait for it. Files: `src/transcribe.ts` 2. Serve captions and an interactive transcript A caption track plus clickable lines that seek. The transcript is also how people skim a recording. Files: `src/player.ts` 3. Label and allow correction Automatic is a state, reviewed is another. The distinction matters for accessibility. Files: `src/transcript.ts` ### Done when - [ ] The share link works before the transcript is ready - [ ] Captions display and can be disabled - [ ] Clicking a transcript line seeks the video - [ ] Automatic transcripts are labelled and editable ### Watch out - An automatic transcript presented as captions without review produces confidently wrong text for the viewers who most depend on it. Label it as automatic, make correcting it easy, and treat a reviewed transcript as a different state from a generated one. ## Phase 5 · Comments and knowing it was watched Someone can reply at a moment in the video, and you can tell whether it was watched. ### Steps 1. Anchor comments to time Stored with a millisecond offset, rendered on the timeline, seeking on click. Files: `src/comments.ts` 2. Record viewing coarsely Watch time and a count. Not a per-viewer timeline of exactly what they rewatched. Files: `src/views.ts` 3. Disclose it on the share page One line telling the viewer what the sender can see. They cannot read your policy and should not have to. Files: `templates/share.html` ### Done when - [ ] A timestamped comment appears on the timeline and seeks on click - [ ] The owner is notified of a new comment - [ ] Watch time is recorded and shown in aggregate - [ ] The share page discloses what the sender can see ### Watch out - Detailed per-viewer watch tracking on a link you sent to a colleague is more surveillance than most people expect from a video message. Record coarse aggregates and be explicit in the interface about what the sender can see, because the recipient cannot see your privacy policy. ## Not in this build - Reliable hosting at scale, which is what the subscription sells. - Workspace permissions and team libraries. - Editing beyond trimming. - A commitment that links keep working for years without your attention. ## After v1, if you want it - Trimming the start and end, which is the only edit most recordings need - A recording queue so several can upload while you make the next one - Reactions on the timeline, which are cheaper than comments and used more ===== .env.example ===== # Copy to .env and fill in. Never commit .env; this file documents it. # Required. Used in share links. Must be HTTPS or capture will not work at all. PUBLIC_URL=https://record.example.com # Required. Private bucket holding video, posters and transcripts. BUCKET_NAME=recordings # Required · secret. Scoped to this bucket only. BUCKET_SECRET_KEY=replace-with-secret # Required. What viewers fetch from. The bucket hostname never appears in a share page. CDN_BASE_URL=https://cdn.example.com # Required. How long a signed video URL lasts. Long enough for the recording, short enough to matter. SIGNED_URL_TTL_SECONDS=7200 # Required. How long recordings are kept unless marked to keep. A warning is sent first. DEFAULT_RETENTION_DAYS=365 # Required. Upload chunk size. Smaller resumes more finely and costs more requests. CHUNK_SIZE_MB=5
You are building a lean indie version of Loom. 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 ===== # Loom · indie build A recorder that captures screen and camera together, encodes locally, uploads to storage you own, generates a transcript, and serves a share page with a working link, viewing permissions and an expiry, so the whole loop from recording to a link takes under a minute. 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 | | --- | --- | --- | | Capture | The browser's display and camera capture APIs | no installation for the recorder, which is most of why the original is used | | Encoding | MediaRecorder in the browser, with ffmpeg server side for the final file | record locally, fix the container and generate a poster afterwards | | Storage | S3-compatible object storage behind a delivery network | video is large and viewers are elsewhere | | Pages | A small server for upload, share pages and permissions | the link is the product and it has to keep working | | Transcript | whisper.cpp server side, after upload | searchable and accessible, without sending anything to a third party | ## 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 server, the upload endpoint and the processing pipeline. - Get it: nodejs.org, or nvm install 22. - Verify: node --version prints v22 or higher - [ ] **ffmpeg** · free - Why: Remuxing for seekability and poster generation both go through it. - Get it: brew install ffmpeg, your package manager, or a static build. - Verify: ffmpeg -version prints a banner - [ ] **An HTTPS host** · 5 to 10 USD a month plus a domain - Why: Display capture and camera capture both require a secure context, so plain HTTP cannot record at all. - Get it: A small server with a domain and automatic certificates, or a tunnel for local development. - Verify: The recorder page can request display capture - [ ] **An S3-compatible bucket with a delivery network** · storage is cheap; egress varies by an order of magnitude between providers - Why: Recordings are large and viewers are elsewhere. Egress from a bucket without a network in front is usually the expensive path. - Get it: Create a private bucket and credentials, and put a delivery network in front. Compare egress pricing, since it dominates the cost here. - Verify: An object uploads, is refused directly, and serves through the network with a signature - [ ] **whisper.cpp with a model** (optional) · free, but it uses server time per recording - Why: Phase 4 only. Sharing works without it and the transcript arrives afterwards. - Get it: Build whisper.cpp and download a model sized for your server. - Verify: It transcribes a short file ## Quick start ```sh ffmpeg -i in.webm -c copy -movflags +faststart out.mp4 ffmpeg -i out.mp4 -ss 2 -vframes 1 poster.jpg ``` Then copy `.env.example` to `.env` and fill in the values it documents. ## Honest limits This build deliberately does not replace: - Reliable hosting at scale, which is what the subscription sells. - Workspace permissions and team libraries. - Editing beyond trimming. - A commitment that links keep working for years without your attention. - instant reliable hosting - permissions - comments - analytics - workspace libraries - browser extensions If one of those is essential to you, that is the reason to keep paying for Loom, and the README should say so rather than pretend. ===== BRIEF.md ===== # Build brief · Loom 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 record-and-share tool: capture screen and camera, upload, and hand someone a link that plays to replace Loom. 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. ### Stack (fixed, do not substitute) - Capture: The browser's display and camera capture APIs · no installation for the recorder, which is most of why the original is used - Encoding: MediaRecorder in the browser, with ffmpeg server side for the final file · record locally, fix the container and generate a poster afterwards - Storage: S3-compatible object storage behind a delivery network · video is large and viewers are elsewhere - Pages: A small server for upload, share pages and permissions · the link is the product and it has to keep working - Transcript: whisper.cpp server side, after upload · searchable and accessible, without sending anything to a third party ### Data model (create this before Phase 1) - recordings(id, owner_id, title, duration_ms, width, height, state, created_at, expires_at) - assets(recording_id, kind, key, bytes) · kind is video, poster, transcript or captions - shares(id, recording_id, token, visibility, password_hash, expires_at, created_at) · visibility is link, password or account - views(recording_id, share_id, at, watched_ms, viewer_hash) - comments(id, recording_id, at_ms, author, body, created_at) · anchored to a timestamp in the video - The share token is separate from the recording identifier, so a link can be revoked without destroying the recording. ### Phase 1 · Recording screen and camera together Build: Request display capture and camera capture, compose them onto a canvas with the camera as an inset, capture the canvas stream with microphone audio, and record with MediaRecorder. Handle the user stopping the share from the browser's own control. Trap: The browser's own stop sharing control ends the display track without telling your application through any of the obvious paths, and a recorder that does not listen for the track ending produces a truncated or corrupt file. Listen for the track's end event and finalise the recording properly. Done when: A recording contains screen, camera inset and microphone audio, and plays back in the browser. Stopping via the browser's own control finalises the recording rather than corrupting it. Denying camera access still records the screen with audio and says what is missing. Do not build yet: No upload, no sharing, no transcript. ### Phase 2 · Upload that survives a bad connection Build: Upload in chunks with resume. Start uploading while recording continues rather than waiting for the end. Show real progress. On completion, run a server-side pass to fix the container and generate a poster frame. Trap: Browser-recorded files often have unseekable metadata, because the duration is not known until recording ends and the container is written progressively. A viewer cannot scrub such a file. Run a server-side remux to move the metadata to the front, which is fast and makes the file seekable. Done when: A ten minute recording uploads with visible progress. Interrupting the network mid-upload and reconnecting resumes rather than restarting. The processed file is seekable, verified by scrubbing to the middle immediately after load. A poster frame is generated and shown before playback. Do not build yet: No sharing or permissions yet. ### Phase 3 · The link, and who may open it Build: A share page with an unguessable token. Visibility settings of anyone with the link, password protected, or signed-in accounts only. An expiry date. Revoking a share without deleting the recording. Trap: An unguessable link is obscurity, not access control, and for anything sensitive the video object itself must require a signature. Otherwise the share page's permissions are decoration and the object URL circulates instead. Done when: A share link plays for someone with no account. A password protected share refuses without the password, and the video object is refused without a signature, tested by requesting it directly. Revoking a share breaks the link and leaves the recording intact. An expired share refuses rather than showing an empty page. Do not build yet: No transcript or comments yet. ### Phase 4 · Transcript and captions Build: Transcribe server side after upload. Serve captions as a track and the transcript as text alongside the player, with clicking a line seeking the video. Mark automatic transcripts as automatic. Trap: An automatic transcript presented as captions without review produces confidently wrong text for the viewers who most depend on it. Label it as automatic, make correcting it easy, and treat a reviewed transcript as a different state from a generated one. Done when: A transcript is generated after upload without blocking the share link. Captions display in the player and can be turned off. Clicking a transcript line seeks the video there. Automatic transcripts are visibly labelled and can be edited, and editing marks them reviewed. Do not build yet: No comments or analytics yet. ### Phase 5 · Comments and knowing it was watched Build: Comments anchored to a timestamp, shown on the timeline. View records with watch time, derived from player events. Notify the owner of a new comment. Keep viewer records coarse rather than identifying. Trap: Detailed per-viewer watch tracking on a link you sent to a colleague is more surveillance than most people expect from a video message. Record coarse aggregates and be explicit in the interface about what the sender can see, because the recipient cannot see your privacy policy. Done when: A comment anchored at a timestamp appears on the timeline and seeks there when clicked. The owner is notified of a new comment. Watch time is recorded and shown in aggregate. The share page states what the sender can see about viewing. Do not build yet: Retention and operations come last. ### Phase 6 · Storage cost, retention and backups Build: Retention that deletes recordings past an age unless kept, with a warning first. Storage and egress cost reporting. Backups of the database and, selectively, of recordings. A restore that produces working links. Trap: Deleting a recording breaks every link to it that anyone has ever shared, including in documents and messages you cannot see. Warn before deletion, keep the share page as a clear expired notice rather than a broken link, and make retention opt-in per recording rather than silent. Done when: Retention warns before deleting and a kept recording is exempt. A deleted recording's link shows an expired notice rather than an error. Storage and egress cost per month is reported. A restore reproduces recordings and their share links work, and the date is recorded. Do not build yet: Nothing after this. ### Out of scope (and why) - Instant reliable hosting at any scale, which is what the subscription actually sells and which becomes your operational problem. - The workspace permissions and team libraries that make it usable inside a company. - Editing beyond trimming, since a recording tool that becomes an editor is a different product. - The reliability of a link that has to keep working for years. That is now a commitment you are making personally. ### README must contain - That the browser's own stop control must be handled or recordings truncate - That uploaded files are remuxed so viewers can scrub, and why browser recordings are not seekable - That share tokens are separate from recordings, so revoking does not delete - That video objects require a signature and the share page is not the only gate - What the sender can see about viewing, and that the share page says so ===== AGENTS.md ===== # Agent instructions · Loom indie build - Read `README.md` and `BUILD_PLAN.md` before writing code. The stack is fixed: The browser's display and camera capture APIs, MediaRecorder in the browser, with ffmpeg server side for the final file, S3-compatible object storage behind a delivery network, A small server for upload, share pages and permissions, whisper.cpp server side, after upload. 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 - The browser's own stop sharing control ends the display track without telling your application through any of the obvious paths, and a recorder that does not listen for the track ending produces a truncated or corrupt file. Listen for the track's end event and finalise the recording properly. - Browser-recorded files often have unseekable metadata, because the duration is not known until recording ends and the container is written progressively. A viewer cannot scrub such a file. Run a server-side remux to move the metadata to the front, which is fast and makes the file seekable. - An unguessable link is obscurity, not access control, and for anything sensitive the video object itself must require a signature. Otherwise the share page's permissions are decoration and the object URL circulates instead. - An automatic transcript presented as captions without review produces confidently wrong text for the viewers who most depend on it. Label it as automatic, make correcting it easy, and treat a reviewed transcript as a different state from a generated one. - Detailed per-viewer watch tracking on a link you sent to a colleague is more surveillance than most people expect from a video message. Record coarse aggregates and be explicit in the interface about what the sender can see, because the recipient cannot see your privacy policy. ===== BUILD_PLAN.md ===== # Build plan · Loom A recorder that captures screen and camera together, encodes locally, uploads to storage you own, generates a transcript, and serves a share page with a working link, viewing permissions and an expiry, so the whole loop from recording to a link takes under a minute. Phases are in dependency order. Each ends in a "Done when" list; treat an unticked item as a blocker, not a note. ## Phase 1 · Recording screen and camera together One recording contains the screen and a camera inset, with audio, and it plays. ### Steps 1. Capture both sources Display capture and camera capture as separate streams, composed onto a canvas each frame. Files: `src/capture.ts` 2. Record the composed stream MediaRecorder over the canvas stream plus microphone audio. Choose a container the browser actually supports. Files: `src/record.ts` 3. Handle the browser's stop control Listen for the display track ending and finalise. This is the case that produces corrupt files. Files: `src/record.ts` ### Done when - [ ] A recording contains screen, camera and audio - [ ] The browser's stop control finalises rather than corrupts - [ ] Denying camera still records screen and audio - [ ] The recorded file plays back in the browser ### Watch out - The browser's own stop sharing control ends the display track without telling your application through any of the obvious paths, and a recorder that does not listen for the track ending produces a truncated or corrupt file. Listen for the track's end event and finalise the recording properly. ## Phase 2 · Upload that survives a bad connection A ten minute recording uploads reliably, and an interruption does not mean starting again. ### Steps 1. Upload in resumable chunks Multipart upload with recorded offsets so a reconnection continues. Files: `src/upload.ts` 2. Start uploading during recording Chunks go as they are produced, so the wait after stopping is short. This is most of the perceived speed. Files: `src/upload.ts` 3. Remux and generate a poster Move metadata to the front so the file is seekable, and extract a frame for the poster. ```sh ffmpeg -i in.webm -c copy -movflags +faststart out.mp4 ffmpeg -i out.mp4 -ss 2 -vframes 1 poster.jpg ``` ### Done when - [ ] A ten minute recording uploads with visible progress - [ ] An interrupted upload resumes rather than restarting - [ ] The processed file is seekable immediately - [ ] A poster frame is generated and displayed ### Watch out - Browser-recorded files often have unseekable metadata, because the duration is not known until recording ends and the container is written progressively. A viewer cannot scrub such a file. Run a server-side remux to move the metadata to the front, which is fast and makes the file seekable. ## Phase 3 · The link, and who may open it You get a link the moment the upload finishes, and you control who can use it. ### Steps 1. Generate share tokens separately Separate from the recording identifier, so revocation does not destroy anything. Files: `src/shares.ts` 2. Sign object access The share page issues short-lived signed URLs after checking the visibility rule. Objects stay private. Files: `src/access.ts` 3. Test the bypass paths Request the object directly, use a revoked token, and open an expired share. All three must fail. ### Done when - [ ] A share link plays for someone with no account - [ ] A direct object request without a signature is refused - [ ] Revoking breaks the link and keeps the recording - [ ] An expired share refuses access ### Watch out - An unguessable link is obscurity, not access control, and for anything sensitive the video object itself must require a signature. Otherwise the share page's permissions are decoration and the object URL circulates instead. ## Phase 4 · Transcript and captions Every recording is searchable and watchable without sound. ### Steps 1. Transcribe asynchronously After upload, in the background. The share link must not wait for it. Files: `src/transcribe.ts` 2. Serve captions and an interactive transcript A caption track plus clickable lines that seek. The transcript is also how people skim a recording. Files: `src/player.ts` 3. Label and allow correction Automatic is a state, reviewed is another. The distinction matters for accessibility. Files: `src/transcript.ts` ### Done when - [ ] The share link works before the transcript is ready - [ ] Captions display and can be disabled - [ ] Clicking a transcript line seeks the video - [ ] Automatic transcripts are labelled and editable ### Watch out - An automatic transcript presented as captions without review produces confidently wrong text for the viewers who most depend on it. Label it as automatic, make correcting it easy, and treat a reviewed transcript as a different state from a generated one. ## Phase 5 · Comments and knowing it was watched Someone can reply at a moment in the video, and you can tell whether it was watched. ### Steps 1. Anchor comments to time Stored with a millisecond offset, rendered on the timeline, seeking on click. Files: `src/comments.ts` 2. Record viewing coarsely Watch time and a count. Not a per-viewer timeline of exactly what they rewatched. Files: `src/views.ts` 3. Disclose it on the share page One line telling the viewer what the sender can see. They cannot read your policy and should not have to. Files: `templates/share.html` ### Done when - [ ] A timestamped comment appears on the timeline and seeks on click - [ ] The owner is notified of a new comment - [ ] Watch time is recorded and shown in aggregate - [ ] The share page discloses what the sender can see ### Watch out - Detailed per-viewer watch tracking on a link you sent to a colleague is more surveillance than most people expect from a video message. Record coarse aggregates and be explicit in the interface about what the sender can see, because the recipient cannot see your privacy policy. ## Not in this build - Reliable hosting at scale, which is what the subscription sells. - Workspace permissions and team libraries. - Editing beyond trimming. - A commitment that links keep working for years without your attention. ## After v1, if you want it - Trimming the start and end, which is the only edit most recordings need - A recording queue so several can upload while you make the next one - Reactions on the timeline, which are cheaper than comments and used more ===== .env.example ===== # Copy to .env and fill in. Never commit .env; this file documents it. # Required. Used in share links. Must be HTTPS or capture will not work at all. PUBLIC_URL=https://record.example.com # Required. Private bucket holding video, posters and transcripts. BUCKET_NAME=recordings # Required · secret. Scoped to this bucket only. BUCKET_SECRET_KEY=replace-with-secret # Required. What viewers fetch from. The bucket hostname never appears in a share page. CDN_BASE_URL=https://cdn.example.com # Required. How long a signed video URL lasts. Long enough for the recording, short enough to matter. SIGNED_URL_TTL_SECONDS=7200 # Required. How long recordings are kept unless marked to keep. A warning is sent first. DEFAULT_RETENTION_DAYS=365 # Required. Upload chunk size. Smaller resumes more finely and costs more requests. CHUNK_SIZE_MB=5
You are building a production product version of Loom. 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 ===== # Loom · product brief ## Problem Local screen recording is easy, but instant hosting, permissions, viewer analytics, comments, trimming, and team libraries push it beyond one sitting. ## Product outcome A recorder that produces a working link in under a minute, whose files are seekable, whose permissions are enforced on the objects rather than the page, and whose links do not silently break when a recording ages out. ## Target user A builder who needs a maintainable product foundation, not a one-off demo. ## Required capabilities - screen capture API - ffmpeg - object storage - hosted app - auth - optional transcription API ## Explicit non-goals for v1 - Reliable hosting at scale, which is what the subscription sells. - Workspace permissions and team libraries. - Editing beyond trimming. - A commitment that links keep working for years without your attention. - instant reliable hosting - permissions - comments - analytics - workspace libraries - browser extensions ## Success criteria - The browser stop control is confirmed to finalise recordings - An interrupted upload is confirmed to resume - A processed file is confirmed seekable immediately - A direct object request without a signature is confirmed refused - A restore is confirmed to produce working share links ===== BRIEF.md ===== # Build brief · Loom 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 record-and-share tool: capture screen and camera, upload, and hand someone a link that plays to replace Loom. 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. ### Stack (fixed, do not substitute) - Capture: The browser's display and camera capture APIs · no installation for the recorder, which is most of why the original is used - Encoding: MediaRecorder in the browser, with ffmpeg server side for the final file · record locally, fix the container and generate a poster afterwards - Storage: S3-compatible object storage behind a delivery network · video is large and viewers are elsewhere - Pages: A small server for upload, share pages and permissions · the link is the product and it has to keep working - Transcript: whisper.cpp server side, after upload · searchable and accessible, without sending anything to a third party ### Data model (create this before Phase 1) - recordings(id, owner_id, title, duration_ms, width, height, state, created_at, expires_at) - assets(recording_id, kind, key, bytes) · kind is video, poster, transcript or captions - shares(id, recording_id, token, visibility, password_hash, expires_at, created_at) · visibility is link, password or account - views(recording_id, share_id, at, watched_ms, viewer_hash) - comments(id, recording_id, at_ms, author, body, created_at) · anchored to a timestamp in the video - The share token is separate from the recording identifier, so a link can be revoked without destroying the recording. ### Phase 1 · Recording screen and camera together Build: Request display capture and camera capture, compose them onto a canvas with the camera as an inset, capture the canvas stream with microphone audio, and record with MediaRecorder. Handle the user stopping the share from the browser's own control. Trap: The browser's own stop sharing control ends the display track without telling your application through any of the obvious paths, and a recorder that does not listen for the track ending produces a truncated or corrupt file. Listen for the track's end event and finalise the recording properly. Done when: A recording contains screen, camera inset and microphone audio, and plays back in the browser. Stopping via the browser's own control finalises the recording rather than corrupting it. Denying camera access still records the screen with audio and says what is missing. Do not build yet: No upload, no sharing, no transcript. ### Phase 2 · Upload that survives a bad connection Build: Upload in chunks with resume. Start uploading while recording continues rather than waiting for the end. Show real progress. On completion, run a server-side pass to fix the container and generate a poster frame. Trap: Browser-recorded files often have unseekable metadata, because the duration is not known until recording ends and the container is written progressively. A viewer cannot scrub such a file. Run a server-side remux to move the metadata to the front, which is fast and makes the file seekable. Done when: A ten minute recording uploads with visible progress. Interrupting the network mid-upload and reconnecting resumes rather than restarting. The processed file is seekable, verified by scrubbing to the middle immediately after load. A poster frame is generated and shown before playback. Do not build yet: No sharing or permissions yet. ### Phase 3 · The link, and who may open it Build: A share page with an unguessable token. Visibility settings of anyone with the link, password protected, or signed-in accounts only. An expiry date. Revoking a share without deleting the recording. Trap: An unguessable link is obscurity, not access control, and for anything sensitive the video object itself must require a signature. Otherwise the share page's permissions are decoration and the object URL circulates instead. Done when: A share link plays for someone with no account. A password protected share refuses without the password, and the video object is refused without a signature, tested by requesting it directly. Revoking a share breaks the link and leaves the recording intact. An expired share refuses rather than showing an empty page. Do not build yet: No transcript or comments yet. ### Phase 4 · Transcript and captions Build: Transcribe server side after upload. Serve captions as a track and the transcript as text alongside the player, with clicking a line seeking the video. Mark automatic transcripts as automatic. Trap: An automatic transcript presented as captions without review produces confidently wrong text for the viewers who most depend on it. Label it as automatic, make correcting it easy, and treat a reviewed transcript as a different state from a generated one. Done when: A transcript is generated after upload without blocking the share link. Captions display in the player and can be turned off. Clicking a transcript line seeks the video there. Automatic transcripts are visibly labelled and can be edited, and editing marks them reviewed. Do not build yet: No comments or analytics yet. ### Phase 5 · Comments and knowing it was watched Build: Comments anchored to a timestamp, shown on the timeline. View records with watch time, derived from player events. Notify the owner of a new comment. Keep viewer records coarse rather than identifying. Trap: Detailed per-viewer watch tracking on a link you sent to a colleague is more surveillance than most people expect from a video message. Record coarse aggregates and be explicit in the interface about what the sender can see, because the recipient cannot see your privacy policy. Done when: A comment anchored at a timestamp appears on the timeline and seeks there when clicked. The owner is notified of a new comment. Watch time is recorded and shown in aggregate. The share page states what the sender can see about viewing. Do not build yet: Retention and operations come last. ### Phase 6 · Storage cost, retention and backups Build: Retention that deletes recordings past an age unless kept, with a warning first. Storage and egress cost reporting. Backups of the database and, selectively, of recordings. A restore that produces working links. Trap: Deleting a recording breaks every link to it that anyone has ever shared, including in documents and messages you cannot see. Warn before deletion, keep the share page as a clear expired notice rather than a broken link, and make retention opt-in per recording rather than silent. Done when: Retention warns before deleting and a kept recording is exempt. A deleted recording's link shows an expired notice rather than an error. Storage and egress cost per month is reported. A restore reproduces recordings and their share links work, and the date is recorded. Do not build yet: Nothing after this. ### Out of scope (and why) - Instant reliable hosting at any scale, which is what the subscription actually sells and which becomes your operational problem. - The workspace permissions and team libraries that make it usable inside a company. - Editing beyond trimming, since a recording tool that becomes an editor is a different product. - The reliability of a link that has to keep working for years. That is now a commitment you are making personally. ### README must contain - That the browser's own stop control must be handled or recordings truncate - That uploaded files are remuxed so viewers can scrub, and why browser recordings are not seekable - That share tokens are separate from recordings, so revoking does not delete - That video objects require a signature and the share page is not the only gate - What the sender can see about viewing, and that the share page says so ===== ARCHITECTURE.md ===== # Architecture · Loom ## Stack | Part | Choice | Why | | --- | --- | --- | | Capture | The browser's display and camera capture APIs | no installation for the recorder, which is most of why the original is used | | Encoding | MediaRecorder in the browser, with ffmpeg server side for the final file | record locally, fix the container and generate a poster afterwards | | Storage | S3-compatible object storage behind a delivery network | video is large and viewers are elsewhere | | Pages | A small server for upload, share pages and permissions | the link is the product and it has to keep working | | Transcript | whisper.cpp server side, after upload | searchable and accessible, without sending anything to a third party | ## Modules Each module has one owner concern and a documented way to replace it. | Module | Owns | How to replace it | | --- | --- | --- | | Capture | Screen, camera and audio composition. | Browser APIs are what avoid an installation, which is most of the appeal. A native recorder would be higher quality and a different product. | | Upload | Chunked resumable transfer. | Any multipart implementation. Uploading during recording is the decision that makes it feel fast. | | Process | Remux, poster and transcript. | ffmpeg for the first two. The remux for seekability is not optional; browser output is not seekable without it. | | Shares | Tokens, visibility and expiry. | The separation of token from recording is what makes revocation safe and must survive any change. | | Delivery | Serving video cheaply and quickly. | Providers are interchangeable; keeping objects private is not. | ## Configuration Every runtime setting is an environment variable documented in `.env.example`, validated at startup, with a safe local default wherever one exists. - `PUBLIC_URL` · required · Used in share links. Must be HTTPS or capture will not work at all. - `BUCKET_NAME` · required · Private bucket holding video, posters and transcripts. - `BUCKET_SECRET_KEY` · required, secret · Scoped to this bucket only. - `CDN_BASE_URL` · required · What viewers fetch from. The bucket hostname never appears in a share page. - `SIGNED_URL_TTL_SECONDS` · required · How long a signed video URL lasts. Long enough for the recording, short enough to matter. - `DEFAULT_RETENTION_DAYS` · required · How long recordings are kept unless marked to keep. A warning is sent first. - `CHUNK_SIZE_MB` · required · Upload chunk size. Smaller resumes more finely and costs more requests. ## 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 · Loom product build - Read `PRODUCT.md` and `ARCHITECTURE.md` before changing code. The stack is fixed: The browser's display and camera capture APIs, MediaRecorder in the browser, with ffmpeg server side for the final file, S3-compatible object storage behind a delivery network, A small server for upload, share pages and permissions, whisper.cpp server side, after upload. - 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 - The browser's own stop sharing control ends the display track without telling your application through any of the obvious paths, and a recorder that does not listen for the track ending produces a truncated or corrupt file. Listen for the track's end event and finalise the recording properly. - Browser-recorded files often have unseekable metadata, because the duration is not known until recording ends and the container is written progressively. A viewer cannot scrub such a file. Run a server-side remux to move the metadata to the front, which is fast and makes the file seekable. - An unguessable link is obscurity, not access control, and for anything sensitive the video object itself must require a signature. Otherwise the share page's permissions are decoration and the object URL circulates instead. - An automatic transcript presented as captions without review produces confidently wrong text for the viewers who most depend on it. Label it as automatic, make correcting it easy, and treat a reviewed transcript as a different state from a generated one. - Detailed per-viewer watch tracking on a link you sent to a colleague is more surveillance than most people expect from a video message. Record coarse aggregates and be explicit in the interface about what the sender can see, because the recipient cannot see your privacy policy. - Deleting a recording breaks every link to it that anyone has ever shared, including in documents and messages you cannot see. Warn before deletion, keep the share page as a clear expired notice rather than a broken link, and make retention opt-in per recording rather than silent. ===== MILESTONES.md ===== # Delivery milestones · Loom Estimated effort: **weekend** for the indie phases; the production-only milestones add the trust and operability layer. ## M1 · Recording screen and camera together One recording contains the screen and a camera inset, with audio, and it plays. ### Steps 1. Capture both sources Display capture and camera capture as separate streams, composed onto a canvas each frame. Files: `src/capture.ts` 2. Record the composed stream MediaRecorder over the canvas stream plus microphone audio. Choose a container the browser actually supports. Files: `src/record.ts` 3. Handle the browser's stop control Listen for the display track ending and finalise. This is the case that produces corrupt files. Files: `src/record.ts` ### Done when - [ ] A recording contains screen, camera and audio - [ ] The browser's stop control finalises rather than corrupts - [ ] Denying camera still records screen and audio - [ ] The recorded file plays back in the browser ### Watch out - The browser's own stop sharing control ends the display track without telling your application through any of the obvious paths, and a recorder that does not listen for the track ending produces a truncated or corrupt file. Listen for the track's end event and finalise the recording properly. ## M2 · Upload that survives a bad connection A ten minute recording uploads reliably, and an interruption does not mean starting again. ### Steps 1. Upload in resumable chunks Multipart upload with recorded offsets so a reconnection continues. Files: `src/upload.ts` 2. Start uploading during recording Chunks go as they are produced, so the wait after stopping is short. This is most of the perceived speed. Files: `src/upload.ts` 3. Remux and generate a poster Move metadata to the front so the file is seekable, and extract a frame for the poster. ```sh ffmpeg -i in.webm -c copy -movflags +faststart out.mp4 ffmpeg -i out.mp4 -ss 2 -vframes 1 poster.jpg ``` ### Done when - [ ] A ten minute recording uploads with visible progress - [ ] An interrupted upload resumes rather than restarting - [ ] The processed file is seekable immediately - [ ] A poster frame is generated and displayed ### Watch out - Browser-recorded files often have unseekable metadata, because the duration is not known until recording ends and the container is written progressively. A viewer cannot scrub such a file. Run a server-side remux to move the metadata to the front, which is fast and makes the file seekable. ## M3 · The link, and who may open it You get a link the moment the upload finishes, and you control who can use it. ### Steps 1. Generate share tokens separately Separate from the recording identifier, so revocation does not destroy anything. Files: `src/shares.ts` 2. Sign object access The share page issues short-lived signed URLs after checking the visibility rule. Objects stay private. Files: `src/access.ts` 3. Test the bypass paths Request the object directly, use a revoked token, and open an expired share. All three must fail. ### Done when - [ ] A share link plays for someone with no account - [ ] A direct object request without a signature is refused - [ ] Revoking breaks the link and keeps the recording - [ ] An expired share refuses access ### Watch out - An unguessable link is obscurity, not access control, and for anything sensitive the video object itself must require a signature. Otherwise the share page's permissions are decoration and the object URL circulates instead. ## M4 · Transcript and captions Every recording is searchable and watchable without sound. ### Steps 1. Transcribe asynchronously After upload, in the background. The share link must not wait for it. Files: `src/transcribe.ts` 2. Serve captions and an interactive transcript A caption track plus clickable lines that seek. The transcript is also how people skim a recording. Files: `src/player.ts` 3. Label and allow correction Automatic is a state, reviewed is another. The distinction matters for accessibility. Files: `src/transcript.ts` ### Done when - [ ] The share link works before the transcript is ready - [ ] Captions display and can be disabled - [ ] Clicking a transcript line seeks the video - [ ] Automatic transcripts are labelled and editable ### Watch out - An automatic transcript presented as captions without review produces confidently wrong text for the viewers who most depend on it. Label it as automatic, make correcting it easy, and treat a reviewed transcript as a different state from a generated one. ## M5 · Comments and knowing it was watched Someone can reply at a moment in the video, and you can tell whether it was watched. ### Steps 1. Anchor comments to time Stored with a millisecond offset, rendered on the timeline, seeking on click. Files: `src/comments.ts` 2. Record viewing coarsely Watch time and a count. Not a per-viewer timeline of exactly what they rewatched. Files: `src/views.ts` 3. Disclose it on the share page One line telling the viewer what the sender can see. They cannot read your policy and should not have to. Files: `templates/share.html` ### Done when - [ ] A timestamped comment appears on the timeline and seeks on click - [ ] The owner is notified of a new comment - [ ] Watch time is recorded and shown in aggregate - [ ] The share page discloses what the sender can see ### Watch out - Detailed per-viewer watch tracking on a link you sent to a colleague is more surveillance than most people expect from a video message. Record coarse aggregates and be explicit in the interface about what the sender can see, because the recipient cannot see your privacy policy. ## M6 · Storage cost, retention and backups (production only) Old recordings do not accumulate cost forever, and links keep working. ### Steps 1. Warn before retention deletes A notice to the owner before anything goes, with a keep action. Files: `src/retention.ts` 2. Show an expired page, not an error A deleted recording's link explains rather than breaking. People will follow those links for years. Files: `templates/expired.html` 3. Back up and restore Database always; recordings selectively, since they are large. Restore and confirm links work. Files: `scripts/backup.sh`, `README.md` ### Done when - [ ] Retention warns before deleting and honours a keep - [ ] A deleted recording's link shows an expired notice - [ ] Storage and egress cost is reported monthly - [ ] A restore produces working share links and is dated ### Watch out - Deleting a recording breaks every link to it that anyone has ever shared, including in documents and messages you cannot see. Warn before deletion, keep the share page as a clear expired notice rather than a broken link, and make retention opt-in per recording rather than silent. ===== OPERATIONS.md ===== # Operations · Loom ## Backup The database is small and essential, holding shares and permissions. Recordings are large; back up selectively and be explicit about which are protected, because a link that outlives its file is worse than one that was never made. ## Restore Restore the database and the recordings you kept, then follow a share link to confirm it plays. A restore that produces broken links has not finished. Do a restore drill before the first real user, and write the date here when it passes. ## Monitoring Storage growth, egress against a budget, upload failure rate and transcription queue depth. Egress is the cost that surprises people, and a recording that circulates widely is the usual cause. ## Incident checklist For failed uploads, check the chunk endpoint before anything else; a partial upload leaves billed incomplete parts on some providers. For a link that stopped working, check whether retention removed the recording, which is why the warning and the expired page exist. 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 - [ ] The browser stop control is confirmed to finalise recordings - [ ] An interrupted upload is confirmed to resume - [ ] A processed file is confirmed seekable immediately - [ ] A direct object request without a signature is confirmed refused - [ ] A restore is confirmed to produce working share links ## Launch constraint Do not market omitted Loom 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. Used in share links. Must be HTTPS or capture will not work at all. PUBLIC_URL=https://record.example.com # Required. Private bucket holding video, posters and transcripts. BUCKET_NAME=recordings # Required · secret. Scoped to this bucket only. BUCKET_SECRET_KEY=replace-with-secret # Required. What viewers fetch from. The bucket hostname never appears in a share page. CDN_BASE_URL=https://cdn.example.com # Required. How long a signed video URL lasts. Long enough for the recording, short enough to matter. SIGNED_URL_TTL_SECONDS=7200 # Required. How long recordings are kept unless marked to keep. A warning is sent first. DEFAULT_RETENTION_DAYS=365 # Required. Upload chunk size. Smaller resumes more finely and costs more requests. CHUNK_SIZE_MB=5
# Loom · indie build A recorder that captures screen and camera together, encodes locally, uploads to storage you own, generates a transcript, and serves a share page with a working link, viewing permissions and an expiry, so the whole loop from recording to a link takes under a minute. 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 | | --- | --- | --- | | Capture | The browser's display and camera capture APIs | no installation for the recorder, which is most of why the original is used | | Encoding | MediaRecorder in the browser, with ffmpeg server side for the final file | record locally, fix the container and generate a poster afterwards | | Storage | S3-compatible object storage behind a delivery network | video is large and viewers are elsewhere | | Pages | A small server for upload, share pages and permissions | the link is the product and it has to keep working | | Transcript | whisper.cpp server side, after upload | searchable and accessible, without sending anything to a third party | ## 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 server, the upload endpoint and the processing pipeline. - Get it: nodejs.org, or nvm install 22. - Verify: node --version prints v22 or higher - [ ] **ffmpeg** · free - Why: Remuxing for seekability and poster generation both go through it. - Get it: brew install ffmpeg, your package manager, or a static build. - Verify: ffmpeg -version prints a banner - [ ] **An HTTPS host** · 5 to 10 USD a month plus a domain - Why: Display capture and camera capture both require a secure context, so plain HTTP cannot record at all. - Get it: A small server with a domain and automatic certificates, or a tunnel for local development. - Verify: The recorder page can request display capture - [ ] **An S3-compatible bucket with a delivery network** · storage is cheap; egress varies by an order of magnitude between providers - Why: Recordings are large and viewers are elsewhere. Egress from a bucket without a network in front is usually the expensive path. - Get it: Create a private bucket and credentials, and put a delivery network in front. Compare egress pricing, since it dominates the cost here. - Verify: An object uploads, is refused directly, and serves through the network with a signature - [ ] **whisper.cpp with a model** (optional) · free, but it uses server time per recording - Why: Phase 4 only. Sharing works without it and the transcript arrives afterwards. - Get it: Build whisper.cpp and download a model sized for your server. - Verify: It transcribes a short file ## Quick start ```sh ffmpeg -i in.webm -c copy -movflags +faststart out.mp4 ffmpeg -i out.mp4 -ss 2 -vframes 1 poster.jpg ``` Then copy `.env.example` to `.env` and fill in the values it documents. ## Honest limits This build deliberately does not replace: - Reliable hosting at scale, which is what the subscription sells. - Workspace permissions and team libraries. - Editing beyond trimming. - A commitment that links keep working for years without your attention. - instant reliable hosting - permissions - comments - analytics - workspace libraries - browser extensions If one of those is essential to you, that is the reason to keep paying for Loom, and the README should say so rather than pretend.
# Build brief · Loom 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 record-and-share tool: capture screen and camera, upload, and hand someone a link that plays to replace Loom. 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. ### Stack (fixed, do not substitute) - Capture: The browser's display and camera capture APIs · no installation for the recorder, which is most of why the original is used - Encoding: MediaRecorder in the browser, with ffmpeg server side for the final file · record locally, fix the container and generate a poster afterwards - Storage: S3-compatible object storage behind a delivery network · video is large and viewers are elsewhere - Pages: A small server for upload, share pages and permissions · the link is the product and it has to keep working - Transcript: whisper.cpp server side, after upload · searchable and accessible, without sending anything to a third party ### Data model (create this before Phase 1) - recordings(id, owner_id, title, duration_ms, width, height, state, created_at, expires_at) - assets(recording_id, kind, key, bytes) · kind is video, poster, transcript or captions - shares(id, recording_id, token, visibility, password_hash, expires_at, created_at) · visibility is link, password or account - views(recording_id, share_id, at, watched_ms, viewer_hash) - comments(id, recording_id, at_ms, author, body, created_at) · anchored to a timestamp in the video - The share token is separate from the recording identifier, so a link can be revoked without destroying the recording. ### Phase 1 · Recording screen and camera together Build: Request display capture and camera capture, compose them onto a canvas with the camera as an inset, capture the canvas stream with microphone audio, and record with MediaRecorder. Handle the user stopping the share from the browser's own control. Trap: The browser's own stop sharing control ends the display track without telling your application through any of the obvious paths, and a recorder that does not listen for the track ending produces a truncated or corrupt file. Listen for the track's end event and finalise the recording properly. Done when: A recording contains screen, camera inset and microphone audio, and plays back in the browser. Stopping via the browser's own control finalises the recording rather than corrupting it. Denying camera access still records the screen with audio and says what is missing. Do not build yet: No upload, no sharing, no transcript. ### Phase 2 · Upload that survives a bad connection Build: Upload in chunks with resume. Start uploading while recording continues rather than waiting for the end. Show real progress. On completion, run a server-side pass to fix the container and generate a poster frame. Trap: Browser-recorded files often have unseekable metadata, because the duration is not known until recording ends and the container is written progressively. A viewer cannot scrub such a file. Run a server-side remux to move the metadata to the front, which is fast and makes the file seekable. Done when: A ten minute recording uploads with visible progress. Interrupting the network mid-upload and reconnecting resumes rather than restarting. The processed file is seekable, verified by scrubbing to the middle immediately after load. A poster frame is generated and shown before playback. Do not build yet: No sharing or permissions yet. ### Phase 3 · The link, and who may open it Build: A share page with an unguessable token. Visibility settings of anyone with the link, password protected, or signed-in accounts only. An expiry date. Revoking a share without deleting the recording. Trap: An unguessable link is obscurity, not access control, and for anything sensitive the video object itself must require a signature. Otherwise the share page's permissions are decoration and the object URL circulates instead. Done when: A share link plays for someone with no account. A password protected share refuses without the password, and the video object is refused without a signature, tested by requesting it directly. Revoking a share breaks the link and leaves the recording intact. An expired share refuses rather than showing an empty page. Do not build yet: No transcript or comments yet. ### Phase 4 · Transcript and captions Build: Transcribe server side after upload. Serve captions as a track and the transcript as text alongside the player, with clicking a line seeking the video. Mark automatic transcripts as automatic. Trap: An automatic transcript presented as captions without review produces confidently wrong text for the viewers who most depend on it. Label it as automatic, make correcting it easy, and treat a reviewed transcript as a different state from a generated one. Done when: A transcript is generated after upload without blocking the share link. Captions display in the player and can be turned off. Clicking a transcript line seeks the video there. Automatic transcripts are visibly labelled and can be edited, and editing marks them reviewed. Do not build yet: No comments or analytics yet. ### Phase 5 · Comments and knowing it was watched Build: Comments anchored to a timestamp, shown on the timeline. View records with watch time, derived from player events. Notify the owner of a new comment. Keep viewer records coarse rather than identifying. Trap: Detailed per-viewer watch tracking on a link you sent to a colleague is more surveillance than most people expect from a video message. Record coarse aggregates and be explicit in the interface about what the sender can see, because the recipient cannot see your privacy policy. Done when: A comment anchored at a timestamp appears on the timeline and seeks there when clicked. The owner is notified of a new comment. Watch time is recorded and shown in aggregate. The share page states what the sender can see about viewing. Do not build yet: Retention and operations come last. ### Phase 6 · Storage cost, retention and backups Build: Retention that deletes recordings past an age unless kept, with a warning first. Storage and egress cost reporting. Backups of the database and, selectively, of recordings. A restore that produces working links. Trap: Deleting a recording breaks every link to it that anyone has ever shared, including in documents and messages you cannot see. Warn before deletion, keep the share page as a clear expired notice rather than a broken link, and make retention opt-in per recording rather than silent. Done when: Retention warns before deleting and a kept recording is exempt. A deleted recording's link shows an expired notice rather than an error. Storage and egress cost per month is reported. A restore reproduces recordings and their share links work, and the date is recorded. Do not build yet: Nothing after this. ### Out of scope (and why) - Instant reliable hosting at any scale, which is what the subscription actually sells and which becomes your operational problem. - The workspace permissions and team libraries that make it usable inside a company. - Editing beyond trimming, since a recording tool that becomes an editor is a different product. - The reliability of a link that has to keep working for years. That is now a commitment you are making personally. ### README must contain - That the browser's own stop control must be handled or recordings truncate - That uploaded files are remuxed so viewers can scrub, and why browser recordings are not seekable - That share tokens are separate from recordings, so revoking does not delete - That video objects require a signature and the share page is not the only gate - What the sender can see about viewing, and that the share page says so
# Agent instructions · Loom indie build - Read `README.md` and `BUILD_PLAN.md` before writing code. The stack is fixed: The browser's display and camera capture APIs, MediaRecorder in the browser, with ffmpeg server side for the final file, S3-compatible object storage behind a delivery network, A small server for upload, share pages and permissions, whisper.cpp server side, after upload. 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 - The browser's own stop sharing control ends the display track without telling your application through any of the obvious paths, and a recorder that does not listen for the track ending produces a truncated or corrupt file. Listen for the track's end event and finalise the recording properly. - Browser-recorded files often have unseekable metadata, because the duration is not known until recording ends and the container is written progressively. A viewer cannot scrub such a file. Run a server-side remux to move the metadata to the front, which is fast and makes the file seekable. - An unguessable link is obscurity, not access control, and for anything sensitive the video object itself must require a signature. Otherwise the share page's permissions are decoration and the object URL circulates instead. - An automatic transcript presented as captions without review produces confidently wrong text for the viewers who most depend on it. Label it as automatic, make correcting it easy, and treat a reviewed transcript as a different state from a generated one. - Detailed per-viewer watch tracking on a link you sent to a colleague is more surveillance than most people expect from a video message. Record coarse aggregates and be explicit in the interface about what the sender can see, because the recipient cannot see your privacy policy.
# Build plan · Loom A recorder that captures screen and camera together, encodes locally, uploads to storage you own, generates a transcript, and serves a share page with a working link, viewing permissions and an expiry, so the whole loop from recording to a link takes under a minute. Phases are in dependency order. Each ends in a "Done when" list; treat an unticked item as a blocker, not a note. ## Phase 1 · Recording screen and camera together One recording contains the screen and a camera inset, with audio, and it plays. ### Steps 1. Capture both sources Display capture and camera capture as separate streams, composed onto a canvas each frame. Files: `src/capture.ts` 2. Record the composed stream MediaRecorder over the canvas stream plus microphone audio. Choose a container the browser actually supports. Files: `src/record.ts` 3. Handle the browser's stop control Listen for the display track ending and finalise. This is the case that produces corrupt files. Files: `src/record.ts` ### Done when - [ ] A recording contains screen, camera and audio - [ ] The browser's stop control finalises rather than corrupts - [ ] Denying camera still records screen and audio - [ ] The recorded file plays back in the browser ### Watch out - The browser's own stop sharing control ends the display track without telling your application through any of the obvious paths, and a recorder that does not listen for the track ending produces a truncated or corrupt file. Listen for the track's end event and finalise the recording properly. ## Phase 2 · Upload that survives a bad connection A ten minute recording uploads reliably, and an interruption does not mean starting again. ### Steps 1. Upload in resumable chunks Multipart upload with recorded offsets so a reconnection continues. Files: `src/upload.ts` 2. Start uploading during recording Chunks go as they are produced, so the wait after stopping is short. This is most of the perceived speed. Files: `src/upload.ts` 3. Remux and generate a poster Move metadata to the front so the file is seekable, and extract a frame for the poster. ```sh ffmpeg -i in.webm -c copy -movflags +faststart out.mp4 ffmpeg -i out.mp4 -ss 2 -vframes 1 poster.jpg ``` ### Done when - [ ] A ten minute recording uploads with visible progress - [ ] An interrupted upload resumes rather than restarting - [ ] The processed file is seekable immediately - [ ] A poster frame is generated and displayed ### Watch out - Browser-recorded files often have unseekable metadata, because the duration is not known until recording ends and the container is written progressively. A viewer cannot scrub such a file. Run a server-side remux to move the metadata to the front, which is fast and makes the file seekable. ## Phase 3 · The link, and who may open it You get a link the moment the upload finishes, and you control who can use it. ### Steps 1. Generate share tokens separately Separate from the recording identifier, so revocation does not destroy anything. Files: `src/shares.ts` 2. Sign object access The share page issues short-lived signed URLs after checking the visibility rule. Objects stay private. Files: `src/access.ts` 3. Test the bypass paths Request the object directly, use a revoked token, and open an expired share. All three must fail. ### Done when - [ ] A share link plays for someone with no account - [ ] A direct object request without a signature is refused - [ ] Revoking breaks the link and keeps the recording - [ ] An expired share refuses access ### Watch out - An unguessable link is obscurity, not access control, and for anything sensitive the video object itself must require a signature. Otherwise the share page's permissions are decoration and the object URL circulates instead. ## Phase 4 · Transcript and captions Every recording is searchable and watchable without sound. ### Steps 1. Transcribe asynchronously After upload, in the background. The share link must not wait for it. Files: `src/transcribe.ts` 2. Serve captions and an interactive transcript A caption track plus clickable lines that seek. The transcript is also how people skim a recording. Files: `src/player.ts` 3. Label and allow correction Automatic is a state, reviewed is another. The distinction matters for accessibility. Files: `src/transcript.ts` ### Done when - [ ] The share link works before the transcript is ready - [ ] Captions display and can be disabled - [ ] Clicking a transcript line seeks the video - [ ] Automatic transcripts are labelled and editable ### Watch out - An automatic transcript presented as captions without review produces confidently wrong text for the viewers who most depend on it. Label it as automatic, make correcting it easy, and treat a reviewed transcript as a different state from a generated one. ## Phase 5 · Comments and knowing it was watched Someone can reply at a moment in the video, and you can tell whether it was watched. ### Steps 1. Anchor comments to time Stored with a millisecond offset, rendered on the timeline, seeking on click. Files: `src/comments.ts` 2. Record viewing coarsely Watch time and a count. Not a per-viewer timeline of exactly what they rewatched. Files: `src/views.ts` 3. Disclose it on the share page One line telling the viewer what the sender can see. They cannot read your policy and should not have to. Files: `templates/share.html` ### Done when - [ ] A timestamped comment appears on the timeline and seeks on click - [ ] The owner is notified of a new comment - [ ] Watch time is recorded and shown in aggregate - [ ] The share page discloses what the sender can see ### Watch out - Detailed per-viewer watch tracking on a link you sent to a colleague is more surveillance than most people expect from a video message. Record coarse aggregates and be explicit in the interface about what the sender can see, because the recipient cannot see your privacy policy. ## Not in this build - Reliable hosting at scale, which is what the subscription sells. - Workspace permissions and team libraries. - Editing beyond trimming. - A commitment that links keep working for years without your attention. ## After v1, if you want it - Trimming the start and end, which is the only edit most recordings need - A recording queue so several can upload while you make the next one - Reactions on the timeline, which are cheaper than comments and used more
# Copy to .env and fill in. Never commit .env; this file documents it. # Required. Used in share links. Must be HTTPS or capture will not work at all. PUBLIC_URL=https://record.example.com # Required. Private bucket holding video, posters and transcripts. BUCKET_NAME=recordings # Required · secret. Scoped to this bucket only. BUCKET_SECRET_KEY=replace-with-secret # Required. What viewers fetch from. The bucket hostname never appears in a share page. CDN_BASE_URL=https://cdn.example.com # Required. How long a signed video URL lasts. Long enough for the recording, short enough to matter. SIGNED_URL_TTL_SECONDS=7200 # Required. How long recordings are kept unless marked to keep. A warning is sent first. DEFAULT_RETENTION_DAYS=365 # Required. Upload chunk size. Smaller resumes more finely and costs more requests. CHUNK_SIZE_MB=5
# Loom · product brief ## Problem Local screen recording is easy, but instant hosting, permissions, viewer analytics, comments, trimming, and team libraries push it beyond one sitting. ## Product outcome A recorder that produces a working link in under a minute, whose files are seekable, whose permissions are enforced on the objects rather than the page, and whose links do not silently break when a recording ages out. ## Target user A builder who needs a maintainable product foundation, not a one-off demo. ## Required capabilities - screen capture API - ffmpeg - object storage - hosted app - auth - optional transcription API ## Explicit non-goals for v1 - Reliable hosting at scale, which is what the subscription sells. - Workspace permissions and team libraries. - Editing beyond trimming. - A commitment that links keep working for years without your attention. - instant reliable hosting - permissions - comments - analytics - workspace libraries - browser extensions ## Success criteria - The browser stop control is confirmed to finalise recordings - An interrupted upload is confirmed to resume - A processed file is confirmed seekable immediately - A direct object request without a signature is confirmed refused - A restore is confirmed to produce working share links
# Build brief · Loom 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 record-and-share tool: capture screen and camera, upload, and hand someone a link that plays to replace Loom. 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. ### Stack (fixed, do not substitute) - Capture: The browser's display and camera capture APIs · no installation for the recorder, which is most of why the original is used - Encoding: MediaRecorder in the browser, with ffmpeg server side for the final file · record locally, fix the container and generate a poster afterwards - Storage: S3-compatible object storage behind a delivery network · video is large and viewers are elsewhere - Pages: A small server for upload, share pages and permissions · the link is the product and it has to keep working - Transcript: whisper.cpp server side, after upload · searchable and accessible, without sending anything to a third party ### Data model (create this before Phase 1) - recordings(id, owner_id, title, duration_ms, width, height, state, created_at, expires_at) - assets(recording_id, kind, key, bytes) · kind is video, poster, transcript or captions - shares(id, recording_id, token, visibility, password_hash, expires_at, created_at) · visibility is link, password or account - views(recording_id, share_id, at, watched_ms, viewer_hash) - comments(id, recording_id, at_ms, author, body, created_at) · anchored to a timestamp in the video - The share token is separate from the recording identifier, so a link can be revoked without destroying the recording. ### Phase 1 · Recording screen and camera together Build: Request display capture and camera capture, compose them onto a canvas with the camera as an inset, capture the canvas stream with microphone audio, and record with MediaRecorder. Handle the user stopping the share from the browser's own control. Trap: The browser's own stop sharing control ends the display track without telling your application through any of the obvious paths, and a recorder that does not listen for the track ending produces a truncated or corrupt file. Listen for the track's end event and finalise the recording properly. Done when: A recording contains screen, camera inset and microphone audio, and plays back in the browser. Stopping via the browser's own control finalises the recording rather than corrupting it. Denying camera access still records the screen with audio and says what is missing. Do not build yet: No upload, no sharing, no transcript. ### Phase 2 · Upload that survives a bad connection Build: Upload in chunks with resume. Start uploading while recording continues rather than waiting for the end. Show real progress. On completion, run a server-side pass to fix the container and generate a poster frame. Trap: Browser-recorded files often have unseekable metadata, because the duration is not known until recording ends and the container is written progressively. A viewer cannot scrub such a file. Run a server-side remux to move the metadata to the front, which is fast and makes the file seekable. Done when: A ten minute recording uploads with visible progress. Interrupting the network mid-upload and reconnecting resumes rather than restarting. The processed file is seekable, verified by scrubbing to the middle immediately after load. A poster frame is generated and shown before playback. Do not build yet: No sharing or permissions yet. ### Phase 3 · The link, and who may open it Build: A share page with an unguessable token. Visibility settings of anyone with the link, password protected, or signed-in accounts only. An expiry date. Revoking a share without deleting the recording. Trap: An unguessable link is obscurity, not access control, and for anything sensitive the video object itself must require a signature. Otherwise the share page's permissions are decoration and the object URL circulates instead. Done when: A share link plays for someone with no account. A password protected share refuses without the password, and the video object is refused without a signature, tested by requesting it directly. Revoking a share breaks the link and leaves the recording intact. An expired share refuses rather than showing an empty page. Do not build yet: No transcript or comments yet. ### Phase 4 · Transcript and captions Build: Transcribe server side after upload. Serve captions as a track and the transcript as text alongside the player, with clicking a line seeking the video. Mark automatic transcripts as automatic. Trap: An automatic transcript presented as captions without review produces confidently wrong text for the viewers who most depend on it. Label it as automatic, make correcting it easy, and treat a reviewed transcript as a different state from a generated one. Done when: A transcript is generated after upload without blocking the share link. Captions display in the player and can be turned off. Clicking a transcript line seeks the video there. Automatic transcripts are visibly labelled and can be edited, and editing marks them reviewed. Do not build yet: No comments or analytics yet. ### Phase 5 · Comments and knowing it was watched Build: Comments anchored to a timestamp, shown on the timeline. View records with watch time, derived from player events. Notify the owner of a new comment. Keep viewer records coarse rather than identifying. Trap: Detailed per-viewer watch tracking on a link you sent to a colleague is more surveillance than most people expect from a video message. Record coarse aggregates and be explicit in the interface about what the sender can see, because the recipient cannot see your privacy policy. Done when: A comment anchored at a timestamp appears on the timeline and seeks there when clicked. The owner is notified of a new comment. Watch time is recorded and shown in aggregate. The share page states what the sender can see about viewing. Do not build yet: Retention and operations come last. ### Phase 6 · Storage cost, retention and backups Build: Retention that deletes recordings past an age unless kept, with a warning first. Storage and egress cost reporting. Backups of the database and, selectively, of recordings. A restore that produces working links. Trap: Deleting a recording breaks every link to it that anyone has ever shared, including in documents and messages you cannot see. Warn before deletion, keep the share page as a clear expired notice rather than a broken link, and make retention opt-in per recording rather than silent. Done when: Retention warns before deleting and a kept recording is exempt. A deleted recording's link shows an expired notice rather than an error. Storage and egress cost per month is reported. A restore reproduces recordings and their share links work, and the date is recorded. Do not build yet: Nothing after this. ### Out of scope (and why) - Instant reliable hosting at any scale, which is what the subscription actually sells and which becomes your operational problem. - The workspace permissions and team libraries that make it usable inside a company. - Editing beyond trimming, since a recording tool that becomes an editor is a different product. - The reliability of a link that has to keep working for years. That is now a commitment you are making personally. ### README must contain - That the browser's own stop control must be handled or recordings truncate - That uploaded files are remuxed so viewers can scrub, and why browser recordings are not seekable - That share tokens are separate from recordings, so revoking does not delete - That video objects require a signature and the share page is not the only gate - What the sender can see about viewing, and that the share page says so
# Architecture · Loom ## Stack | Part | Choice | Why | | --- | --- | --- | | Capture | The browser's display and camera capture APIs | no installation for the recorder, which is most of why the original is used | | Encoding | MediaRecorder in the browser, with ffmpeg server side for the final file | record locally, fix the container and generate a poster afterwards | | Storage | S3-compatible object storage behind a delivery network | video is large and viewers are elsewhere | | Pages | A small server for upload, share pages and permissions | the link is the product and it has to keep working | | Transcript | whisper.cpp server side, after upload | searchable and accessible, without sending anything to a third party | ## Modules Each module has one owner concern and a documented way to replace it. | Module | Owns | How to replace it | | --- | --- | --- | | Capture | Screen, camera and audio composition. | Browser APIs are what avoid an installation, which is most of the appeal. A native recorder would be higher quality and a different product. | | Upload | Chunked resumable transfer. | Any multipart implementation. Uploading during recording is the decision that makes it feel fast. | | Process | Remux, poster and transcript. | ffmpeg for the first two. The remux for seekability is not optional; browser output is not seekable without it. | | Shares | Tokens, visibility and expiry. | The separation of token from recording is what makes revocation safe and must survive any change. | | Delivery | Serving video cheaply and quickly. | Providers are interchangeable; keeping objects private is not. | ## Configuration Every runtime setting is an environment variable documented in `.env.example`, validated at startup, with a safe local default wherever one exists. - `PUBLIC_URL` · required · Used in share links. Must be HTTPS or capture will not work at all. - `BUCKET_NAME` · required · Private bucket holding video, posters and transcripts. - `BUCKET_SECRET_KEY` · required, secret · Scoped to this bucket only. - `CDN_BASE_URL` · required · What viewers fetch from. The bucket hostname never appears in a share page. - `SIGNED_URL_TTL_SECONDS` · required · How long a signed video URL lasts. Long enough for the recording, short enough to matter. - `DEFAULT_RETENTION_DAYS` · required · How long recordings are kept unless marked to keep. A warning is sent first. - `CHUNK_SIZE_MB` · required · Upload chunk size. Smaller resumes more finely and costs more requests. ## 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 · Loom product build - Read `PRODUCT.md` and `ARCHITECTURE.md` before changing code. The stack is fixed: The browser's display and camera capture APIs, MediaRecorder in the browser, with ffmpeg server side for the final file, S3-compatible object storage behind a delivery network, A small server for upload, share pages and permissions, whisper.cpp server side, after upload. - 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 - The browser's own stop sharing control ends the display track without telling your application through any of the obvious paths, and a recorder that does not listen for the track ending produces a truncated or corrupt file. Listen for the track's end event and finalise the recording properly. - Browser-recorded files often have unseekable metadata, because the duration is not known until recording ends and the container is written progressively. A viewer cannot scrub such a file. Run a server-side remux to move the metadata to the front, which is fast and makes the file seekable. - An unguessable link is obscurity, not access control, and for anything sensitive the video object itself must require a signature. Otherwise the share page's permissions are decoration and the object URL circulates instead. - An automatic transcript presented as captions without review produces confidently wrong text for the viewers who most depend on it. Label it as automatic, make correcting it easy, and treat a reviewed transcript as a different state from a generated one. - Detailed per-viewer watch tracking on a link you sent to a colleague is more surveillance than most people expect from a video message. Record coarse aggregates and be explicit in the interface about what the sender can see, because the recipient cannot see your privacy policy. - Deleting a recording breaks every link to it that anyone has ever shared, including in documents and messages you cannot see. Warn before deletion, keep the share page as a clear expired notice rather than a broken link, and make retention opt-in per recording rather than silent.
# Delivery milestones · Loom Estimated effort: **weekend** for the indie phases; the production-only milestones add the trust and operability layer. ## M1 · Recording screen and camera together One recording contains the screen and a camera inset, with audio, and it plays. ### Steps 1. Capture both sources Display capture and camera capture as separate streams, composed onto a canvas each frame. Files: `src/capture.ts` 2. Record the composed stream MediaRecorder over the canvas stream plus microphone audio. Choose a container the browser actually supports. Files: `src/record.ts` 3. Handle the browser's stop control Listen for the display track ending and finalise. This is the case that produces corrupt files. Files: `src/record.ts` ### Done when - [ ] A recording contains screen, camera and audio - [ ] The browser's stop control finalises rather than corrupts - [ ] Denying camera still records screen and audio - [ ] The recorded file plays back in the browser ### Watch out - The browser's own stop sharing control ends the display track without telling your application through any of the obvious paths, and a recorder that does not listen for the track ending produces a truncated or corrupt file. Listen for the track's end event and finalise the recording properly. ## M2 · Upload that survives a bad connection A ten minute recording uploads reliably, and an interruption does not mean starting again. ### Steps 1. Upload in resumable chunks Multipart upload with recorded offsets so a reconnection continues. Files: `src/upload.ts` 2. Start uploading during recording Chunks go as they are produced, so the wait after stopping is short. This is most of the perceived speed. Files: `src/upload.ts` 3. Remux and generate a poster Move metadata to the front so the file is seekable, and extract a frame for the poster. ```sh ffmpeg -i in.webm -c copy -movflags +faststart out.mp4 ffmpeg -i out.mp4 -ss 2 -vframes 1 poster.jpg ``` ### Done when - [ ] A ten minute recording uploads with visible progress - [ ] An interrupted upload resumes rather than restarting - [ ] The processed file is seekable immediately - [ ] A poster frame is generated and displayed ### Watch out - Browser-recorded files often have unseekable metadata, because the duration is not known until recording ends and the container is written progressively. A viewer cannot scrub such a file. Run a server-side remux to move the metadata to the front, which is fast and makes the file seekable. ## M3 · The link, and who may open it You get a link the moment the upload finishes, and you control who can use it. ### Steps 1. Generate share tokens separately Separate from the recording identifier, so revocation does not destroy anything. Files: `src/shares.ts` 2. Sign object access The share page issues short-lived signed URLs after checking the visibility rule. Objects stay private. Files: `src/access.ts` 3. Test the bypass paths Request the object directly, use a revoked token, and open an expired share. All three must fail. ### Done when - [ ] A share link plays for someone with no account - [ ] A direct object request without a signature is refused - [ ] Revoking breaks the link and keeps the recording - [ ] An expired share refuses access ### Watch out - An unguessable link is obscurity, not access control, and for anything sensitive the video object itself must require a signature. Otherwise the share page's permissions are decoration and the object URL circulates instead. ## M4 · Transcript and captions Every recording is searchable and watchable without sound. ### Steps 1. Transcribe asynchronously After upload, in the background. The share link must not wait for it. Files: `src/transcribe.ts` 2. Serve captions and an interactive transcript A caption track plus clickable lines that seek. The transcript is also how people skim a recording. Files: `src/player.ts` 3. Label and allow correction Automatic is a state, reviewed is another. The distinction matters for accessibility. Files: `src/transcript.ts` ### Done when - [ ] The share link works before the transcript is ready - [ ] Captions display and can be disabled - [ ] Clicking a transcript line seeks the video - [ ] Automatic transcripts are labelled and editable ### Watch out - An automatic transcript presented as captions without review produces confidently wrong text for the viewers who most depend on it. Label it as automatic, make correcting it easy, and treat a reviewed transcript as a different state from a generated one. ## M5 · Comments and knowing it was watched Someone can reply at a moment in the video, and you can tell whether it was watched. ### Steps 1. Anchor comments to time Stored with a millisecond offset, rendered on the timeline, seeking on click. Files: `src/comments.ts` 2. Record viewing coarsely Watch time and a count. Not a per-viewer timeline of exactly what they rewatched. Files: `src/views.ts` 3. Disclose it on the share page One line telling the viewer what the sender can see. They cannot read your policy and should not have to. Files: `templates/share.html` ### Done when - [ ] A timestamped comment appears on the timeline and seeks on click - [ ] The owner is notified of a new comment - [ ] Watch time is recorded and shown in aggregate - [ ] The share page discloses what the sender can see ### Watch out - Detailed per-viewer watch tracking on a link you sent to a colleague is more surveillance than most people expect from a video message. Record coarse aggregates and be explicit in the interface about what the sender can see, because the recipient cannot see your privacy policy. ## M6 · Storage cost, retention and backups (production only) Old recordings do not accumulate cost forever, and links keep working. ### Steps 1. Warn before retention deletes A notice to the owner before anything goes, with a keep action. Files: `src/retention.ts` 2. Show an expired page, not an error A deleted recording's link explains rather than breaking. People will follow those links for years. Files: `templates/expired.html` 3. Back up and restore Database always; recordings selectively, since they are large. Restore and confirm links work. Files: `scripts/backup.sh`, `README.md` ### Done when - [ ] Retention warns before deleting and honours a keep - [ ] A deleted recording's link shows an expired notice - [ ] Storage and egress cost is reported monthly - [ ] A restore produces working share links and is dated ### Watch out - Deleting a recording breaks every link to it that anyone has ever shared, including in documents and messages you cannot see. Warn before deletion, keep the share page as a clear expired notice rather than a broken link, and make retention opt-in per recording rather than silent.
# Operations · Loom ## Backup The database is small and essential, holding shares and permissions. Recordings are large; back up selectively and be explicit about which are protected, because a link that outlives its file is worse than one that was never made. ## Restore Restore the database and the recordings you kept, then follow a share link to confirm it plays. A restore that produces broken links has not finished. Do a restore drill before the first real user, and write the date here when it passes. ## Monitoring Storage growth, egress against a budget, upload failure rate and transcription queue depth. Egress is the cost that surprises people, and a recording that circulates widely is the usual cause. ## Incident checklist For failed uploads, check the chunk endpoint before anything else; a partial upload leaves billed incomplete parts on some providers. For a link that stopped working, check whether retention removed the recording, which is why the warning and the expired page exist. 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 - [ ] The browser stop control is confirmed to finalise recordings - [ ] An interrupted upload is confirmed to resume - [ ] A processed file is confirmed seekable immediately - [ ] A direct object request without a signature is confirmed refused - [ ] A restore is confirmed to produce working share links ## Launch constraint Do not market omitted Loom 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. Used in share links. Must be HTTPS or capture will not work at all. PUBLIC_URL=https://record.example.com # Required. Private bucket holding video, posters and transcripts. BUCKET_NAME=recordings # Required · secret. Scoped to this bucket only. BUCKET_SECRET_KEY=replace-with-secret # Required. What viewers fetch from. The bucket hostname never appears in a share page. CDN_BASE_URL=https://cdn.example.com # Required. How long a signed video URL lasts. Long enough for the recording, short enough to matter. SIGNED_URL_TTL_SECONDS=7200 # Required. How long recordings are kept unless marked to keep. A warning is sent first. DEFAULT_RETENTION_DAYS=365 # Required. Upload chunk size. Smaller resumes more finely and costs more requests. CHUNK_SIZE_MB=5
$ choose a build depth, inspect the files, then open the complete pack in your agent
They pay because a Loom link works everywhere and preserves context for the team.
xinstant reliable hosting
xpermissions
xcomments
xanalytics
xworkspace libraries
xbrowser extensions
Don't feel like building it? These folks already made it free.
no votes, no pay-to-list · just what's real
Loom pricing
| plan | monthly | annual (per mo) | what you get |
|---|---|---|---|
| starter | $0/user | $0/user | Up to 50 workspace members; 25 videos/person; 5 minutes/video; unlimited-length meeting recordings; 720p. |
| business | $18/user | $15/user | Unlimited videos and recording length; paid Creator/Admin seat. |
| business + ai | $24/user | $20/user | Unlimited videos and length plus AI workflows; 14-day trial. |
| enterprise | custom | — | Custom users and limits; price not public. |
free tier50 workspace members, 25 videos per person, 5 minutes per video, unlimited-length meeting recordings, unlimited screenshots and 720p recording.
billingmonthly + annual (about 17% off); 14-day Business + AI trial
hidden costsCreator and Admin roles are paid; Creator Lite is free. Seat counting and billing mechanics differ between direct Loom subscriptions and Atlassian subscriptions.
verified 2026-08-11 · source ↗
Is Loom free?
The free plan allows 25 videos of up to 5 minutes each. Paid is Business at $18/mo (checked 2026-08-07).
Vibecode Loom
Kinda. The core of Loom is buildable in a weekend with the prompt on this page, but there are real gaps: instant reliable hosting, permissions. Read the honest list above before committing.
How much does Loom cost?
Loom costs about $18/month (Business, checked 2026-08-07), which is $216 per year.
What do I lose by replacing Loom?
Honestly: instant reliable hosting; permissions; comments; analytics; workspace libraries; browser extensions. If any of those are load-bearing for you, keep paying.
Is there an open-source alternative to Loom?
Yes: Cap (Loom with your own storage and your own server chores.) The prompt is for when you want it exactly your way.