Wizbrand Video Storage — Developer Handbook
1. Purpose
This document defines how the Wizbrand application must implement video:
- Upload
- Download
- Listing
- Delete
- Tenant isolation
- User isolation
- S3 object naming
- Database metadata
- Security validation
Video files are stored in the private AWS S3 bucket:
videos.wizbrand.com
The application runs on EC2.
Developers will not receive AWS IAM users, access keys, secret keys, or AWS Console access for this feature.
The EC2 application receives access to S3 through an AWS IAM Role configured by DevOps.
2. Architecture
The application must use this architecture:
┌──────────────────────┐
│ User │
│ Browser / Mobile App │
└──────────┬───────────┘
│
│ HTTPS
▼
┌──────────────────────┐
│ Wizbrand Application │
│ on EC2 │
│ │
│ Authentication │
│ Authorization │
│ Tenant validation │
│ Video metadata │
│ Presigned URLs │
└──────────┬───────────┘
│
EC2 IAM Role
│
▼
┌──────────────────────┐
│ Private AWS S3 │
│ videos.wizbrand.com │
└──────────────────────┘
The application server must not proxy video content through EC2.
Correct:
Browser ──────────────► S3
video upload
Incorrect:
Browser ──► EC2 ──► S3
video
For downloads:
Browser ◄────────────── S3
video
not:
Browser ◄── EC2 ◄── S3
3. AWS Authentication Model
Application code must use the AWS SDK default credential provider.
Do not configure AWS credentials manually.
Correct:
import { S3Client } from "@aws-sdk/client-s3";
export const s3 = new S3Client({
region: process.env.AWS_REGION,
});
Forbidden:
const s3 = new S3Client({
credentials: {
accessKeyId: "...",
secretAccessKey: "..."
}
});
Forbidden environment variables:
AWS_ACCESS_KEY_ID=
AWS_SECRET_ACCESS_KEY=
Forbidden:
.env file containing AWS credentials
The AWS SDK running on EC2 automatically obtains temporary credentials from the EC2 IAM Role.
4. Environment Variables
DevOps will provide non-secret application configuration similar to:
AWS_REGION=ap-south-1
VIDEO_S3_BUCKET=videos.wizbrand.com
VIDEO_UPLOAD_URL_EXPIRY_SECONDS=900
VIDEO_DOWNLOAD_URL_EXPIRY_SECONDS=300
VIDEO_MAX_SIZE_BYTES=5368709120
Example meanings:
900 seconds = 15-minute upload URL
300 seconds = 5-minute download URL
5368709120 = 5 GB
These values may vary by environment.
Code must not hard-code them.
5. Multi-Tenant Storage Model
Every S3 object must live under a tenant-specific and user-specific prefix.
Required format:
tenants/{tenant_id}/users/{user_id}/videos/{video_id}/{filename}
Example:
tenants/
tenant-f936d1/
users/
user-a8711c/
videos/
video-e28c4a/
original.mp4
Full S3 object key:
tenants/tenant-f936d1/users/user-a8711c/videos/video-e28c4a/original.mp4
6. Never Trust Tenant IDs From the Client
The browser must not determine the tenant.
Forbidden:
const tenantId = request.body.tenantId;
Required:
const tenantId = session.tenantId;
const userId = session.userId;
The authenticated session/JWT/backend authentication system is authoritative.
The same applies to:
user_id
tenant_id
organization_id
S3 key
S3 prefix
The client must never control these security-sensitive values.
7. Never Accept an S3 Key From the Client
Forbidden API:
{
"s3Key":
"tenants/company-b/users/user-9/videos/private.mp4"
}
Required:
const videoId = crypto.randomUUID();
const objectKey =
`tenants/${tenantId}` +
`/users/${userId}` +
`/videos/${videoId}` +
`/original.mp4`;
The backend owns the complete S3 key-generation process.
8. Video Database Model
S3 stores the binary object.
The application’s database remains the source of truth for ownership and metadata.
Suggested videos table:
videos
------------------------------------------------
id
tenant_id
user_id
s3_bucket
s3_key
original_filename
content_type
size_bytes
status
created_at
updated_at
deleted_at
Recommended statuses:
UPLOADING
READY
FAILED
DELETED
If media processing is introduced later:
PROCESSING
TRANSCODING
QUARANTINED
may be added.
Example record:
id:
video-e28c4a
tenant_id:
tenant-f936d1
user_id:
user-a8711c
s3_bucket:
videos.wizbrand.com
s3_key:
tenants/tenant-f936d1/users/user-a8711c/videos/video-e28c4a/original.mp4
original_filename:
training-video.mp4
content_type:
video/mp4
size_bytes:
821633445
status:
READY
9. Recommended API Design
Implement approximately these endpoints:
POST /api/videos/uploads
POST /api/videos/{videoId}/uploads/complete
GET /api/videos
GET /api/videos/{videoId}
GET /api/videos/{videoId}/download
DELETE /api/videos/{videoId}
If multipart upload is implemented:
POST /api/videos/uploads/multipart
POST /api/videos/{videoId}/parts
POST /api/videos/{videoId}/multipart/complete
DELETE /api/videos/{videoId}/multipart
10. Upload Workflow
The required flow is:
1. User selects video
2. Browser calls application:
POST /api/videos/uploads
3. Application authenticates user
4. Application determines:
tenant_id
user_id
5. Application validates:
file name
content type
file size
quota
6. Application generates:
video_id
7. Application generates:
S3 object key
8. Application creates DB record:
status = UPLOADING
9. Application generates:
short-lived presigned S3 upload URL
10. Application returns URL to browser
11. Browser uploads video directly to S3
12. Browser calls:
POST /api/videos/{id}/uploads/complete
13. Backend verifies S3 object
14. Backend updates:
status = READY
11. Upload Initiation Request
Example:
POST /api/videos/uploads
Content-Type: application/json
Authorization: Bearer <application-token>
Request:
{
"filename": "training-video.mp4",
"contentType": "video/mp4",
"size": 821633445
}
The client is allowed to provide:
filename
contentType
size
The client is not allowed to provide:
tenantId
userId
bucket
s3Key
s3Prefix
AWS role
12. Upload Initiation Response
Example:
{
"videoId": "video-e28c4a",
"uploadUrl": "https://...",
"method": "PUT",
"expiresIn": 900,
"contentType": "video/mp4"
}
Do not return:
AWS access key
AWS secret key
AWS session credentials
IAM role credentials
13. Generating a Presigned Upload URL
Install:
npm install @aws-sdk/client-s3 @aws-sdk/s3-request-presigner
Example:
import {
PutObjectCommand,
S3Client
} from "@aws-sdk/client-s3";
import {
getSignedUrl
} from "@aws-sdk/s3-request-presigner";
const s3 = new S3Client({
region: process.env.AWS_REGION
});
export async function createUploadUrl({
tenantId,
userId,
videoId,
contentType
}: {
tenantId: string;
userId: string;
videoId: string;
contentType: string;
}) {
const key =
`tenants/${tenantId}` +
`/users/${userId}` +
`/videos/${videoId}` +
`/original.mp4`;
const command = new PutObjectCommand({
Bucket: process.env.VIDEO_S3_BUCKET!,
Key: key,
ContentType: contentType
});
const url = await getSignedUrl(
s3,
command,
{
expiresIn:
Number(
process.env.VIDEO_UPLOAD_URL_EXPIRY_SECONDS
) || 900
}
);
return {
key,
url
};
}
14. Browser Upload
The browser uploads directly to the presigned URL.
Example:
async function uploadVideo(
file: File,
uploadUrl: string
) {
const response = await fetch(uploadUrl, {
method: "PUT",
headers: {
"Content-Type": file.type
},
body: file
});
if (!response.ok) {
throw new Error(
`Video upload failed: ${response.status}`
);
}
}
The Content-Type used during upload should match the value included when signing the URL.
15. Upload Progress
For good UX, implement upload progress.
If using XMLHttpRequest:
function uploadWithProgress(
file: File,
uploadUrl: string,
onProgress: (percent: number) => void
) {
return new Promise<void>((resolve, reject) => {
const xhr = new XMLHttpRequest();
xhr.open("PUT", uploadUrl);
xhr.setRequestHeader(
"Content-Type",
file.type
);
xhr.upload.onprogress = event => {
if (!event.lengthComputable) {
return;
}
const percent =
Math.round(
(event.loaded / event.total) * 100
);
onProgress(percent);
};
xhr.onload = () => {
if (
xhr.status >= 200 &&
xhr.status < 300
) {
resolve();
} else {
reject(
new Error(
`Upload failed: ${xhr.status}`
)
);
}
};
xhr.onerror = () =>
reject(
new Error("Network upload error")
);
xhr.send(file);
});
}
Display progress to the user:
Uploading: 4%
Uploading: 27%
Uploading: 69%
Uploading: 100%
16. Upload Completion
After successful S3 upload:
POST /api/videos/video-e28c4a/uploads/complete
The backend must not blindly mark the upload READY.
Verify the S3 object first.
Example:
import {
HeadObjectCommand
} from "@aws-sdk/client-s3";
const result = await s3.send(
new HeadObjectCommand({
Bucket: process.env.VIDEO_S3_BUCKET,
Key: video.s3Key
})
);
Verify at least:
object exists
ContentLength > 0
ContentLength matches expected size
ContentType is expected
Then:
UPLOADING → READY
If verification fails:
UPLOADING → FAILED
17. File Validation
Validate uploads before issuing a presigned URL.
At minimum validate:
filename
extension
MIME type
file size
user quota
tenant quota
Example allowed MIME types:
video/mp4
video/webm
video/quicktime
Example:
const allowedTypes = new Set([
"video/mp4",
"video/webm",
"video/quicktime"
]);
if (!allowedTypes.has(contentType)) {
throw new BadRequestError(
"Unsupported video format"
);
}
18. File Size Validation
Example:
const MAX_VIDEO_SIZE =
Number(process.env.VIDEO_MAX_SIZE_BYTES);
if (size <= 0) {
throw new BadRequestError(
"Invalid file size"
);
}
if (size > MAX_VIDEO_SIZE) {
throw new BadRequestError(
"Video exceeds maximum allowed size"
);
}
Remember:
Client-side validation is for UX.
Backend validation is mandatory for security.
19. File Extensions Are Not Security Validation
Do not assume:
something.mp4
is necessarily a valid MP4 video.
Filename and browser-provided MIME type can both be manipulated.
For initial implementation, use them as preliminary validation.
For stronger validation later, inspect the uploaded media or use a media-processing/security pipeline before marking the content trusted.
20. Listing Videos
Do not use:
S3 ListObjects
as the primary application video listing mechanism.
Use the application database.
Example:
SELECT
id,
original_filename,
content_type,
size_bytes,
status,
created_at
FROM videos
WHERE tenant_id = :tenant_id
AND user_id = :user_id
AND deleted_at IS NULL
ORDER BY created_at DESC;
Endpoint:
GET /api/videos
Example response:
{
"videos": [
{
"id": "video-e28c4a",
"filename": "training-video.mp4",
"contentType": "video/mp4",
"size": 821633445,
"status": "READY",
"createdAt": "2026-09-25T10:30:00Z"
}
]
}
Do not expose internal s3_key unless there is a specific technical requirement.
21. Download Workflow
Required flow:
User
↓
GET /api/videos/{videoId}/download
↓
Application authentication
↓
Tenant authorization
↓
User authorization
↓
Look up video in DB
↓
Generate short-lived S3 GET URL
↓
Return URL
↓
Browser downloads directly from S3
22. Download Authorization
Before generating a URL:
SELECT *
FROM videos
WHERE id = :video_id
AND tenant_id = :tenant_id
AND user_id = :user_id
AND deleted_at IS NULL;
If there is no record:
404 or 403
according to application security conventions.
Do not perform:
SELECT *
FROM videos
WHERE id = :video_id;
without tenant/user authorization.
23. Generate Download URL
Example:
import {
GetObjectCommand
} from "@aws-sdk/client-s3";
import {
getSignedUrl
} from "@aws-sdk/s3-request-presigner";
async function createDownloadUrl(
s3Key: string
) {
const command = new GetObjectCommand({
Bucket: process.env.VIDEO_S3_BUCKET,
Key: s3Key
});
return getSignedUrl(
s3,
command,
{
expiresIn:
Number(
process.env.VIDEO_DOWNLOAD_URL_EXPIRY_SECONDS
) || 300
}
);
}
Example API response:
{
"url": "https://...",
"expiresIn": 300
}
24. Download URL Lifetime
Keep download URLs short-lived.
Recommended default:
5 minutes
Upload URLs:
10–15 minutes
Do not create URLs valid for:
1 day
7 days
30 days
unless there is a specific approved requirement.
A presigned URL should be treated as a temporary bearer credential.
Anyone possessing the URL may use it until it expires.
25. Delete Workflow
Deletion must go through the application backend.
Do not generate presigned DELETE URLs.
Required flow:
User
↓
DELETE /api/videos/{videoId}
↓
Application authentication
↓
Tenant/user ownership check
↓
DB lookup
↓
S3 DeleteObject
↓
Update/delete DB record
26. Delete Example
import {
DeleteObjectCommand
} from "@aws-sdk/client-s3";
async function deleteVideo(
s3Key: string
) {
await s3.send(
new DeleteObjectCommand({
Bucket: process.env.VIDEO_S3_BUCKET,
Key: s3Key
})
);
}
Important:
Never accept this:
{
"s3Key":
"tenants/some-other-company/video.mp4"
}
The backend loads the S3 key from its own database.
27. Delete Database Transaction
A safe implementation should consider failure handling.
Example sequence:
1. Verify user owns video
2. Delete object from S3
3. Update database status/deleted_at
Alternatively:
1. Mark delete_pending
2. Delete S3 object
3. Mark deleted
The application must handle cases where:
DB succeeds but S3 fails
S3 succeeds but DB fails
object already does not exist
Deletion should be idempotent where practical.
28. No S3 Versioning
The bucket intentionally does not use S3 Versioning.
Therefore deletion should be considered permanent at the S3 storage level.
Application UX should reflect this.
For example:
Are you sure you want to permanently delete this video?
If product requirements later add recycle-bin functionality, that should be designed separately.
29. Large Video Uploads
For smaller initial workloads, presigned single PUT uploads may be sufficient.
For larger videos or unreliable connections, implement S3 Multipart Upload.
Recommended if videos routinely reach:
hundreds of MB
multiple GB
Benefits:
upload parts independently
retry failed parts
resume more effectively
parallel upload
better reliability
DevOps has configured cleanup for incomplete multipart uploads.
30. Multipart Upload Architecture
Flow:
Application
│
├── CreateMultipartUpload
│
▼
return uploadId
│
▼
Generate signed URL for part 1
Generate signed URL for part 2
Generate signed URL for part 3
...
│
Browser uploads directly to S3
│
Browser collects ETags
│
Backend calls CompleteMultipartUpload
The complete video still never passes through EC2.
31. Multipart Upload Data Example
Client might maintain:
{
"videoId": "video-e28c4a",
"uploadId": "abc123...",
"parts": [
{
"partNumber": 1,
"etag": "\"abc...\""
},
{
"partNumber": 2,
"etag": "\"def...\""
}
]
}
The client must not control the S3 object key.
32. Retry Handling
Upload failures must not automatically create duplicate video records.
Recommended:
video_id remains stable
retry upload
↓
same upload/video context
For single PUT uploads, application logic may generate a new signed URL for the same object key when appropriate.
For multipart upload, retry individual failed parts.
33. Handling Expired Presigned URLs
The frontend must gracefully handle URL expiration.
Example:
HTTP 403
SignatureDoesNotMatch
RequestExpired
Do not expose raw AWS messages directly to users.
UX:
Your upload session expired.
Please retry the upload.
The application may request a new signed URL if the video/upload record remains valid.
34. Authentication vs Authorization
These are separate.
Authentication answers:
Who are you?
Authorization answers:
Are you allowed to access this video?
Being logged in is not enough.
Every operation must check:
user belongs to tenant
video belongs to tenant
video belongs to user or user has required role
requested action is permitted
35. Future Organization-Level Permissions
The initial model may be:
User A can access User A videos only.
If future product requirements add:
Organization admin
Team manager
Shared videos
Organization library
then authorization should be expanded centrally.
Do not solve those scenarios by weakening S3 path validation.
36. Mandatory Cross-Tenant Security Tests
Assume:
Tenant A
User A
Video A
Tenant B
User B
Video B
Tests:
User A uploads Video A PASS
User A lists Video A PASS
User A downloads Video A PASS
User A deletes Video A PASS
User B lists Video B PASS
User B downloads Video B PASS
Security tests:
User A downloads Video B MUST FAIL
User A deletes Video B MUST FAIL
User A requests signed URL Video B MUST FAIL
User A modifies tenant ID to B MUST FAIL
User A modifies user ID to B MUST FAIL
User A supplies arbitrary S3 key MUST FAIL
User A guesses another video UUID MUST FAIL
These tests are mandatory.
37. API Test Cases
Upload
Test:
valid MP4
valid WebM
zero-byte file
unsupported type
oversized file
no login
invalid token
tenant mismatch
quota exceeded
expired presigned URL
network failure
duplicate completion
Download
Test:
valid video
missing video
deleted video
different tenant
different user
expired URL
invalid video ID
Delete
Test:
valid owner
already deleted
different tenant
different user
object missing in S3
database failure
S3 failure
38. Do Not Log Presigned URLs
Presigned URLs contain temporary authorization information.
Do not log the complete URL in:
application logs
debug logs
browser analytics
error trackers
APM traces
audit logs
Bad:
logger.info({
uploadUrl
});
Better:
logger.info({
event: "video_upload_url_generated",
videoId,
tenantId,
userId
});
39. Do Not Return S3 Internals Unnecessarily
Public API response should generally not expose:
S3 bucket name
S3 ARN
IAM ARN
AWS account number
internal S3 prefix
AWS credentials
The API may return the presigned URL when needed.
Internally the database may store:
s3_bucket
s3_key
40. Audit Logging
Application should record significant actions.
Recommended events:
VIDEO_UPLOAD_REQUESTED
VIDEO_UPLOAD_COMPLETED
VIDEO_UPLOAD_FAILED
VIDEO_DOWNLOAD_REQUESTED
VIDEO_DELETED
Recommended fields:
event
tenant_id
user_id
video_id
timestamp
request_id
source_ip
user_agent
Do not put:
AWS credentials
full presigned URL
sensitive authorization tokens
in audit logs.
41. Example Audit Event
{
"event": "VIDEO_DOWNLOAD_REQUESTED",
"tenantId": "tenant-f936d1",
"userId": "user-a8711c",
"videoId": "video-e28c4a",
"requestId": "req-78ab2",
"timestamp": "2026-09-25T11:50:00Z"
}
42. Error Handling
Do not expose AWS internals directly to the user.
Instead of:
AccessDenied:
User arn:aws:sts::123456789...
is not authorized to perform s3:GetObject
Return:
{
"error": "VIDEO_ACCESS_DENIED",
"message": "You do not have permission to access this video."
}
Internally log a sanitized error with the application request ID.
43. Recommended Application Error Codes
Examples:
VIDEO_NOT_FOUND
VIDEO_ACCESS_DENIED
VIDEO_INVALID_TYPE
VIDEO_TOO_LARGE
VIDEO_UPLOAD_FAILED
VIDEO_UPLOAD_EXPIRED
VIDEO_UPLOAD_INCOMPLETE
VIDEO_DOWNLOAD_FAILED
VIDEO_DELETE_FAILED
VIDEO_QUOTA_EXCEEDED
44. Security Rules — Mandatory
The following rules must be treated as implementation requirements.
Rule 1
Never place AWS credentials in application code.
Rule 2
Never place AWS credentials in .env.
Rule 3
Never expose AWS credentials to browser code.
Rule 4
Never accept tenant_id as authoritative input from the browser.
Rule 5
Never accept user_id as authoritative input from the browser.
Rule 6
Never accept arbitrary s3_key or prefix from the browser.
Rule 7
Never make the S3 bucket or videos public.
Rule 8
Never proxy full video uploads/downloads through EC2 without an explicitly approved requirement.
Rule 9
Always authorize the user before generating any presigned URL.
Rule 10
Always use short-lived presigned URLs.
Rule 11
Always verify tenant boundaries before download/delete.
Rule 12
Do not log presigned URLs.
45. Frontend Responsibilities
Frontend developers implement:
video picker
file type validation for UX
file size validation for UX
upload progress
upload retry UX
upload cancellation UX
video list UI
download action
delete confirmation
error states
Frontend validation is not a replacement for backend validation.
46. Backend Responsibilities
Backend developers implement:
authentication
tenant resolution
user resolution
authorization
S3 key generation
video ID generation
database metadata
file restrictions
quota enforcement
presigned upload URL
upload completion verification
presigned download URL
server-side delete
audit logging
cross-tenant security checks
47. DevOps Responsibilities
Application developers can assume DevOps handles:
S3 bucket
private bucket configuration
Block Public Access
Bucket Owner Enforced
S3 encryption
S3 CORS
multipart cleanup lifecycle
HTTPS-only bucket policy
EC2 IAM role
S3 IAM permissions
IAM role attachment to EC2
Developers should report IAM-related errors to DevOps rather than adding credentials or broadening permissions themselves.
48. What Developers Will Receive From DevOps
Expected information:
S3 bucket name
AWS region
application environment variable names
upload URL expiration
download URL expiration
maximum video size
supported environment/domain origins
No AWS access will be provided.
49. Local Development
Because developers do not have AWS credentials, local development should not depend on direct production S3 access.
Developers should design S3 functionality behind a storage-service abstraction.
Example:
interface VideoStorage {
createUploadUrl(...): Promise<string>;
createDownloadUrl(...): Promise<string>;
verifyObject(...): Promise<boolean>;
deleteObject(...): Promise<void>;
}
Production implementation:
S3VideoStorage
Testing/local implementation:
MockVideoStorage
This avoids requiring production AWS access on developer laptops.
50. Example Storage Service
export interface VideoStorageService {
createUploadUrl(params: {
tenantId: string;
userId: string;
videoId: string;
contentType: string;
}): Promise<{
objectKey: string;
uploadUrl: string;
}>;
createDownloadUrl(
objectKey: string
): Promise<string>;
objectExists(
objectKey: string
): Promise<boolean>;
deleteObject(
objectKey: string
): Promise<void>;
}
This also makes unit testing substantially easier.
51. Local Mock Example
Local development can use:
class MockVideoStorageService
implements VideoStorageService {
async createUploadUrl() {
return {
objectKey: "mock/video.mp4",
uploadUrl:
"http://localhost/mock-upload"
};
}
async createDownloadUrl() {
return "http://localhost/mock-video.mp4";
}
async objectExists() {
return true;
}
async deleteObject() {
return;
}
}
Production EC2 uses the real S3 adapter.
52. Deployment Validation
After deployment to the AWS environment, verify:
Application starts without AWS keys
AWS SDK receives EC2 role credentials
presigned PUT can be generated
browser can upload directly to S3
uploaded object appears under correct tenant path
application verifies upload
user can list video
download URL works
download URL expires
delete works
different tenant cannot access object
53. Definition of Done
The feature is considered complete only when all of these succeed:
[ ] User can upload a supported video
[ ] Video bytes go directly browser → S3
[ ] No video bytes pass through EC2
[ ] No AWS credentials exist in source code
[ ] No AWS credentials exist in application config
[ ] Upload URL expires
[ ] User can see uploaded video in application
[ ] Video metadata comes from application DB
[ ] User can download own video
[ ] Video download occurs S3 → browser
[ ] Download URL expires
[ ] User can delete own video
[ ] Deleted object disappears from S3
[ ] Tenant A cannot access Tenant B videos
[ ] User cannot control S3 key
[ ] User cannot control authoritative tenant ID
[ ] Unsupported file type is rejected
[ ] Oversized video is rejected
[ ] Upload completion is verified against S3
[ ] Audit events are generated
[ ] Presigned URLs are not logged
54. Final Reference Architecture
WIZBRAND VIDEO STORAGE
┌─────────────────────────────────────────────┐
│ USER │
│ │
│ Browser / Mobile App │
└─────────────────────┬───────────────────────┘
│
│ HTTPS API
▼
┌─────────────────────────────────────────────┐
│ WIZBRAND APPLICATION │
│ EC2 │
│ │
│ Authentication │
│ Tenant Authorization │
│ User Authorization │
│ Database │
│ S3 Key Generation │
│ Presigned URL Generation │
│ DeleteObject │
│ Upload Verification │
└─────────────────────┬───────────────────────┘
│
EC2 IAM ROLE
│
▼
┌─────────────────────────────────────────────┐
│ PRIVATE S3 │
│ │
│ videos.wizbrand.com │
│ │
│ tenants/ │
│ tenant-id/ │
│ users/ │
│ user-id/ │
│ videos/ │
│ video-id/ │
│ original.mp4 │
└─────────────────────────────────────────────┘
UPLOAD
Browser ───────────────────────────────► S3
Presigned PUT
DOWNLOAD
Browser ◄─────────────────────────────── S3
Presigned GET
DELETE
Browser ───► Application ──────────────► S3
DeleteObject
Core Principle
The application decides who may access a video.
AWS IAM decides what the application may do to S3.
S3 stores the video.
The browser transfers the video directly.
Neither developers nor application users need AWS credentials.
That separation must remain intact throughout the implementation.