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.