How to Design a video App using AWS s3 Storage multi Tenant

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.