How to Deploy to AWS Amplify From the CLI
Deploy a React Router 7 app to AWS Amplify with only the AWS CLI: create-app, update-branch, start-job, and the custom-domain CNAMEs, plus the exact errors.

The AWS CLI commands that take a React Router 7 app from local folder to a live custom domain, no console clicking required, plus the exact errors you'll hit doing it.
I already wrote about deploying React Router 7 to Amplify with full SSR. That post is the console walkthrough: click Create app, click Host web app, pick your repo. It works, and if you're deploying once, do that.
I stopped doing it that way once I had more than one site to stand up. This is the CLI version: the one I actually run now, end to end, from an empty GitHub repo to a working https://yoursubdomain.yourdomain.com. It's also the version that scripts, which the console workflow doesn't.
What you need first
- AWS CLI v2, authenticated.
aws sts get-caller-identityshould return your account. - A GitHub personal access token with access to the repo, exported as
$GH_TOKEN. - The app already pushed to that repo and building locally with
npm run build. - For a custom domain, a Route 53 hosted zone you control and its zone id.
Why your Amplify app needs --platform WEB_COMPUTE
aws amplify create-app \
--name my-app \
--repository "https://github.com/you/my-app" \
--access-token "$GH_TOKEN" \
--platform WEB_COMPUTE \
--enable-branch-auto-build--platform WEB_COMPUTE is the one flag in this command you can't get wrong. Skip it and Amplify creates a static-site (SSG) app, so your React Router SSR Lambda never gets provisioned. Every loader that hits a database or reads a request header quietly stops running. The build succeeds, the site loads, and your dynamic routes just serve stale or broken output with nothing pointing at the cause.
Don't pass --custom-rules alongside WEB_COMPUTE either. Those are SPA-fallback rewrite rules for static hosting, and on a compute app they fight with the SSR routing Amplify already generates from your build manifest. The regex also doesn't survive shell escaping cleanly, which is a second, dumber way to lose an afternoon to the same flag.
Why does Amplify say the branch main already exists?
Amplify auto-creates the default branch (usually main) the moment create-app runs. If your script's next step is:
aws amplify create-branch --app-id "$APP_ID" --branch-name mainyou get:
BadRequestException: The branch main already exists
Use update-branch instead. It's also where you set env vars and the production stage:
aws amplify update-branch --app-id "$APP_ID" --branch-name main \
--framework "React" \
--stage PRODUCTION \
--enable-auto-build \
--environment-variables KEY1=val1,KEY2=val2One gotcha inside that gotcha: update-branch --environment-variables replaces the full set, not merges. I checked this against a real branch rather than assume it: setting one new variable without the existing ones wiped all of them. If you're adding one variable to a branch that already has three, read the existing ones first (aws amplify get-branch --app-id "$APP_ID" --branch-name main --query "branch.environmentVariables") and pass all four back, or the other three silently disappear.
How do I trigger the first build?
create-app and update-branch only configure the app. Neither builds it, so a
script that stops there leaves you with an app that has never deployed. Start
the first build explicitly and keep the job id:
JOB_ID=$(aws amplify start-job --app-id "$APP_ID" --branch-name main \
--job-type RELEASE --query 'jobSummary.jobId' --output text)Then poll it. SUCCEED, FAILED, and CANCELLED are the terminal states:
aws amplify get-job --app-id "$APP_ID" --branch-name main \
--job-id "$JOB_ID" --query 'job.summary.status' --output textHow do I attach a custom domain from the CLI?
aws amplify create-domain-association --app-id "$APP_ID" --domain-name example.com \
--sub-domain-settings "prefix=myapp,branchName=main"This returns immediately with CREATING, then moves to PENDING_VERIFICATION. At that point get-domain-association gives you two DNS records to install: a certificate-validation CNAME and a traffic CNAME. This is where a scripted deploy earns its keep. You can pull both records and write them straight into Route 53 without touching a UI:
aws route53 change-resource-record-sets \
--hosted-zone-id "$HOSTED_ZONE_ID" \
--change-batch file://dns-records.jsonThe trap: it's easy to install only the traffic CNAME and miss the certificate-validation one, because the traffic record looks like the one that matters (it's the domain you're trying to make work) and the validation record looks like AWS noise. Without both, the association just sits at PENDING_VERIFICATION indefinitely, no error, no timeout. Check both landed:
aws route53 list-resource-record-sets --hosted-zone-id "$HOSTED_ZONE_ID" \
--query "ResourceRecordSets[?contains(Name,'myapp')||contains(Name,'acm-validations')]"If that query returns one record instead of two, that's the whole bug.
Amplify CLI errors and what they mean
ERR_MODULE_NOT_FOUND at Lambda startup isn't a CLI issue, and it isn't a missing config option either. vite-plugin-react-router-amplify-hosting@0.4.0 copies the SSR entry (build/server/server.mjs) into the compute bundle but not the server's split chunks (build/server/assets/*.js). Any route whose loader does a dynamic await import(...) compiles to its own server chunk, so at runtime the Lambda's import("./assets/<chunk>.js") fails and that route alone returns HTTP 500. The fix is a postbuild step that copies build/server/assets/* into .amplify-hosting/compute/default/assets/, wired into npm run build so it runs locally and on Amplify alike.
Cannot find module '@rollup/rollup-linux-x64-gnu' is npm's optional-dependency resolution skipping the Linux binary when your lockfile was generated on macOS or Windows. Fix it in the build spec, not locally: add npm install @rollup/rollup-linux-x64-gnu --save-optional --legacy-peer-deps as an explicit preBuild step in amplify.yml, after wiping node_modules and package-lock.json.
If the domain's stuck at PENDING_VERIFICATION past 30 minutes, it's almost always the missing-CNAME issue above. Confirm both records with the list-resource-record-sets query before assuming it's an AWS-side delay.
I've also seen vite-plugin-react-router-amplify-hosting@0.7 builds fail where 0.4 succeeds, on a handful of repos, without a clear explanation why. If you hit an inexplicable build failure on the current version and the diff makes no sense, npm install --legacy-peer-deps --save-dev vite-plugin-react-router-amplify-hosting@^0.4.0 is a real, if unsatisfying, fallback.
Versions this held true for
Node 20+, AWS CLI v2, React Router 7.x. This site itself runs on vite-plugin-react-router-amplify-hosting@^0.4.0, the fallback mentioned above, not the newer 0.7.x line. Amplify's CLI surface moves slower than most AWS services, but if a flag here doesn't match what you're seeing, check aws amplify create-app help against your installed CLI version first.