Why Next.js SSR Apps Break on AWS Amplify (and How to Fix Runtime Env Vars)

When migrating a Next.js App Router + SSR app from Vercel to AWS Amplify, everything can look correct — builds succeed, environment variables are set, IAM permissions are configured — yet the app still crashes at runtime with 500 errors and “missing env vars”.

I hit exactly this problem while migrating a production app using Cognito + API Gateway + Lambda + RDS.

This post explains why it happens and the reliable fix.


The Root Cause: Build-time vs Runtime Env Vars on Amplify

On AWS Amplify Web Compute (SSR), environment variables behave differently than on Vercel.

What works on Vercel

  • Env vars are available:

    • at build time
    • at runtime (SSR, route handlers, server components)
  • process.env.MY_VAR “just works” everywhere

What happens on Amplify

  • Amplify does inject env vars at build time

  • But server-side runtime (SSR compute) does NOT automatically receive them

  • Result:

    • Build succeeds
    • SSR crashes at runtime
    • process.env.MY_VAR === undefined in route handlers

This is by design, not a bug.

AWS documents this behavior, but it’s easy to miss if you’re coming from Vercel.


Common Symptoms

You’ll usually see:

  • 500 errors on all pages

  • Logs showing:

    • all env vars missing at once
    • or CredentialsProviderError when accessing AWS SDK
  • Works locally and on Vercel, fails only on Amplify


The Correct Fix (What Actually Works)

1. Move secrets to AWS Secrets Manager

For sensitive values (e.g. Cognito client secret):

  • Store them in Secrets Manager

  • Give the Amplify Compute role permission:

    secretsmanager:GetSecretValue
    

This avoids relying on runtime env injection for secrets.


2. Bake runtime env vars into the build

For non-secret runtime values (API URLs, redirect URIs), Amplify’s recommended pattern is:

During the build, write env vars into a .env.production file so Next.js can load them at runtime.

Example in amplify.yml:

build:
  commands:
    - env | grep '^COGNITO_' >> .env.production
    - env | grep '^REPORTS_API_BASE_URL' >> .env.production
    - pnpm build

This bridges the gap between:

  • Amplify’s build environment
  • Next.js SSR runtime expectations

3. Fix server-side config code (important)

After moving secrets to Secrets Manager:

  • Do not require NEXT_PUBLIC_* vars in server code
  • Load secrets from Secrets Manager
  • Load non-secret runtime values from server env (baked via .env.production)

Bad pattern (breaks on Amplify):

process.env.NEXT_PUBLIC_SOMETHING

Correct pattern:

process.env.SOMETHING

Keep NEXT_PUBLIC_* strictly for browser/client code.


Why This Only Breaks on Amplify

  • Vercel injects env vars into SSR runtime automatically

  • Amplify separates:

    • build environment
    • compute runtime
  • Next.js assumes env vars are present at runtime

  • Amplify requires you to explicitly bridge that gap

Once you know this, the fix is straightforward.


TL;DR

If your Next.js SSR app crashes on Amplify:

  1. Secrets → AWS Secrets Manager
  2. Runtime env vars → write to .env.production during build
  3. Server code must not depend on NEXT_PUBLIC_*
  4. Assign a Compute role (not just logging role)

After that, Amplify SSR works reliably.


A Note on IAM Roles (Easy to Miss)

AWS Amplify uses two different IAM roles, and confusing them will break SSR apps silently:

  • Service role: used by Amplify for builds and logging (not for your app code)
  • Compute role: assumed by the SSR runtime when your Next.js server code is executing

To access AWS services at runtime (e.g. Secrets Manager), you must assign a Compute role to the app. Without it, the SSR runtime has no AWS credentials, leading to errors like CredentialsProviderError.

The Compute role should have only the permissions your server code needs (for example, secretsmanager:GetSecretValue). This role is separate from the logging/service role and must be explicitly configured in the Amplify console.