<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Nwosa Emeka Afamefuna</title>
    <description>The latest articles on DEV Community by Nwosa Emeka Afamefuna (@nwosaemeka).</description>
    <link>https://dev.to/nwosaemeka</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F437634%2F4d18055f-a5d6-46fe-b8d0-28469fbb7f4b.jpeg</url>
      <title>DEV Community: Nwosa Emeka Afamefuna</title>
      <link>https://dev.to/nwosaemeka</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/nwosaemeka"/>
    <language>en</language>
    <item>
      <title>Deploying a Next.js SSR App to AWS Amplify (the stuff nobody tells you)</title>
      <dc:creator>Nwosa Emeka Afamefuna</dc:creator>
      <pubDate>Sun, 14 Jun 2026 14:07:25 +0000</pubDate>
      <link>https://dev.to/nwosaemeka/deploying-a-nextjs-ssr-app-to-aws-amplify-the-stuff-nobody-tells-you-4gei</link>
      <guid>https://dev.to/nwosaemeka/deploying-a-nextjs-ssr-app-to-aws-amplify-the-stuff-nobody-tells-you-4gei</guid>
      <description>&lt;p&gt;"Connect your repo and click Deploy."&lt;/p&gt;

&lt;p&gt;That's the entire AWS Amplify pitch, and it's a lie of omission. The five-minute demo works great right up until you bring a real app. Server-side rendering, a package manager that isn't npm, a Node version that's slightly too new, environment variables that quietly disappear at runtime. Then you watch a build go green on your laptop and red in the cloud, and the error message links you to a troubleshooting doc that 404s.&lt;/p&gt;

&lt;p&gt;We shipped a production Next.js 16 SSR app to Amplify across development, staging, and production. Between the demo and a working deploy, we hit most of the edge cases Amplify has to offer: a Node version it flatly refused to run, SSR environment variables that vanished at runtime, and a one-line pnpm error that took down a production deploy.&lt;/p&gt;

&lt;p&gt;None of it is in the tutorial. So here's the whole map, happy path and every landmine, written as the post I wish I'd had open in the other tab.&lt;/p&gt;

&lt;h2&gt;
  
  
  What we're actually shipping
&lt;/h2&gt;

&lt;p&gt;Quick cast of characters, because every problem below traces back to one of them:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Next.js 16&lt;/strong&gt; (App Router) and &lt;strong&gt;React 19&lt;/strong&gt;, running in &lt;strong&gt;SSR mode&lt;/strong&gt;, not a static export. That one distinction matters more than you'd expect.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;pnpm&lt;/strong&gt; for package management. Fast, strict, and the source of two separate landmines.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;next-intl&lt;/strong&gt; for localized routes like &lt;code&gt;/en/...&lt;/code&gt; and &lt;code&gt;/fr/...&lt;/code&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Sentry&lt;/strong&gt; for error tracking and readable production stack traces.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Three environments&lt;/strong&gt; (dev, staging, production), each wired to its own git branch.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  First, the fork in the road: SSR or static
&lt;/h2&gt;

&lt;p&gt;Before anything else, get honest about which Next.js you're running. We're on SSR, not a static export. Amplify hosts both, but they behave like completely different products. Static is just files sitting on a CDN. SSR provisions real server-side compute that runs your app on every request.&lt;/p&gt;

&lt;p&gt;Almost everything that bit us traces back to that fact. If you're doing a static export, you can close the tab after the next section. If you're on SSR, stick around, because the environment variable handling alone will save you an afternoon.&lt;/p&gt;

&lt;h2&gt;
  
  
  The one file that runs everything: &lt;code&gt;amplify.yml&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;Drop an &lt;code&gt;amplify.yml&lt;/code&gt; at your repo root and it becomes the brain of every build. Here's ours, trimmed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;
