Back to Blog
Cloud StorageCDNArchitecture

Browser Direct Upload to Object Storage with CDN: A Practical Architecture

Implementing browser-to-OSS direct uploads, relative path storage, and CDN-based image processing

Author: ekent·Published on January 24, 2026

File uploads are fundamental to web applications, but many teams implement them as: frontend uploads to backend → backend forwards to object storage. This is straightforward but routes all file traffic through your server, increasing bandwidth costs and server load. We adopted a browser direct upload + CDN architecture that dramatically reduced server burden.

The Problem with Server-Relay Uploads

Browser → Backend Server → Object Storage (OSS/COS)

A 10MB image going through the backend means:

  • The server processes a 10MB request body, consuming memory and bandwidth
  • Upload speed is limited by the slower of the two hops
  • Concurrent uploads can bottleneck the server

Browser Direct Upload

The idea is simple: let the browser upload directly to object storage. The backend only issues temporary credentials:

Browser → Object Storage (direct)
   ↑
Backend issues STS credentials

Backend: Issuing Temporary Credentials

Using Tencent Cloud COS as an example, the backend generates scoped temporary keys via STS (Security Token Service):

// API: GET /api/upload/credentials
export async function GET() {
  const credentials = await getStsCredential({
    secretId: process.env.COS_SECRET_ID,
    secretKey: process.env.COS_SECRET_KEY,
    durationSeconds: 1800, // Valid for 30 minutes
    policy: {
      statement: [{
        effect: 'allow',
        action: ['cos:PutObject', 'cos:PostObject'],
        resource: [`qcs::cos:${region}:uid/${appId}:${bucket}/uploads/*`],
      }],
    },
  });

  return Response.json(credentials);
}

Key security decisions:

  • Credentials expire after 30 minutes
  • Permissions restricted to PutObject and PostObject—no delete or list
  • Resource path scoped to /uploads/*—can't write elsewhere

Frontend: Direct Upload

The frontend fetches credentials and uploads directly:

async function uploadFile(file: File) {
  // 1. Get temporary credentials
  const creds = await fetch('/api/upload/credentials').then(r => r.json());

  // 2. Generate storage path (organized by date)
  const date = new Date().toISOString().slice(0, 10);
  const key = `uploads/${date}/${generateId()}_${file.name}`;

  // 3. Upload directly to COS
  await cos.putObject({
    Bucket: bucket,
    Region: region,
    Key: key,
    Body: file,
    Headers: {
      'x-cos-security-token': creds.sessionToken,
    },
  });

  // 4. Return the relative path (no domain)
  return key;
}

Store Relative Paths, Resolve CDN URLs at Display Time

This is an easily overlooked but critical design choice: store only relative paths in the database, never full URLs.

✅ Database: uploads/2026-01-24/abc123_photo.jpg
❌ Database: https://cdn.example.com/uploads/2026-01-24/abc123_photo.jpg

Why? Because CDN domains change. Switching cloud providers, updating CDN acceleration domains, or binding multiple domains to one bucket (separate domains for different regions)—if you stored full URLs, migration means bulk-updating every record.

Resolve at display time instead:

function getImageUrl(relativePath: string): string {
  const cdnBase = process.env.CDN_BASE_URL; // https://cdn.example.com
  return `${cdnBase}/${relativePath}`;
}

CDN Image Processing

Major cloud providers support real-time image processing via URL parameters—no need to pre-generate thumbnails:

function getThumbnail(path: string, width: number): string {
  const base = getImageUrl(path);
  // Tencent CI: resize to width, auto WebP
  return `${base}?imageMogr2/thumbnail/${width}x/format/webp`;
}

// Alibaba Cloud OSS style:
// ${base}?x-oss-process=image/resize,w_${width}/format,webp

List pages use small images (200px wide), detail pages use medium (800px), and click-to-zoom shows the original. Same image, different sizes per context—the bandwidth savings are significant.

Handling URLs in Rich Text

If your project includes a rich text editor (e.g., for articles), uploaded images are embedded as full URLs in HTML. You need to convert to relative paths on save and back to full URLs on read:

// Before saving: full URL → relative path
function normalizeContent(html: string): string {
  return html.replace(
    new RegExp(`${CDN_BASE_URL}/`, 'g'),
    ''
  );
}

// After reading: relative path → full URL
function renderContent(html: string): string {
  return html.replace(
    /(src=["'])(uploads\/)/g,
    `$1${CDN_BASE_URL}/$2`
  );
}

Takeaways

The browser direct upload + CDN architecture is built on three separations: upload traffic bypasses the server, stored paths exclude domains, and image processing is handled by CDN. In our project, this reduced file-upload-related server bandwidth by over 90%, while CDN caching and edge delivery improved image load times by 3-5x. Any web application with file upload needs should consider this architecture.