Designing Subscription-Gated Audio Streaming That Actually Protects the Media
I recently wanted to download an audio track from a platform that required a subscription before it would allow downloads. I did not subscribe. Instead, while the track was playing, I opened DevTools and looked at what the browser was actually doing.
The player was not downloading the entire MP3 in one request. It was making HTTP range requests and receiving 206 Partial Content responses from a CloudFront-hosted media resource. After following the requests, I found that the underlying audio resource was directly accessible.
That was the interesting part for me, not because I had found a way to download an audio file, but because it exposed a design question I would rather answer as an engineer: how should I actually build a subscription-gated audio streaming system? The answer is not to hide the download button. It starts much deeper in the architecture.
The Actual Problem
Suppose I am building an audio platform where users can listen to music, podcasts, courses, or any other audio content. I want to support something like this: free users can access free tracks, subscribers can access subscriber-only tracks, some users may be allowed to stream but not download through the application's intended interface, audio should start playing quickly, large files should not have to be downloaded completely before playback begins, the CDN should handle the heavy bandwidth, the media storage should remain private, and the system should scale without turning the application server into a giant media proxy.
At first glance, this sounds straightforward. I could put an audio player on the frontend and point its src at an MP3:
<audio src="https://cdn.example.com/audio/track.mp3" controls />The problem is that the browser now knows where the file is. And if the URL is publicly accessible, the browser is not going to care whether I put a "Download" button behind a subscription check somewhere else in the interface. The browser needs the media. If the media resource itself is public, the resource is public. That is the fundamental problem.
The Browser Does Not Care About My UI
This is probably the easiest mistake to make when building a subscription-based media application. I can write:
if (!user.hasSubscription) {
hideDownloadButton()
}That may change what the user sees. It does not change whether the audio file can be requested. The browser is ultimately making HTTP requests. A determined user can inspect those requests, see what resources are being fetched, and understand how the application obtains them.
So there are two completely different things at play. There's product-level restriction: don't show the download option to users without a subscription. And there's resource-level authorization: do not allow this user to retrieve this media resource unless they are authorized. Only the second one actually protects the asset, and this distinction becomes particularly important with streaming, because the browser has to retrieve the media somehow.
How Range-Based Audio Streaming Works
One thing I found particularly interesting in the situation I encountered was that the browser was not simply requesting the entire MP3. It was using HTTP byte ranges. A request can look conceptually like this:
GET /audio/track.mp3 HTTP/1.1
Range: bytes=0-1048575The server can respond:
HTTP/1.1 206 Partial Content
Content-Range: bytes 0-1048575/4035568
Content-Length: 1048576
Accept-Ranges: bytesThe 206 Partial Content response tells the client that it received only part of the resource. The Content-Range header tells it which portion it received and the total size of the resource. The browser can then request another portion, Range: bytes=1048576-2097151, and continue from there.
This is useful for media because the browser does not necessarily need the entire file before playback can begin. It also makes seeking much more practical. If a user jumps from 20 seconds into a track to 3 minutes, the browser can request the relevant portion rather than downloading everything that came before it. So HTTP range requests are not a security problem. They are actually a perfectly reasonable way to deliver media. The security problem is what sits behind those requests.
Streaming and Authorization Are Different Problems
This is the architectural distinction I would build the entire system around. Range requests answer how the media should be delivered efficiently. Authorization answers who is allowed to receive it. Those are separate concerns, and I could have a beautifully implemented range-based streaming system and still have terrible access control.
In the naive version, the client sends a range request straight to a public CDN URL, and that URL points directly at a public MP3. The streaming works fine. The authorization simply does not exist anywhere in that picture.
The better version looks different. The client sends an authenticated request to a media API first. That API authenticates the user, authorizes the request, and checks their entitlement to the specific track. Only after that check passes does it grant temporary access to a CDN or delivery layer, which is the only thing allowed to reach the actual private media storage. The important change is that the actual media object is no longer public. Nothing reaches it without first passing through that authorization step.
The Media Should Live Somewhere the Public Cannot Reach
If I upload track-123.mp3 to object storage, I do not want GET https://storage.example.com/track-123.mp3 to work for an unauthenticated user. The media should be private. The application should own the decision about who gets access to it.
That gives me a clean separation. The application database owns the metadata and the entitlements. It hands off to a media authorization step, which grants temporary access. That temporary access is what actually reaches private media delivery, which is the only path to the private object itself. Nothing skips a step in that chain.
The database might contain something like this for a track:
Track
-----
id
title
storage_key
visibility
subscription_requiredAnd the user's subscription might be represented separately:
Subscription
------------
user_id
plan
status
expires_atThe media API brings those pieces together when the user requests access.
The Request Should Pass Through an Authorization Boundary
Instead of giving the frontend a permanent media URL, I would have the application expose something like:
GET /api/tracks/123/stream
Authorization: Bearer <access-token>The backend receives the request and can then work through the actual decision chain: who is this user, is the session valid, does the user have access to track 123, is their subscription active, is streaming permitted, and only after all of that, grant temporary media access.
This is where the subscription actually becomes a security boundary. Not at the button. Not at the React component. Not in some JavaScript variable. At the point where the system decides whether the media resource can be delivered.
The CDN Should Still Do the Heavy Lifting
There is one thing I would not do just because I want proper authorization. I would not make my application server download every MP3 and then stream it byte by byte to users. That turns the application server into a bandwidth bottleneck, and the whole point of a CDN is to move media delivery closer to users and away from the application servers.
So the architecture should stay simple in shape: the client talks to the application, the application authorizes and hands off to the CDN, and the CDN is the only thing that talks to private object storage. The application controls access. The CDN handles delivery. The object store holds the actual asset. That separation matters because these systems have very different responsibilities.
Short-Lived Access Instead of Permanent URLs
Once I decide that the media should be private, the next question is how the browser gets access to it. One common solution is temporary authorization. Instead of handing the browser https://cdn.example.com/audio/track-123.mp3, I can issue access that is valid only for a limited period and within the intended scope. Conceptually, the authorization might represent something like a resource of track-123, an operation of stream, and an expiry of 10 minutes.
The exact implementation can vary. Depending on the delivery architecture, this could involve signed URLs, signed cookies, an authenticated media endpoint, or another mechanism provided by the delivery layer. The important property isn't the name of the mechanism. It's that the underlying object remains private, while authorized clients receive temporary permission to retrieve it. That is a much stronger model than exposing a permanent public object URL.
But what happens to range requests?
This is where the architecture has to be designed properly. The browser still wants to make requests like Range: bytes=0-1048575 and later Range: bytes=1048576-2097151. The authorization mechanism therefore has to work across the media delivery process. The CDN should be able to determine that the request is authorized to retrieve the requested object while still supporting normal HTTP range semantics. The result can remain a standard 206 Partial Content response. The difference is that the request is no longer reaching an unrestricted public object.
The conceptual flow becomes authentication, then authorization, then temporary media access, then the actual HTTP range request, through the CDN, down to the private object. This is the architecture I would want. The range request solves efficient streaming. The authorization mechanism solves entitlement. Neither needs to replace the other.
What About Preventing Downloads?
This is where things get more complicated. Suppose I have a subscriber who is authorized to listen to a track. The browser must receive the audio data to play it. That means I cannot honestly promise that the user can listen to the audio while making it physically impossible for them to capture the audio.
If the client can receive the media, a sufficiently determined user can potentially capture it. They could record the output. They could inspect the network traffic. They could reconstruct the media from authorized requests. They could use specialized software.
So there is an important distinction between preventing unauthorized access to the media and making it mathematically impossible for an authorized user to copy the media. The first is a realistic security objective. The second is much harder and, for ordinary browser-based audio, generally not something I would promise.
This means a subscription-gated streaming platform should not define its security model as "users cannot download our files." It should define it more precisely as "only users entitled to the content can retrieve the media through our delivery system." That is a meaningful security boundary.
If I Really Needed Stronger Protection
There are situations where simple private objects and temporary access are not enough. For example, a premium music platform may have strong incentives to make unauthorized redistribution substantially harder. At that point, I would start considering technologies such as encrypted media, DRM, protected playback environments, forensic watermarking, or other forms of content protection.
But that is a different problem from accidentally exposing a public MP3. There is no reason to introduce an enormous DRM architecture when the fundamental problem is simply that GET /audio/track.mp3 works without authorization. The first job is to establish the correct access boundary. Then stronger content protection can be layered on if the business actually requires it.
Caching Introduces Another Consideration
There is another architectural detail that is easy to overlook. If a CDN caches media, I need to make sure that authorization does not accidentally become detached from the cached resource.
Imagine a user makes an authorized request, the CDN fetches the media from private storage, and now the CDN has that media cached. If I design the cache policy incorrectly, I could end up with a situation where authorization was checked for the first request but subsequent requests receive the cached resource without the appropriate access controls. That would defeat the whole purpose.
The delivery architecture therefore has to ensure that authorization and caching interact correctly. This is one reason I would prefer the authorization model to be built into the media delivery layer rather than bolted onto the frontend. The CDN can still cache aggressively. But the cache must not turn an authorized resource into an anonymously accessible resource.
I Would Also Separate Media Metadata From Media Access
Another design choice I would make is not to treat the media file itself as the source of truth for application permissions. The database should know things like the track's title, its visibility as subscriber-only, and its storage key. The media storage should not need to understand the entire subscription system. The application owns the business logic. The media layer owns delivery.
That gives me a clean boundary: business logic asks "can this user stream this track," an authorization decision gets made, and only then does the client get told "give this client temporary access" before media delivery happens.
This becomes particularly useful once the platform has more complicated rules. Maybe a user has purchased one track but does not have a subscription. Maybe a subscription expires in three hours. Maybe an artist has made a track private. Maybe a user is allowed to stream but not access certain territories. Those decisions belong in the application authorization layer. The CDN should not become my business logic engine.
The System I Would Actually Build
Putting everything together, the client talks to a media API over an authenticated request. That media API handles authentication, authorization, subscription checks, and track-level permissions, and it's the only thing that decides whether temporary authorization gets issued. That temporary authorization is what lets the request reach the CDN, which handles range requests, caching, and the actual media delivery. Only the CDN ever talks to private storage, where the raw track files live.
The responsibilities are deliberately boring:
| Component | Responsibility |
|---|---|
| Client | Authentication, playback, range requests |
| Media API | Authentication and entitlement decisions |
| CDN | Efficient global media delivery |
| Object storage | Private persistence of media |
| Database | Track metadata and subscription state |
That is exactly what I want from the architecture. Each layer has a job. The frontend does not enforce authorization. The CDN does not decide who is a subscriber. The object store does not understand subscription plans. The application does not become a media streaming bottleneck.
The Lesson From Finding the Exposed File
Coming back to the situation that started this article, the interesting part was not that the browser was using range requests. That was perfectly reasonable. The problem was that I could follow the browser's requests to the underlying media resource and retrieve it directly. The application had created a restriction around the user interface, while the actual media resource sat behind a much weaker boundary.
That is an important distinction because modern applications are full of abstractions that can create a false sense of security. A button can be hidden. A route can be hidden. A component can be hidden. A download option can be hidden. None of those things matter if the resource itself remains publicly retrievable. The resource is where the security boundary needs to exist.
Streaming Is Not Access Control
This is the principle I would take away from the entire exercise. A platform can have HTTP range requests, 206 Partial Content responses, CDN caching, fast global delivery, buffering, seeking, a beautiful subscription system, and a frontend with no download button, and still expose its media. None of those features automatically provide authorization. Streaming determines how content moves. Authorization determines who is allowed to receive it. Those are different engineering problems.
If I ever build an audio platform where downloads are subscription-gated, I would not start by asking how to hide the download button. I would start with what happens when an unauthorized client asks for the bytes. If the answer is that the CDN gives them the file anyway, then the subscription gate is not protecting the content.
The solution is to make the media private, put authorization at the resource boundary, give authorized clients appropriately constrained access, and let the CDN handle the expensive work of delivering the bytes. That is a much more interesting system than simply disabling a download button.
Comments
Post a Comment