What

MediaBridge is a self-hosted file management layer over AWS S3. It gives teams a proper interface for uploading, browsing, and sharing files across multiple S3 buckets without AWS console access and without distributing IAM credentials to individuals.

It runs in production managing 33 S3 buckets.

Why

The access problem

Content teams upload files constantly: images for a web app, PDFs for client portals, videos for a dashboard. Those files need to live in S3 and they need URLs. The question is how the team gets them there.

Option 1: AWS console access. Technically works. In practice it means distributing IAM credentials, teaching non-technical team members to use the console, and accepting that anyone with access can accidentally delete buckets, change policies, or access data they should not see. Console access is all-or-nothing.

Option 2: Per-service upload endpoints. Build a custom upload form in every application that needs it. Works, but you end up with no unified place to browse what has been uploaded, no access control across services, and the same upload logic duplicated everywhere.

Option 3: A dedicated file management layer. One tool that wraps S3 with proper access controls, a file browser, and upload handling. Teams get URLs. Admins manage who can access what. AWS credentials stay on the server.

MediaBridge is option 3.

The IAM credential problem

Giving someone an IAM access key and secret is a long-term liability. Keys leak in git repos, in Slack messages, in .env files that get copied to the wrong machine. Rotating them requires chasing down every place they were pasted. Revoking them is a manual operation that often does not happen when a contractor leaves.

MediaBridge uses one IAM user per bucket, stored and encrypted on the server. No one outside the backend ever sees an AWS credential. If a bucket’s key needs rotation, it is a single field update in the admin panel.

The Design Philosophy

The frontend knows nothing

Most SPAs make their own decisions: which routes to render, which UI elements to show, which features are active. The logic lives in JavaScript bundles that anyone can read.

MediaBridge is built on the opposite principle. The frontend knows nothing, assumes nothing, and decides nothing. Every piece of structure comes from the server.

On login, the frontend calls GET /auth/me and receives:

{
  "id": "...",
  "name": "Sarah",
  "isAdmin": false,
  "menuItems": ["resources"]
}

The frontend renders exactly that. It does not check a role. It does not hide an admin panel. It does not have routing logic that decides whether to show user management. If "user-management" is not in menuItems, it does not exist as far as the frontend is concerned. The code that renders the admin panel never runs.

An admin gets:

{
  "menuItems": ["resources", "user-management"]
}

And user management appears. Same frontend code, different server response.

This means even if someone reverse engineers the frontend, they just get a worse UI hitting the same backend. There is no client-side gate to bypass. The authorization is on the server, where it cannot be tampered with.

AWS credentials stay in the backend

Every bucket in MediaBridge has its own IAM credentials stored in the database, encrypted at rest. The frontend never sees a bucket name, an access key, or an S3 path. It sees a display name and a list of files.

When a user uploads a file, the backend validates access, generates a presigned S3 URL, and hands that URL to the frontend. The frontend uploads directly to S3 using that URL. The backend is never in the upload path. But the backend controls everything about that URL: which bucket, which path, the allowed file size, the allowed content type. S3 enforces all of those constraints at the infrastructure level.

What It Does

Multi-bucket access control

Each bucket is registered with its own IAM credentials, CloudFront URL, display name, and root path. Root paths let you confine users to a subfolder: a user assigned to projects/marketing/ can only see and upload to that prefix. They cannot go up. Any attempt to access outside their root path triggers an immediate force logout.

Delete permission is configurable per user per bucket. Off by default. An admin can enable it per assignment. A user with delete access on one bucket has no delete access on another.

File browser

Browse folders, view thumbnails, preview files inline without opening a new tab. Images, video, and PDFs render natively in a modal overlay. Grid and list view toggle. Bulk select and bulk delete.

Search runs over WebSocket and streams results folder by folder as the engine walks the S3 tree. Folders already traversed by any user are cached in PostgreSQL, so repeat searches return instantly from SQL with no S3 calls.

Direct-to-S3 upload

Files travel from the browser directly to S3. The server generates presigned PUT URLs with content-type and content-length-range constraints baked in. S3 enforces the 25MB per-file limit itself. Multiple files upload in parallel with per-file progress bars. Before uploading, the frontend checks for filename collisions in the current folder and requires explicit confirmation before any overwrite.

S3 Archive Restore Automation

When a user hits an archived S3 object (Glacier or Deep Archive), the system automatically detects it, triggers the restore, sends an email when the restore starts, and sends another when the file is ready. No human in the loop. The pipeline runs through CloudTrail, EventBridge, two Lambda functions, and five background crons with batched email debouncing. Verified end-to-end on real Glacier files.

Thumbnail pipeline

A Lambda function connected to all 33 buckets generates thumbnails automatically on upload. Images get a 400x300 JPEG via Sharp. Non-image files (PDF, Word, Excel, video, audio, archives) get a colour-coded SVG icon matching the file type. On file delete, the thumbnail is removed automatically. For files uploaded externally via rclone, CLI, or the AWS console, the same Lambda handles cache eviction so the file browser always reflects the actual bucket state.

Stack

Backend: Node.js + TypeScript + Fastify

Frontend: React + Vite

Database: PostgreSQL (metadata cache, URL cache, folder traversal index, audit log, archive restore tracking)

Storage: AWS S3 + CloudFront

Infrastructure: Backend as systemd service, frontend on S3 + CloudFront via GitHub Actions

This Series

The posts that follow go into specific parts of the system: