How to build a Loom alternative
How to build a Loom alternative with a tab recording, a private upload, and a watch link your teammate can play. Follow the prompts and skip the studio.
How to build a Loom alternative is a tab recording, a private upload, and a watch link. Loom is that product. The catalog calls it “Record screen, share link, receive “nice!” comment.”
You pay $15–$24 a person a month. That is $180–$288 a year per person, and the price scales with the team. The verdict is “Great internal tool”. The first estimate is 10 hours: tab recording, upload, share link.
The Loom teardown covers the encoding ladder and the signed media URL. This guide is the 10-hour screens, rules, and prompts. The comment in that catalog line waits until the watch page already plays.
The audience is a remote team sharing short screen recordings internally. If the company already lives in a desktop recorder with transcripts, keep paying. The same v1 also replaces Tella, Screen Studio, and Vidyard when the job is “watch this click path”. A file with an expiry is the file transfer guide, at $20–$32 a month for Dropbox Transfer. A hosted player is Vimeo, $20–$65 a month. Notes pinned to a frame are Frame.io, $15–$25 a person a month.
How to build a Loom alternative as one private clip
The Loom page splits the work into three rows.
The first row is the one you want. Record a browser tab. Upload the file. Hand a teammate a watch link. That is the 10-hour v1. The catalog names the project Showtell.
The second row is 1 week, 40 hours. Comments, folders, library search, and retakes. The third row is 1 month and up, 160 hours. Desktop capture, AI transcription, and video editing. The non-goal already names those, plus public discovery.
If you tell an agent to “clone Loom”, it aims at the third row. Paste the prompts below, and keep each one on a tab, one file, and a private player.
Flavio records “Pricing page walkthrough”. The take runs about three minutes. Jordan is the other member. Jordan opens the link, signs in, and watches. A signed-out visitor gets a login wall.
The parts and where the file goes
Every product prompt ships in two flavors: Node.js plus Astro, and TanStack Start. The capability line for Loom is Astro, the browser capture API, and R2. This site is an Astro app on Cloudflare, so that is the path I default to. Use TanStack Start if React is your weekday language. The stack post is which tab you copy. The prompt post is how you pin the first estimate. The scope post is why transcription waits.
The browser does the recording. A click calls the display picker. The user chooses a tab. MediaRecorder writes a WebM blob and a clock measures the take. That work never hits the server.
A save is a form post that creates the recording row, then one upload of that blob into a private bucket. The watch page is a normal request. It checks the session, then mints a short-lived signed URL for the player. D1 holds the row. R2 holds the bytes. There is no cron and no encoder.
TanStack Start keeps the same screens, with server functions and a SQL database with migrations. The file still goes to private object storage. The hosting post is the R2 binding when you leave localhost.
The data one recording carries
The card lists Recording, MediaFile, Comment, and Folder. This v1 needs the recording, the file, and a share slug. Comment and Folder wait for the 40-hour row.
Showtell has one workspace. Flavio and Jordan are members. Seed passwords come from the environment, and the database stores hashes. The session cookie is httpOnly.
A recording carries a stable id, a title, an owner, a status, a duration in milliseconds, and the mime type the browser reported. Status moves from uploading to ready. A failed upload stays on the same id with status failed, so a retry does not mint a second clip.
The file row points at one object key and stores the byte size. The key does not change on retry. The slug is unguessable and unique. The watch address uses the slug. Deleting the recording hides it from the library and refuses the next signed URL. The object can be removed in the same request. An audit line can wait.
Here is the shape after the pricing walkthrough is ready. The teardown uses the same example.
const recording = {
id: 'rec-pricing-walkthrough',
title: 'Pricing page walkthrough',
status: 'ready',
durationMs: 184000,
}
The object key is recordings/rec-pricing-walkthrough/source.webm. The slug on the share row is k7m2n9p4q1. The player never sees the key.
The screens you ship
Three surfaces. Keep them boxes and labels. If the agent starts matching a desktop recorder, send it back to these boxes.
The library is /. It lists title, duration, owner, and status as words. Empty state says to record a tab. There is no search box and no folder picker. A failed row offers retry on the same id.
The recorder is /record. It explains that the next click will ask for a tab and, if you leave the box checked, a microphone. The picker opens only from that button. While you record, the page shows the tab name, a running timer, Stop, and Discard. Stop opens the review. Discard drops the blob and writes nothing.
The review sits on the same recording after Stop. You see a local preview, a title field, the byte size, and an upload bar. The title starts as “Pricing page walkthrough”. Upload posts the blob to the existing id. When status is ready, the page shows the slug and a copy control. A dropped connection leaves status failed and a Retry button.
The watch page is /w/k7m2n9p4q1. It shows the title, the owner, the duration, and a player. The player source is a signed URL that lasts a few minutes. Copy link copies the page address, not the signed URL. Delete asks for a confirm, then the next visit from Jordan is the login wall or a missing-clip page that does not describe the title to a stranger. There is no comment box.
The path from a click to a watch link
One take moves through states you can point at: no row, uploading, ready, or deleted. A failed upload stays on the uploading id.
- Flavio opens
/recordwhile signed in. The page explains the picker. Nothing asks for a tab until he presses the button. - He grants the pricing tab and leaves the microphone on. The timer runs. Stopping the browser share ends the take and opens review. Discard would drop the blob.
- He keeps the title “Pricing page walkthrough” and uploads. The library shows one row, status uploading, then ready. The slug is
k7m2n9p4q1. - He copies
https://show.flaviocopes.com/w/k7m2n9p4q1, or the localhost equivalent, and sends it to Jordan. - Jordan signs in and plays the clip. View source on that page has no bucket host and no object key.
- Flavio deletes the recording. Jordan refreshes. The player does not get a new signed URL. The library no longer lists the title.
Prompt 1. Seed the team and the library
Build Showtell, a private
screen recorder for one
team.
Use Node.js and Astro.
Server-rendered pages.
Ordinary HTML forms.
Cloudflare D1 for the rows.
Private R2 for the video.
If you use TanStack Start
instead, use server functions
and a SQL database with
migrations. Same screens.
Same rules. The file still
goes to private object
storage.
Seed one workspace. Flavio
is the owner. Jordan is a
member. Both sign in with
a session cookie. Passwords
live in env, not in the repo.
Every page and every
mutation checks membership
on the server. A signed-out
visitor gets a login wall.
A guessed watch URL with
no session returns nothing
about the recording.
The library at / lists
title, duration, owner,
and status as words.
Empty state tells you to
record a tab. No search.
No folders. No billing.
Done when both members
can sign in, and a private
window cannot read the
library.
What to check after login exists
Sign in as Flavio. Sign in as Jordan in another browser. Both see an empty library and a way to record.
Open / with no cookie. You get the login wall. After the first clip exists, repeat the check and confirm the HTML has no title.
Prompt 2. Record a tab in the browser
The teardown is why the picker has to start from a click, and why a clock beats a chunk count. MDN says the dataavailable interval is not exact.
Add the recorder at /record.
Explain, before the button,
that the click will ask for
a tab and an optional
microphone. Call the display
picker only from that click.
If the user cancels, write
no row.
While recording, show the
chosen tab, a running clock,
Stop, and Discard. If the
user stops sharing from the
browser, end the take and
open review. Discard drops
the blob and writes nothing.
If the microphone is denied
and the user confirms a
silent take, record video
without asking for audio
again on that attempt.
Store duration from the
clock in milliseconds.
Store the mime type the
browser reports. Do not
count recorder chunks to
get a duration.
No desktop app. No pause
and resume library. No
transcript.
Done when a cancelled
picker leaves the library
empty, and a three-minute
take shows about three
minutes on the review
screen.
What to check after a local take
Load /record. The picker must not appear by itself. Press the button, then cancel. The library stays empty.
Record the pricing tab for a short run with the mic allowed. Stop. The review plays locally and the duration is close to the clock you watched.
Deny the mic, confirm silent, and record again. You get a clip with no second audio prompt.
Prompt 3. Upload one private file
After Stop, the review
shows a local preview, a
title, the byte size, and
an upload action.
Create the recording row
when upload starts. The
id stays stable. Status
moves from uploading to
ready. A failed upload
stays on that id with
status failed.
Put the bytes in one
private object. The key
is recordings/{id}/source
plus the extension for
the mime type. A retry
overwrites that key. A
retry must not insert a
second recording.
Show progress from bytes
sent against the blob
size. When status is
ready, show the unguessable
slug and a control that
copies the watch page
URL.
Never return a public
bucket URL. Never put
the object key in the
HTML.
Done when one failed
upload plus a retry
leaves a single ready
row, and the library
shows that title once.
What to check after the file lands
Upload the pricing walkthrough. The library shows one ready row. Turn the network off halfway, restore it, and retry. You still have one row and one object.
View source on the review page. You should not find an r2.dev host or the object key.
Prompt 4. Play through a signed URL
Build the watch page at
/w/:slug.
A signed-in member sees
the title, the owner, the
duration, and a player.
The player source is a
signed URL for that one
object, valid for a few
minutes. Copy link copies
the page address.
Mint the signed URL only
after the session check
and only when status is
ready. Refreshing after
the URL expires gets a
new one. A signed-out
request gets the login
wall, and that HTML has
no title and no media URL.
Delete confirms, hides
the row, and refuses the
next signed URL. A viewer
who already has a URL
loses access on refresh
once that URL expires.
No comments. No reactions.
No transcript panel.
Done when Jordan can play
the clip, a private window
learns nothing from the
slug, and delete blocks
the next grant.
What to check after Jordan presses play
Open the slug as Jordan. The player starts. Copy link matches /w/k7m2n9p4q1. The network request for the media is a signed URL, and the page address stays the slug.
Wait out the TTL and paste the signed URL in a new tab. It fails. Refresh the watch page. Playback works again.
Delete as Flavio. Refresh as Jordan. The player does not load. Open the slug signed out. The login wall does not include “Pricing page walkthrough”.
Failures the agent ships anyway
A picker on page load
You notice the permission dialog as soon as /record renders. The browser also blocks capture that does not come from a gesture, so the button then does nothing. Move the call onto the click.
A public object URL
src="https://pub-….r2.dev/…/source.webm" is a permanent copy. The hosting post says to put the file through the Worker. Sign on the request. Keep the bucket private.
A duration taken from chunks
A three-minute take shows a few seconds, or a neat multiple of the timeslice. The teardown already says the interval is not exact. Read the clock you started on the click.
A retry that clones the clip
The library lists “Pricing page walkthrough” twice, and R2 has two keys. The upload should overwrite recordings/rec-pricing-walkthrough/source.webm and leave the slug alone.
An encoder on the 10-hour row
A queue, ffmpeg, or a “processing video” state that never reaches ready means the agent left Showtell and started the third estimate. Serve the uploaded file with the mime type you stored. Transcription and a desktop capture app sit on that same skip row.
Delete that only hides a card
The library is empty and Jordan’s refresh still plays. The watch action has to refuse a new signed URL when the row is gone. Keep the TTL short so an old URL dies quickly. The DocSend guide uses the same idea for a private file.
Deploy the recorder
Showtell wants Astro, D1, and private R2. Put it on a subdomain you already own, show.flaviocopes.com. Nothing in this v1 runs on a schedule.
The hosting post is that path: the adapter, then the bindings, then the custom domain.
Attach the domain on the Worker. Record a ten-second tab from your phone’s browser if the picker is available there, or from the laptop. Open the slug as Jordan on another device. Delete on the laptop. Refresh the other device. Confirm the player does not get a new URL.
Take a D1 backup and an R2 listing before you delete a paid Loom workspace. The clips are the files you will miss.
Use it for a week of internal walkthroughs. If you miss a comment at a timestamp, a folder, or search, those are the 40-hour row. Then cancel Loom, or keep paying if guests outside the workspace must watch without an account.
What to skip on purpose
Leave out desktop capture, AI transcription, video editing, and public discovery.
Comments, folders, library search, and retakes wait until the team is already sending links. The second mini-prompt on the card lives there. A timestamp on a comment is useless if the player is still wrong.
Anyone-with-the-link sharing is a later privacy mode. This v1 is members only, which matches an internal team. Guests, passwords, and a public gallery are how a small recorder becomes a hosting company.
More writeups land on the guides index. The audit is the catalog if the next ship should be smaller.
Start now
Go to Build your own Loom. Copy the Astro prompt, or TanStack Start if that is what you write.
Tell the agent the first estimate only: tab recording, upload, share link. Ten hours.
Send Jordan the pricing walkthrough before you add a comment box.