&lt;span class="na"&gt;frontend&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;phases&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;preBuild&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;commands&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;npm install -g pnpm&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;pnpm install&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;pnpm run lint&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;pnpm run typecheck&lt;/span&gt;
    &lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;commands&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;env | grep -e NEXT_PUBLIC_ &amp;gt;&amp;gt; .env.production&lt;/span&gt;
        &lt;span class="c1"&gt;# ...more env piping, see below&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;pnpm run build&lt;/span&gt;
  &lt;span class="na"&gt;artifacts&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;baseDirectory&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;.next&lt;/span&gt;
    &lt;span class="na"&gt;files&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;**/*'&lt;/span&gt;
  &lt;span class="na"&gt;cache&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;paths&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;.next/cache/**/*&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;node_modules/**/*&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three deliberate choices are hiding in there:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Lint and typecheck run as build gates.&lt;/strong&gt; Both happen in &lt;code&gt;preBuild&lt;/code&gt;, before the build itself. A type error or a lint failure kills the deploy instead of shipping broken code. It's cheap insurance and it pays out constantly.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;We cache&lt;/strong&gt; &lt;code&gt;.next/cache&lt;/code&gt; &lt;strong&gt;and&lt;/strong&gt; &lt;code&gt;node_modules&lt;/code&gt; to shave real minutes off every build after the first.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;baseDirectory: .next&lt;/code&gt; because that's where Next.js drops its output.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Now the landmines.&lt;/p&gt;

&lt;h2&gt;
  
  
  Gotcha #1: the Node version nvm can't save you from
&lt;/h2&gt;

&lt;p&gt;Our repo targets Node 24 (&lt;code&gt;.nvmrc&lt;/code&gt; says &lt;code&gt;v24&lt;/code&gt;). Amplify, as of today, only supports Node 16, 18, 20, and 22. Push a build expecting 24 and you get this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;CustomerError: Unsupported NodeJS version: v24.6.0.
Supported versions are 16, 18, 20 and 22.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The obvious move is to install Node 24 with nvm in &lt;code&gt;preBuild&lt;/code&gt;. It doesn't work. Amplify validates the Node version itself and rejects it no matter what nvm pulls down. I lost the better part of an afternoon to this before I accepted it.&lt;/p&gt;

&lt;p&gt;The real fix isn't in your code at all. It's a checkbox in the console:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Amplify Console → Hosting → Build settings → Build image settings → Edit → Live package updates → Node.js → set to 22.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Pin Node 22 there and the app runs fine. This is pure platform knowledge, the kind nothing in your repo will ever hint at, so we left a fat comment block in &lt;code&gt;amplify.yml&lt;/code&gt; pointing the next person straight at it. If you want to follow the upstream thread, it's &lt;code&gt;aws-amplify/amplify-hosting#4073&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Gotcha #2: the env vars that show up to build and ghost at runtime
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;⚠️ &lt;strong&gt;This is the one that cost us the most time:&lt;/strong&gt; close to four hours, because the build kept succeeding and the app failed anyway.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;On a static build, your &lt;code&gt;NEXT_PUBLIC_*&lt;/code&gt; vars get inlined at build time and that's the end of it. On SSR, the server runtime needs them too, and Amplify's build-time environment variables don't automatically flow into the running Next.js server. You set them in the console, the build reads them fine, and then your live app reaches for them and finds nothing.&lt;/p&gt;

&lt;p&gt;The workaround is a little ugly and completely reliable. In the &lt;code&gt;build&lt;/code&gt; phase, grep the vars you need out of the environment and append them to the &lt;code&gt;.env&lt;/code&gt; files Next bakes into the build:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;commands&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;env | grep -e NEXT_PUBLIC_ &amp;gt;&amp;gt; .env.production&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;env | grep -e SENTRY_AUTH_TOKEN &amp;gt;&amp;gt; .env.production&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;env | grep -e INTERNAL_API_SECRET &amp;gt;&amp;gt; .env.production&lt;/span&gt;
    &lt;span class="c1"&gt;# ...same pattern for each prefix you need, repeated for .env.dev / .env.staging&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;pnpm run build&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In plain terms: pull everything matching those prefixes out of the live build environment, write it into the env files for each target, then build. Now both the client bundle and the SSR server have what they need.&lt;/p&gt;

