Front page.What it is
Building
The ad-free video library
All my long-form video in one place, on storage I own, with no ads.
Running on a temporary address while the domain waits.

Set in type.The stack
What it runs on
A static front on Cloudflare, a small box for accounts, and the files in my own bucket.
Front
- Astro7.3a prerendered catalogue
- Preact10.29the player island
- Cloudflare Workershosting
Box
- Bun + Hono4.13the API
- Better Auth1.7accounts
- Polarmembership
- Postgres17with Drizzle 0.45, on Railway
Media
- Cloudflare R2the files, no egress fees
- Trigger.dev4.5renditions and previews with ffmpeg
- Payloadone row per video
- <video>no third-party player, no DRM
On the press.How it works
5 steps, in order
From one upload to an honest view count.
Upload once
The master goes up through CCC, which writes the video's CMS row.
Transcode in the background
A background job cuts the renditions and a hover preview with ffmpeg, straight into R2.
Rebuild the catalogue
A push rebuilds every catalogue page as static HTML in about 40 seconds.

Play from a signed link
The player asks for a short-lived signed URL. Member videos check the membership first.

Count honestly
A view counts after 3 seconds of real playback, once per viewer per day.
Pulled proof.The code
What counts as a view
// A VIEW, as the watch page reports it: the viewer pressed play and has watched at least// VIEW_SEC seconds of the video. The box counts it once per viewer per 24 hours and keeps the// rules (box/src/viewing.ts); this file only measures.export const VIEW_SEC = 3/** * Seconds actually played on this page, from `player.played` — the stretches of the video the * viewer watched. A seek adds nothing and a stretch replayed counts once, so no timer and no * per-second event is needed to know it: it is read at the moments the page already saves. */export function playedSeconds(ranges: Pick<TimeRanges, 'length' | 'start' | 'end'>): number { let sum = 0 for (let i = 0; i < ranges.length; i++) sum += ranges.end(i) - ranges.start(i) return sum}A seek adds nothing and a replay counts once, so there is no timer and no per-second write.
The reel.The walkthrough
Watch it run
Corrections.What it fixes
The problems it solves
The platform keeps the files and the audience.The files sit in my own bucket, and the list is mine.
Ads on every video, and less reach for linking my own work.No ads. Linking my own work costs nothing.
Bandwidth bills that grow with every view.R2 charges no egress, and the catalogue is static.
View counts nobody can check.A view is 3 seconds, once a day, and the rule is tested.
Editor's letter.Why I built it
Why it exists
Own the data. Every re-hosting provider was out from the start.
I want to promote my own work without a platform punishing me for it. So each video is published here first, and the copies elsewhere point back to the ad-free version.

Against the rack.Better than average
Against the usual tools
Against Vimeo OTT, Uscreen and Patreon video. Their side was read from each vendor's own pages on 2026-09-27.
| What | The market average | This app |
|---|---|---|
| Where files live | On the vendor's servers. | In my own R2 bucket.vendor's page |
| Cost as the audience grows | A fee per paying subscriber, or a cut of each sale. | No fee per subscriber and no cut; R2 charges no egress.vendor's page |
| What counts as a view | Any start of playback (Vimeo). | 3 seconds of real playback, once per viewer per day, tested.vendor's page |
| Privacy | Player analytics cookies on by default (Vimeo). | Signed-out viewers' IP addresses are held in memory only.vendor's page |
The imprint.How long it took
64 commits in 11 days
- commits on the site
- 64
- days with a commit
- 10
- days, first commit to latest
- 11
- First commit
- Latest site commit
- Plus 25 commits on the ingest and transcode side
Measured: git log of the site's repo, 2026-09-27.
Colophon.Where it sits in the ledger
Still building. Nothing shipped is claimed for it.
← Back to the ledger