Portfolio

Cheatsheet: Next.js (App Router) + Amplify Gen 2 for Protected S3 Media

15 min read
Next.js
AWS
Amplify
S3
Authentication

Cheatsheet: Next.js (App Router) + Amplify Gen 2 for Protected S3 Media

Goal: Display images, videos, or audio securely from S3. Only logged-in users can access content under protected/, and only the file owner can access content under private/{user_identity_id}/.
Assumptions: You have Node.js, npm/yarn, and the Amplify CLI installed and configured (amplify configure).

Steps:

Step 0: Project Initialization

Create Next.js App:
bash
bash

npx create-next-app@latest my-amplify-app --typescript --tailwind --eslint # Use App Router
cd my-amplify-app

Amplify Backend: (Run inside your project directory)
bash
bash

# Choose one: Sandbox (local dev) OR Deployed Env

npx amplify sandbox          # For rapid local development (auto-deploys on save)
# --- OR ---

npx amplify init            # To create a cloud environment (requires `amplify push`)

Step 1: Define Backend (Auth & Storage)

Add/Configure Auth:
If needed: npx amplify add auth (select Email login).
Ensure amplify/auth/resource.ts exists (minimal example):
typescript
typescript

// amplify/auth/resource.ts
import { defineAuth } from '@aws-amplify/backend';
export const auth = defineAuth({ loginWith: { email: true } });

/Configure Storage & Access Rules:
If needed: npx amplify add storage.
Edit amplify/storage/resource.ts to define rules (CRITICAL):
typescript
typescript

// amplify/storage/resource.ts
import { defineStorage } from '@aws-amplify/backend';
import { auth } from '../auth/resource'; // Import auth

export const storage = defineStorage({
  name: 'myProjectStorage', // Can be any name
  access: (allow) => ({
    // Logged-in users can READ anything under 'protected/'
    'protected/*': [
      allow.authenticated.to(['read']),
      // allow.authenticated.to(['read', 'write']), // Optional: Allow uploads too
    ],
    // File owner (based on Identity ID) can CRUD their files under 'private/{cognito_identity_id}/'
    'private/{entity_id}/*': [ // {entity_id} maps to Cognito Identity ID
      allow.entity('identity').to(['read', 'write', 'delete'])
    ],
  }),
  // Link Storage permissions to your Auth setup
  authorizationModes: { default: 'userPool' }, // Use Cognito User Pool auth
  userPool: auth.resources.userPool,           // Link the User Pool
  identityPool: auth.resources.identityPool, // Link the Identity Pool (REQUIRED for 'identity' access)
});

Backend:
amplify sandbox: Saves should trigger auto-deploy. Watch terminal.
amplify init environment: npx amplify push (or amplify publish).

Step 2: Install Frontend Libraries

bash
bash

npm install aws-amplify @aws-amplify/ui-react

Step 3: Configure Amplify in Next.js Root Layout (❗ MOST IMPORTANT STEP for Next.js)

Edit app/layout.tsx:
typescript
typescript

// app/layout.tsx
import type { Metadata } from "next";
import { Inter } from "next/font/google";
import "./globals.css";
import { Amplify } from 'aws-amplify';
// Adjust path if amplify_outputs.json is elsewhere (e.g. '@/amplify_outputs.json')
import outputs from '../amplify_outputs.json';
import ConfigureAmplifyClientSide from './ConfigureAmplifyClientSide'; // Our helper

// Configure Amplify Server-Side (runs first)
Amplify.configure(outputs, { ssr: true });

const inter = Inter({ subsets: ["latin"] });
export const metadata: Metadata = { title: "Amplify Media App" }; // Customize

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="en">
      <body className={inter.className}>
        {/* This component ensures Amplify is configured Client-Side */}
        <ConfigureAmplifyClientSide />
        {children}
      </body>
    </html>
  );
}

app/ConfigureAmplifyClientSide.tsx:
typescript
typescript

// app/ConfigureAmplifyClientSide.tsx
'use client'; // <-- MUST BE A CLIENT COMPONENT

import { Amplify } from 'aws-amplify';
import outputs from '../amplify_outputs.json'; // Adjust path if needed

// Configure Amplify Client-Side (runs after hydration)
Amplify.configure(outputs, { ssr: true });

export default function ConfigureAmplifyClientSide() { return null; }

