The Route That Never Touches the File
When we built the private training portal for our agricultural client, getting video files into Vercel's private blob store turned out to be a separate problem from serving them.
Uploading is easy when the file is small. You POST it to an API route, the function receives the bytes, writes them to storage, responds. That flow breaks at 4.5MB: Vercel's serverless functions have a hard limit on request body size. A training video is 50-100MB. It was never going to fit.
The Client-Upload Flow
Vercel's Blob SDK has a solution for this. Instead of bytes going browser to function to storage, they go browser to storage directly. The function's only job is to decide whether the upload is allowed and mint a short-lived token if it is.
The flow:
- The client (a local Node.js script, in our case) calls the API route with the destination pathname and a shared secret.
- The route verifies the secret, inspects the pathname, and if everything looks right, calls
handleUpload()from@vercel/blob/client. That function generates a scoped upload token and returns it. - The client receives the token and uses it to upload the file directly to Vercel's blob infrastructure.
- The function never sees the video bytes at all.
This pattern sidesteps the request body limit entirely. The heavy transfer happens outside the serverless execution context.
The Security Constraints Matter
An upload endpoint that mints tokens freely is an open bucket. We built it to fail closed.
If the TRAINING_UPLOAD_SECRET environment variable is not set, the route refuses every request with an error. It does not fall back to some default behavior. An unset secret means uploads are disabled, not unprotected.
The secret comparison uses timing-safe equality. Comparing strings with === leaks information about the match length through response time differences. Timing-safe comparison takes the same amount of time whether the secret is completely wrong or off by one character at the end.
The destination pathname is checked against a regex before the token is issued. The pattern requires a valid filename and restricts the extension to .mp4. That means the token minted by this route cannot be used to overwrite arbitrary blobs in the store. If a request comes in asking to upload to ../../config.json or media/photo.jpg, the route rejects it before a token is generated.
Content type is locked to video/mp4. The uploaded file cannot claim to be something else. And there is a size cap: 500MB. That is well above any training video we would realistically upload, but it exists so a compromised secret cannot be used to fill the store with arbitrarily large files.
The Upload Script
The other half is a small Node.js script that uses the SDK's upload() function. It reads the secret from .env.local, accepts a local file path and a blob pathname as arguments, and does the two-step: first calls the token route, then sends the file directly to Blob.
node scripts/upload-training-video.mjs ./module-1.mp4 freedom-ag/module-1.mp4
Output tells you the file size before it starts, elapsed time when it finishes, and the final blob pathname. Not sophisticated, but it's the kind of tooling that only the person deploying the portal ever runs, so it does not need to be more than that.
What This Adds to the Portal Build
The training portal has three video-adjacent problems we solved in sequence:
First, the delivery route had to proxy private blobs through authentication. Then it had to handle Range requests so browsers could seek and Safari would play at all. Then the video file itself had to have its moov atom at the front so playback could start before the full download completed.
The upload route closes the loop on the operator side. Getting a new video into the system is one command, secured by a secret that only we hold, with constraints narrow enough that the endpoint can only do the one thing it is supposed to do.
An upload endpoint that fails open is an open bucket. We kept the constraint explicit: no secret, no uploads.