Part of the MediaBridge series.
Overview
MediaBridge has two deployable components and two AWS-side components:
- Backend: Node.js process running as a systemd service behind nginx
- Frontend: Static React build deployed to S3, served via CloudFront
- Thumbnail Lambda: AWS Lambda handling S3 object events
- Archive restore pipeline: CloudTrail, EventBridge, and two Lambda functions
Prerequisites
- A Linux server with Node.js 18+, PostgreSQL, and nginx
- An AWS account with at least one S3 bucket and IAM credentials for it
- A domain with DNS pointing to your server (for TLS)
Backend
Clone and install:
git clone https://github.com/sagarnayak/mediabridgeBackend-public.git
cd mediabridgeBackend-public
npm install
Configure:
cp .env.example .env
Edit .env. Generate fresh values for JWT_SECRET and ENCRYPTION_KEY:
openssl rand -hex 64 # JWT_SECRET
openssl rand -hex 32 # ENCRYPTION_KEY
Set DATABASE_URL to your PostgreSQL connection string and FRONTEND_URL to your frontend’s public URL (used for CORS).
Run migrations:
npm run migrate
Build:
npm run build
Systemd service at /etc/systemd/system/mediabridge.service:
[Unit]
Description=MediaBridge Backend
After=network.target
[Service]
User=youruser
WorkingDirectory=/path/to/mediabridgeBackend-public
EnvironmentFile=/path/to/mediabridgeBackend-public/.env
ExecStart=/usr/bin/node dist/index.js
Restart=on-failure
RestartSec=5
[Install]
WantedBy=multi-user.target
sudo systemctl daemon-reload
sudo systemctl enable mediabridge
sudo systemctl start mediabridge
Nginx Reverse Proxy
The backend listens on the port set in PORT (default 4000). Nginx handles TLS termination and proxies to the backend. The WebSocket search endpoint requires Upgrade headers:
server {
listen 443 ssl;
server_name api.yourdomain.com;
ssl_certificate /etc/letsencrypt/live/api.yourdomain.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/api.yourdomain.com/privkey.pem;
location / {
proxy_pass http://localhost:4000;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
}
The Upgrade and Connection headers are required for WebSocket connections. Without them, the search endpoint fails silently.
Frontend
Clone and install:
git clone https://github.com/sagarnayak/mediabridgeFrontend-public.git
cd mediabridgeFrontend-public
npm install
Configure:
cp .env.example .env
Set VITE_API_URL to your backend’s public URL.
Build:
npm run build
This produces a dist/ directory of static files.
S3 hosting: Create an S3 bucket for the frontend. Upload the dist/ contents. Enable static website hosting or serve via CloudFront (recommended). Set the CloudFront default root object to index.html and configure a custom error response: 404 -> /index.html with status 200. This is required for React Router to work on direct URL loads.
GitHub Actions deployment: The included deploy.yml workflow is set to manual trigger (workflow_dispatch). To use it, add these secrets to your repository:
| Secret | Value |
|---|---|
VITE_API_URL | Your backend URL |
S3_BUCKET | S3 bucket name |
AWS_ACCESS_KEY_ID | IAM key with S3 and CloudFront permissions |
AWS_SECRET_ACCESS_KEY | IAM secret |
CLOUDFRONT_DISTRIBUTION_ID | CloudFront distribution ID |
Run the workflow manually from the Actions tab to deploy.
Registering Your First Bucket
After the backend is running and you can reach it, visit /setup in the frontend (the backend will have no users yet and will redirect there). Create the master admin account.
In the admin panel, go to AWS Keys and add your first bucket:
- Display name: what users see
- System code: short identifier used in audit logs
- Bucket name: the actual S3 bucket name
- Region: the bucket’s AWS region
- Access Key ID and Secret: IAM credentials for this bucket
- CloudFront Base URL: if the bucket is behind CloudFront, e.g.
https://cdn.yourdomain.com. Leave blank for private buckets. - Root path: optional prefix to confine all operations to a sub-path, e.g.
shared/
Credentials are encrypted with ENCRYPTION_KEY before being stored.
Thumbnail Lambda
The thumbnail Lambda is a single index.js file with Sharp as its only dependency. Source code is in the backend README.
Deploy:
mkdir lambda-thumbnail && cd lambda-thumbnail
# Copy index.js from the README
npm install @aws-sdk/client-s3 sharp
zip -r function.zip index.js node_modules/
Upload function.zip to AWS Lambda (Node.js 22.x runtime). Set the handler to index.handler.
Environment variables on the Lambda:
EVICT_WEBHOOK_URL = https://api.yourdomain.com
EVICT_WEBHOOK_SECRET = (must match EVICT_WEBHOOK_SECRET in your backend .env)
S3 triggers: Add ObjectCreated:* and ObjectRemoved:* event notifications on each of your S3 buckets pointing to this Lambda. Update the THUMBNAIL_BUCKETS set in the Lambda source to list the buckets that should get image thumbnail generation (cache eviction fires for all connected buckets regardless).
Lambda permissions: The Lambda execution role needs s3:GetObject, s3:PutObject, s3:DeleteObject, and s3:HeadObject on your buckets.
Archive Restore Pipeline
This pipeline is optional. Skip it if you do not use Glacier or Deep Archive storage classes.
Enable CloudTrail on your AWS account if not already enabled. CloudTrail captures all S3 GetObject calls including those that fail because the object is archived.
EventBridge rule: Create a rule that matches S3 events where errorCode is InvalidObjectState (the error S3 returns when accessing an archived object). Route matching events to Lambda A.
Lambda A receives the CloudTrail event, extracts s3_bucket, s3_key, and access_key_id from it, and posts to POST /restore/request on your backend with X-Restore-Secret authentication.
Lambda B is triggered by the S3 event ObjectRestore:Completed. It extracts bucket and key and posts to POST /restore/completed.
Both Lambdas use the same RESTORE_WEBHOOK_SECRET which must match RESTORE_WEBHOOK_SECRET in your backend .env.
aws_key_user_map table: Populate this table with your IAM access key to email mappings. The restore crons use it to resolve which user triggered the access attempt and send them the notification emails.
Backend SMTP: Set SMTP_HOST, SMTP_PORT, SMTP_USER, SMTP_PASS, and SMTP_FROM in your .env. These are required for the restore email notifications.
Restore IAM credentials: Set RESTORE_AWS_ACCESS_KEY_ID, RESTORE_AWS_SECRET_ACCESS_KEY, and RESTORE_AWS_REGION in your .env. This is a separate IAM user that only has s3:RestoreObject and s3:GetObject (for HeadObject on pending restores). The per-bucket IAM credentials stored in the database do not need restore permissions.
Checklist
- Backend
.envconfigured with real secrets - Migrations run (
npm run migrate) - Backend builds cleanly (
npm run build) - systemd service enabled and running
- Nginx config with Upgrade headers for WebSocket
- TLS certificate installed
- Frontend
.envpointing to backend URL - Frontend built and deployed to S3
- CloudFront configured with
index.htmlfallback for 404s - At least one bucket registered in admin panel
- Thumbnail Lambda deployed with S3 event triggers
- (Optional) Archive restore pipeline wired up