amplify_outputs.json: Make sure this file exists (root usually) and contains storage info (bucket_name, aws_region) after deploying the backend. If not, run amplify pull.

Step 4: Add Authentication UI

Use the Authenticator (e.g., in app/page.tsx):
typescript
typescript

// app/page.tsx (Example Login/Dashboard Page)
'use client'; // Authenticator needs client-side interaction

import { Authenticator } from '@aws-amplify/ui-react';
import '@aws-amplify/ui-react/styles.css';
import Link from 'next/link';

function AppContent({ signOut, user }: { signOut?: () => void; user?: any }) {
  return (
    <div className="p-4">
      <h1>Welcome {user?.username}!</h1>
      <nav className="my-4 space-x-4">
        <Link href="/protected-media" className="text-blue-500 hover:underline">View Protected Media</Link>
        {/* <Link href="/my-media" className="text-blue-500 hover:underline">View My Media</Link> */}
        {/* <Link href="/upload" className="text-green-500 hover:underline">Upload</Link> */}
      </nav>
      <button onClick={signOut} className="bg-red-500 text-white p-2 rounded">Sign Out</button>
    </div>
  );
}

export default function HomePage() {
  return (
    <Authenticator>
      {({ signOut, user }) => (
        <AppContent signOut={signOut} user={user} />
      )}
    </Authenticator>
  );
}

Step 5: Create Reusable Media Display Component

Create app/components/ProtectedMediaDisplay.tsx: (Code from previous answer - includes image, video, audio handling)
Make sure to copy the full code for ProtectedMediaDisplay including getMediaType helper.
Key parts: Takes path prop, uses getUrl, determines type, renders <img>/<video>/<audio>.

Step 6: Use the Display Component on a Page

Create a page (e.g., app/protected-media/page.tsx):
typescript
typescript

// app/protected-media/page.tsx
import ProtectedMediaDisplay from '../components/ProtectedMediaDisplay'; // Adjust path
import Link from 'next/link';

// This page CAN be a Server Component, but ProtectedMediaDisplay MUST be 'use client'
export default function ProtectedMediaPage() {
  // TODO: Upload these files to the 'protected/' prefix in your S3 bucket
  const imageFile = "protected/gs.jpg";
  const videoFile = "protected/002.mp4";
  const audioFile = "protected/Aws-Amplify-Al-tool-kit.wav"; // Use the EXACT name from S3

  return (
    <div className="container mx-auto p-4">
      <h1 className="text-2xl font-bold mb-6">Protected Media</h1>
      <div className="space-y-6">
        <div>
          <h2 className="text-lg font-semibold">Image:</h2>
          <ProtectedMediaDisplay path={imageFile} alt="Protected JPG" />
        </div>
        <div>
          <h2 className="text-lg font-semibold">Video:</h2>
          <ProtectedMediaDisplay path={videoFile} alt="Protected MP4" />
        </div>
        <div>
          <h2 className="text-lg font-semibold">Audio:</h2>
          <ProtectedMediaDisplay path={audioFile} alt="Protected WAV" />
        </div>
        {/* To display PRIVATE files, you'd need to get identityId first */}
        {/* See Step 5 in the detailed tutorial for Private files */}
      </div>
      <Link href="/" className="text-blue-500 hover:underline mt-8 block">Back</Link>
    </div>
  );
}

Step 7: Testing & Verification

Upload Files: Manually upload your test files (gs.jpg, 002.mp4, Aws-Amplify-Al-tool-kit.wav) to the protected/ prefix in your S3 bucket via the AWS Console. MATCH FILENAMES EXACTLY (CASE-SENSITIVE)!
Run App: npm run dev
Login: Use the Authenticator UI.
Navigate: Go to your /protected-media page.
Verify: The image, video player, and audio player should load and display correctly.

🔧 Troubleshooting:

NoBucket Error: Check Step 3 (Root Layout Config). ConfigureAmplifyClientSide is likely missing or amplify_outputs.json path is wrong.
404 NotFound Error: The file path in your component (path="...") does NOT exactly match the object key in S3 (inside the protected/ folder). Check S3 Console for typos, case sensitivity, or missing files.
403 Forbidden Error:
User not logged in?
Access rules in amplify/storage/resource.ts incorrect or not deployed?
Trying to access private/ without correct identityId logic?
General Issues: Run amplify pull to sync amplify_outputs.json. Check browser console for detailed errors.