Selling access, not files
- Published
- System
- English with Gift
- Reading time
- 5 min
A tutor's course videos are the business. If they leak there's no second product to fall back on.
So the job isn't "put videos on a website". It's: this student, this video, this window of time, nobody else.
Access is a row, not a file path
The naive version drops videos in a bucket and hands out URLs. Whoever has the URL has the video forever and can pass it on.
Instead access is a row — user, video, expiry — and playback resolves it every single time, server-side:
- Student opens
/videos/[id] - Server checks for a permission row for this user + video
- Only if it exists does the page render with a player
- Player asks
/api/videos/[id]/signed-url - That endpoint re-validates the session and the permission, then mints a 15-minute signed URL to object storage
- Player streams from that URL
Step 5 is the one worth arguing about, because step 2 already checked permission. Looks redundant.
It isn't. Step 2 authorises rendering a page. Step 5 authorises handing out a credential to the actual file. Different acts, different lifetimes, and the second one is the one that matters. If permission gets revoked between page load and pressing play, that endpoint must not still produce a working URL. So whatever mints credentials always re-checks. It never trusts that something upstream did.
Why fifteen minutes
The signed URL is the only thing between a logged-in student and a permanent copy. Its expiry is the security property, so the number is a real trade-off.
Too short and someone on hotel wifi loses playback mid-lesson. Too long and the URL becomes a shareable asset — paste it in a group chat and everyone has the file until it dies.
Fifteen minutes covers a lesson segment plus a reconnect, and makes a shared link close to worthless by the time it's travelled anywhere. Longer videos re-sign transparently; the player asks for a fresh URL instead of holding one open.
Storage is never public. There's no path that serves the file without a signature, which means a leaked URL expires — as opposed to a leaked bucket path, which is forever.
Permissions expire too, and bundles
Time-boxing isn't only at the URL level. A student who bought a three-month course has permission rows that stop working after three months, with nobody running a cleanup job. The check is a comparison, not a state you maintain.
Granting one video at a time doesn't survive contact with an actual course. Invite links fix that: one link grants a defined bundle of videos, with its own validity window. The tutor sends a link to a new cohort instead of hand-adding thirty students to twelve videos.
Looks like a convenience feature; it's really a correctness feature. Manual granting at that volume produces missed students and — worse — students who keep access after their course ended because nobody remembered to remove them.
Every admin action lands in an append-only audit log. When a student says they should still have access, you have a record instead of a reconstruction.
What this doesn't stop
Worth being blunt, because security writing usually isn't.
None of this stops a determined student from screen-recording. The player disables right-click and shows no download button, and both are deterrents, not controls — they raise the effort for a casual save and stop nothing else. Anyone who wants a copy badly enough will just point a recorder at it.
That's fine, and it's the right place to stop. The realistic threat isn't a pirate; it's a URL forwarded into a group chat, or a student from last term still watching this term's material. Both of those are fully handled by expiring credentials and expiring permissions.
Chasing the rest means DRM, and DRM would cost more than the business loses while making playback worse for every honest student. Knowing which threat you're actually defending against is most of the design.
The bits worth stealing
Three ideas that transfer to anything selling access rather than files:
- Authorise the credential, not the page. Re-check at the moment you hand out the thing that grants access, no matter how recently you checked upstream.
- Make expiry a comparison, not a job. Permissions carrying their own end date can't be forgotten by a cron that died at 3am.
- Name the threat you're not defending against. Otherwise you spend the budget on right-click theatre and skip the expiring URL.