&lt;p&gt;Two things worth keeping straight:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;List each prefix on purpose. &lt;code&gt;NEXT_PUBLIC_*&lt;/code&gt; is safe to ship to the browser. Secrets like &lt;code&gt;SENTRY_AUTH_TOKEN&lt;/code&gt; and your internal API secret are server-side only, so never give one a &lt;code&gt;NEXT_PUBLIC_&lt;/code&gt; prefix.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;AWS documents this under &lt;a href="https://docs.aws.amazon.com/amplify/latest/userguide/ssr-environment-variables.html" rel="noopener noreferrer"&gt;SSR environment variables&lt;/a&gt;. Worth reading, because "present at build, missing at runtime" is a genuinely baffling failure until you've seen it once.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Make missing config fail loud, not late
&lt;/h3&gt;

&lt;p&gt;To dodge the "deployed fine, crashed in prod because one var was blank" trap, we validate the whole environment at build time with &lt;code&gt;@t3-oss/env-nextjs&lt;/code&gt; and Zod:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;env&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;createEnv&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;server&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;SENTRY_AUTH_TOKEN&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="na"&gt;WIDGET_SCRIPT&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="na"&gt;INTERNAL_API_SECRET&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="c1"&gt;// ...&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;client&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;NEXT_PUBLIC_API_END_POINT&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;url&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="na"&gt;NEXT_PUBLIC_APP_ENV&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;union&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="cm"&gt;/* production | staging | development | localhost */&lt;/span&gt;&lt;span class="p"&gt;]),&lt;/span&gt;
    &lt;span class="c1"&gt;// ...&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A missing or malformed var now fails the build with a clear message instead of blowing up in production. Put that on top of the lint and typecheck gates and broken config simply can't reach a live URL.&lt;/p&gt;

&lt;h3&gt;
  
  
  One default, surgical overrides
&lt;/h3&gt;

&lt;p&gt;In the Amplify console (Hosting → Environment Variables → Manage Variables) you set vars per environment. We use "All branches" as the default, which doubles as production, then add per-branch overrides for develop and staging only where they actually differ. One source of truth, with a scalpel for the exceptions.&lt;/p&gt;

&lt;h2&gt;
  
  
  Replacing Amplify's auto-builds with GitHub Actions
&lt;/h2&gt;

&lt;p&gt;The first two were platform landmines. This one is a deliberate choice, and it's worth explaining because it shaped everything downstream.&lt;/p&gt;

&lt;p&gt;Amplify can auto-build on every push. We turned that off. Instead, GitHub Actions triggers each deploy explicitly through the AWS CLI. That buys us three things: control over when a branch ships, real pass/fail exit codes in CI, and somewhere to bolt on automation.&lt;/p&gt;

&lt;p&gt;Every environment gets its own workflow (&lt;code&gt;.github/workflows/deploy-{dev,staging,main}.yml&lt;/code&gt;). The spine of it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;push&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;branches&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;main&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;   &lt;span class="c1"&gt;# or develop / staging&lt;/span&gt;
&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;deploy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;aws-actions/configure-aws-credentials@v6&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;aws-access-key-id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.AWS_ACCESS_KEY_ID }}&lt;/span&gt;
          &lt;span class="na"&gt;aws-secret-access-key&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.AWS_SECRET_ACCESS_KEY }}&lt;/span&gt;
          &lt;span class="na"&gt;aws-region&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.AWS_BUCKET_REGION }}&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v5&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Deploy&lt;/span&gt;
        &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;APP_ID&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.AWS_APP_ID }}&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;./scripts/amplify-deploy.sh $APP_ID main&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And the script it calls. This is the part that makes CI actually wait for the deploy and report the truth instead of firing and forgetting:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;JOB_ID&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;aws amplify start-job &lt;span class="nt"&gt;--app-id&lt;/span&gt; &lt;span class="nv"&gt;$1&lt;/span&gt; &lt;span class="nt"&gt;--branch-name&lt;/span&gt; &lt;span class="nv"&gt;$2&lt;/span&gt; &lt;span class="nt"&gt;--job-type&lt;/span&gt; RELEASE &lt;span class="se"&gt;\&lt;/span&gt;
  | jq &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="s1"&gt;'.jobSummary.jobId'&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;

