Skip to content

16 September 2025 · 2 min read

Big uploads straight to storage with signed URLs

How a healthcare app uploads large patient videos straight to cloud storage with short-lived signed URLs, so the API never touches the bytes.

In a heart-failure care app I worked on, patients record short neck videos at home for a clinical assessment. Video files are large, mobile networks are flaky, and the data is sensitive. Routing those uploads through the API server would have made it a bottleneck and a liability. Instead, the app uploads straight to cloud storage using short-lived signed URLs, and the API only ever handles small JSON requests.

Why not upload through the API

  • Memory and timeouts. Buffering a large video per request ties up the server and hits request-size and timeout limits (serverless platforms cap request bodies at a few MB).
  • Cost and latency. Every byte crosses the network twice.
  • Blast radius. The API never needs broad write access to the bucket.

The flow

text
1. app  ──POST /uploads/initiate──►  API   (auth, checks, returns signed PUT URL)
2. app  ──PUT video──────────────►  storage (direct, URL valid ~15 min)
3. app  ──POST /uploads/complete──►  API   (verify the object exists, create the record)
4. worker ─poll pending─► API ─► process ─► upload result ─► notify the patient

The API stays in charge of who may upload what, without ever touching the bytes.

Signing a URL with no SDK

Google Cloud Storage accepts URLs signed with a service account's private key. You don't need the storage SDK for this; Node's crypto does it:

ts
import { createSign, randomUUID } from 'node:crypto';

function signedUploadUrl(bucket: string, path: string, contentType: string) {
  const { client_email, private_key } = serviceAccount(); // from a secret, never the repo
  const expires = Math.floor(Date.now() / 1000) + 15 * 60;
  const stringToSign = `PUT\n\n${contentType}\n${expires}\n/${bucket}/${path}`;
  const signature = createSign('RSA-SHA256').update(stringToSign).sign(private_key, 'base64');
  return (
    `https://storage.googleapis.com/${bucket}/${path}` +
    `?GoogleAccessId=${encodeURIComponent(client_email)}` +
    `&Expires=${expires}&Signature=${encodeURIComponent(signature)}`
  );
}

// Names are server-generated, so clients can't overwrite or guess paths.
const objectName = `${randomUUID()}.mp4`;

The signature covers the method, content type, expiry and exact path. A URL for one file can't be reused to write another, and it dies after 15 minutes.

The details that made it reliable

  • Server-generated names. The client never chooses the object path; the API generates a UUID name inside a per-patient, per-assessment folder.
  • Verify before you record. On "complete", the API checks the object really exists before creating anything in the database. A cheap way: a signed GET with Range: bytes=0-0, which returns one byte instead of the whole video.
  • Reads are signed too. Clinicians get short-lived read URLs on demand; nothing in the bucket is public.
  • Idempotent deletes. Deleting treats 404 as success, so retries and clean-up jobs never fail on something already gone.
  • Lock the internal endpoints properly. The processing worker talks to internal endpoints protected by a shared secret, checked in a guard, so unauthenticated requests are rejected before any multipart body is buffered into memory. Checking inside the handler would let anyone make the server allocate memory first.
  • Set CORS on the bucket for the app's origins and the PUT method, or browsers will block the direct upload.

What it bought us

The API handles small, fast requests; uploads scale with the storage provider, not the server; and the security model is easy to explain: the server signs narrowly scoped, short-lived permissions, and the client spends them.

The same pattern works on S3 (presigned URLs) and Azure (SAS tokens).