&lt;span class="c"&gt;# poll until the job leaves PENDING/RUNNING&lt;/span&gt;
&lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="o"&gt;[[&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;aws amplify get-job &lt;span class="nt"&gt;--app-id&lt;/span&gt; &lt;span class="nv"&gt;$1&lt;/span&gt; &lt;span class="nt"&gt;--branch-name&lt;/span&gt; &lt;span class="nv"&gt;$2&lt;/span&gt; &lt;span class="nt"&gt;--job-id&lt;/span&gt; &lt;span class="nv"&gt;$JOB_ID&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  | jq &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="s1"&gt;'.job.summary.status'&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;~ ^&lt;span class="o"&gt;(&lt;/span&gt;PENDING|RUNNING&lt;span class="o"&gt;)&lt;/span&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="o"&gt;]]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do &lt;/span&gt;&lt;span class="nb"&gt;sleep &lt;/span&gt;1&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;done

&lt;/span&gt;&lt;span class="nv"&gt;JOB_STATUS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;aws amplify get-job &lt;span class="nt"&gt;--app-id&lt;/span&gt; &lt;span class="nv"&gt;$1&lt;/span&gt; &lt;span class="nt"&gt;--branch-name&lt;/span&gt; &lt;span class="nv"&gt;$2&lt;/span&gt; &lt;span class="nt"&gt;--job-id&lt;/span&gt; &lt;span class="nv"&gt;$JOB_ID&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  | jq &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="s1"&gt;'.job.summary.status'&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$JOB_STATUS&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="s2"&gt;"SUCCEED"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then &lt;/span&gt;&lt;span class="nb"&gt;exit &lt;/span&gt;1&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;fi&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Why bother instead of flipping the auto-build toggle?&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Real exit codes.&lt;/strong&gt; The script polls &lt;code&gt;get-job&lt;/code&gt; until the release leaves &lt;code&gt;PENDING&lt;/code&gt; or &lt;code&gt;RUNNING&lt;/code&gt;, then exits non-zero unless the final status is &lt;code&gt;SUCCEED&lt;/code&gt;. A failed deploy shows up as a red X in GitHub instead of a surprise you find later.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;You own the trigger.&lt;/strong&gt; Deploys fire on pushes to specific branches, on your terms.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Somewhere to hang automation,&lt;/strong&gt; which is exactly where the next two pieces plug in.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Gotcha #3: the one-line pnpm error that killed a production deploy
&lt;/h2&gt;

&lt;p&gt;This one actually took prod down, and the error buries the lede. Mid-build, the logs coughed up:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[ERR_PNPM_IGNORED_BUILDS] Ignored build scripts: @parcel/watcher@2.5.6,
@sentry/cli@2.58.4, @swc/core@1.15.11, esbuild@0.27.3, sharp@0.34.5, unrs-resolver@1.11.1
Run "pnpm approve-builds" to pick which dependencies should be allowed to run scripts.
...
!!! Build failed
!!! Error: Command failed with exit code 1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here's the trap. As of pnpm 10, and still in 11, pnpm no longer runs the &lt;code&gt;postinstall&lt;/code&gt; and build scripts of your dependencies by default. It's a supply-chain hardening move. Packages with native build steps (&lt;code&gt;sharp&lt;/code&gt;, &lt;code&gt;esbuild&lt;/code&gt;, &lt;code&gt;@swc/core&lt;/code&gt;, &lt;code&gt;@sentry/cli&lt;/code&gt;, &lt;code&gt;@parcel/watcher&lt;/code&gt;, &lt;code&gt;unrs-resolver&lt;/code&gt;) get installed but never built. On your laptop you don't notice, because you approved them interactively months ago. In a clean CI box like Amplify, the build face-plants.&lt;/p&gt;

&lt;p&gt;The interactive fix, &lt;code&gt;pnpm approve-builds&lt;/code&gt;, is useless in CI because there's no TTY to answer the prompt. The durable fix is to commit the approval to the repo by declaring the allowed builds in &lt;code&gt;pnpm-workspace.yaml&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;allowBuilds&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;@parcel/watcher'&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
  &lt;span class="s"&gt;'@sentry/cli'&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
  &lt;span class="s"&gt;'@swc/core'&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
  &lt;span class="s"&gt;esbuild&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
  &lt;span class="s"&gt;sharp&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
  &lt;span class="s"&gt;unrs-resolver&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now pnpm runs exactly those build scripts everywhere, laptops and CI alike, with no prompt. The &lt;a href="https://pnpm.io/migration" rel="noopener noreferrer"&gt;pnpm migration guide&lt;/a&gt; covers the wider set of behavior changes if you're jumping a major version, but this is the one most likely to surface as a green-locally, red-in-CI deploy.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Heads up:&lt;/strong&gt; the field name has drifted across pnpm versions (&lt;code&gt;onlyBuiltDependencies&lt;/code&gt; in some, &lt;code&gt;allowBuilds&lt;/code&gt; in others). Check what your exact version expects. Paste the wrong key and it quietly does nothing, and you're back to the same failed build.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Bonus: readable stack traces instead of minified soup
&lt;/h2&gt;

&lt;p&gt;Our &lt;code&gt;next.config.ts&lt;/code&gt; wraps the config in &lt;code&gt;withSentryConfig&lt;/code&gt;. During the Amplify build, source maps upload to Sentry using &lt;code&gt;SENTRY_AUTH_TOKEN&lt;/code&gt;, and only when &lt;code&gt;CI&lt;/code&gt; is set, so local builds stay quiet:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="nf"&gt;withSentryConfig&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;nextWithIntl&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;org&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;your-org&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;project&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;your-project&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;authToken&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;SENTRY_AUTH_TOKEN&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;silent&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;CI&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;widenClientFileUpload&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The payoff is production stack traces that point at real source lines instead of minified gibberish. It's also why &lt;code&gt;SENTRY_AUTH_TOKEN&lt;/code&gt; is one of the vars we pipe into the &lt;code&gt;.env&lt;/code&gt; files back in Gotcha #2. The whole thing is connected.&lt;/p&gt;

&lt;h2&gt;
  
  
  The cheat sheet
&lt;/h2&gt;

&lt;p&gt;Putting a Next.js SSR app on Amplify? Tape this above your monitor:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Know whether you're on SSR or static.&lt;/strong&gt; It explains everything else.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Pin Node 22 via Live Package Updates&lt;/strong&gt; in the console, not the build spec. Amplify won't run 24, and nvm can't force it.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Pipe your env vars into&lt;/strong&gt; &lt;code&gt;.env.*&lt;/code&gt; &lt;strong&gt;files in the build phase.&lt;/strong&gt; The SSR runtime won't get them otherwise.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Validate env vars at build&lt;/strong&gt; with Zod, so missing config fails loud instead of in prod.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Declare&lt;/strong&gt; &lt;code&gt;allowBuilds&lt;/code&gt; &lt;strong&gt;in&lt;/strong&gt; &lt;code&gt;pnpm-workspace.yaml&lt;/code&gt;&lt;strong&gt;.&lt;/strong&gt; pnpm 10+ won't run dependency build scripts in CI, and &lt;code&gt;pnpm approve-builds&lt;/code&gt; can't help a non-interactive box.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Gate the build on lint and typecheck&lt;/strong&gt; so broken code can't ship.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Drive deploys from GitHub Actions and the AWS CLI&lt;/strong&gt;, polling &lt;code&gt;get-job&lt;/code&gt; for an honest pass/fail.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Amplify is a perfectly capable home for a Next.js SSR app. The catch is that the official onboarding stops at the happy path, and real workloads live well past it: Node versions it won't run, an SSR runtime that wants its own env vars, a package manager with opinions, and deploy orchestration you have to build yourself. We already paid for those debugging sessions.&lt;/p&gt;

&lt;p&gt;Hopefully now they cost you nothing.&lt;/p&gt;

</description>
      <category>amplify</category>
      <category>nextjs</category>
      <category>aws</category>
    </item>
  </channel>
</rss>
