Guide · AWS Amplify · Hosting
AWS Amplify Hosting for MCP Server Web Frontends — CI/CD, Branch Builds, and Custom Domains
AWS Amplify Hosting is a fully managed static site and SSR host that connects directly to your Git repository — push a commit and Amplify builds, tests, and deploys your MCP server's dashboard or configuration UI automatically. For MCP server teams, Amplify Hosting solves the "I just want to deploy a React/Next.js admin panel" problem without running EC2 or configuring CloudFront manually. Amplify provisions a CloudFront distribution, S3 origin (for static), and Lambda@Edge functions (for SSR) automatically. Build settings live in amplify.yml at the repository root — you control the build command, base directory, artifact output directory, and cache paths. Branch-level configuration lets main deploy to production and develop deploy to a staging environment with different environment variables. The critical Amplify Hosting gotchas: the build container is Amazon Linux 2 (not your local environment), the Node.js version defaults to 16 unless you override it in amplify.yml, and SSR (Next.js) requires the App Router or Pages Router to be at the root of the output directory — not a sub-path.
TL;DR
Connect your Git repo to an Amplify App, add an amplify.yml with the correct baseDirectory pointing to your build output, set nvm use 20 in the pre-build phase to override the default Node.js 16, configure branch environment variables for API endpoints, and map a custom domain with a CNAME or ALIAS record. For monorepos, set appRoot to the subdirectory. For SSR (Next.js), set the platform to WEB_COMPUTE — not WEB (which only serves static files). PR preview environments are on by default for connected branches — they create a temporary URL at https://<pr-id>.<branch>.<appId>.amplifyapp.com and are deleted when the PR closes.
Creating an Amplify App and connecting a repository
An Amplify App is the top-level resource that maps to a single Git repository. You create it once, then add branches as sub-resources. Amplify supports GitHub, GitLab, Bitbucket, and AWS CodeCommit. For GitHub, Amplify uses the Amplify GitHub App (not OAuth tokens) — install the GitHub App on your organization and grant access to the target repositories.
# AWS CLI — create an Amplify app connected to GitHub
aws amplify create-app \
--name mcp-dashboard \
--repository https://github.com/your-org/mcp-dashboard \
--access-token $GITHUB_PAT \
--platform WEB \
--environment-variables \
REACT_APP_API_URL=https://api.alivemcp.com,NODE_ENV=production
# Add a branch (main) with auto-build enabled
aws amplify create-branch \
--app-id $APP_ID \
--branch-name main \
--stage PRODUCTION \
--enable-auto-build \
--environment-variables \
REACT_APP_ENV=production
# Trigger an initial build
aws amplify start-job \
--app-id $APP_ID \
--branch-name main \
--job-type RELEASE
The --platform flag is critical: WEB for static sites (React/Vite/Angular), WEB_COMPUTE for SSR frameworks like Next.js. Setting the wrong platform causes blank pages or 404s for SSR routes because Amplify won't provision the Lambda@Edge functions needed to render server-side pages.
amplify.yml — build settings
Amplify reads amplify.yml from the repository root to determine how to build each branch. The file has four phases: preBuild, build, postBuild, and test. The artifacts.baseDirectory field tells Amplify where to find built files to deploy. If you omit amplify.yml, Amplify tries to auto-detect the framework — but auto-detection fails for non-standard setups.
# amplify.yml — React + Vite app (static hosting)
version: 1
frontend:
phases:
preBuild:
commands:
- nvm use 20 # override default Node 16
- npm ci # use lockfile, not npm install
build:
commands:
- npm run build
artifacts:
baseDirectory: dist # Vite default output directory
files:
- '**/*'
cache:
paths:
- node_modules/**/* # cached between builds — saves ~30s
---
# amplify.yml — Next.js App Router (SSR, platform=WEB_COMPUTE)
version: 1
frontend:
phases:
preBuild:
commands:
- nvm use 20
- npm ci
build:
commands:
- npm run build
artifacts:
baseDirectory: .next # Next.js output directory for WEB_COMPUTE
files:
- '**/*'
cache:
paths:
- .next/cache/**/*
- node_modules/**/*
For WEB_COMPUTE (Next.js SSR), baseDirectory must be .next — not out. The out directory is for static export (next export) which requires WEB platform. If you accidentally use baseDirectory: out with WEB_COMPUTE, Amplify deploys an empty distribution because next build does not create out/ by default.
Monorepo configuration with appRoot
If your MCP server repository contains both the backend and the frontend in separate directories (common in Turborepo or Nx monorepos), set appRoot in amplify.yml to the subdirectory containing the frontend. Amplify uses this as the working directory for all build phases and treats relative paths in artifacts.baseDirectory as relative to appRoot.
# amplify.yml — monorepo with frontend in apps/dashboard
version: 1
applications:
- frontend:
phases:
preBuild:
commands:
- nvm use 20
- npm ci --workspace=apps/dashboard
build:
commands:
- npm run build --workspace=apps/dashboard
artifacts:
baseDirectory: apps/dashboard/dist
files:
- '**/*'
cache:
paths:
- node_modules/**/*
- apps/dashboard/node_modules/**/*
appRoot: apps/dashboard
In the Amplify console, you can also set the app root under App settings → Build settings → App root directory. CLI equivalent: aws amplify update-app --app-id $APP_ID --custom-rules '...' does not handle app root — use the appRoot key in amplify.yml instead, which takes precedence over console settings.
Branch-level environment variables
Environment variables in Amplify have three scopes: App-level (inherited by all branches), Branch-level (override app-level for a specific branch), and Secret (encrypted, not visible in console after save). Build-time variables are available in the build container as shell environment variables. Runtime variables (for SSR) are injected into Lambda@Edge at deploy time — they are NOT the same as build-time variables and must be set separately.
# Set branch-level environment variable via CLI
aws amplify update-branch \
--app-id $APP_ID \
--branch-name develop \
--environment-variables \
REACT_APP_API_URL=https://staging-api.alivemcp.com,\
REACT_APP_ENV=staging
# Add a secret environment variable (encrypted, masked in logs)
aws amplify update-branch \
--app-id $APP_ID \
--branch-name main \
--environment-variables \
API_SECRET_KEY=$SECRET_VALUE # pass via shell variable, not plaintext
# For SSR runtime variables (Next.js), set them on the app:
aws amplify update-app \
--app-id $APP_ID \
--environment-variables \
NEXT_PUBLIC_API_URL=https://api.alivemcp.com
Variables prefixed with REACT_APP_ (Create React App) or NEXT_PUBLIC_ (Next.js) are embedded in the JavaScript bundle at build time — they are visible in the browser. Never put secrets in these variables. For server-side secrets in Next.js API routes, use variables without the NEXT_PUBLIC_ prefix — they're available in the Node.js runtime but not in the client bundle.
Custom domain and HTTPS setup
Amplify provisions an ACM certificate automatically when you add a custom domain. The domain must either be in Route 53 (Amplify can update DNS automatically) or be in an external DNS provider (you manually create CNAME records). Root domain support requires the DNS provider to support ALIAS/ANAME records — Route 53 supports ALIAS, but many external providers only support CNAME (which cannot be used at the zone apex).
# Associate a custom domain (Route 53 auto-DNS-update)
aws amplify create-domain-association \
--app-id $APP_ID \
--domain-name dashboard.alivemcp.com \
--sub-domains \
prefix=www,branchName=main \
prefix='',branchName=main
# For external DNS providers, get the CNAME target:
aws amplify get-domain-association \
--app-id $APP_ID \
--domain-name dashboard.alivemcp.com \
--query 'domainAssociation.subDomains[*].[prefix,dnsRecord]' \
--output table
# Output:
# www | CNAME abcdef1234.cloudfront.net
# '' | CNAME abcdef1234.cloudfront.net ← zone apex (use ALIAS if supported)
ACM certificate validation takes 2–5 minutes when Route 53 manages DNS (Amplify creates the validation CNAME record automatically). For external DNS, you must create the validation CNAME record yourself — Amplify shows it in the console under the domain association. HTTPS is enforced by default; HTTP requests are redirected to HTTPS by the CloudFront distribution that Amplify manages.
Redirects and rewrites for SPA routing
Single-page applications (React, Vue, Angular) handle routing client-side. When a user navigates directly to https://dashboard.alivemcp.com/settings/integrations, Amplify's CloudFront distribution tries to serve a file at /settings/integrations — which doesn't exist. You need a rewrite rule that maps all unmatched paths to /index.html.
# amplify.yml — custom redirects/rewrites section
version: 1
frontend:
phases:
build:
commands:
- npm run build
artifacts:
baseDirectory: dist
files:
- '**/*'
customRules:
- source: '</^[^.]+$|\.(?!(css|gif|ico|jpg|js|png|txt|svg|woff|ttf|map|json)$)([^.]+$)/>'
target: /index.html
status: '200'
# Redirect /api/* to backend (useful for proxying in development)
- source: /api/<path>
target: https://api.alivemcp.com/<path>
status: '200'
The regex rewrite rule matches any path that either has no extension or has an extension that is not one of the listed static asset types. Paths like /settings/integrations (no extension) or /file.unknown are rewritten to /index.html with a 200 status. The status: '200' is a rewrite (transparent); status: '301' or '302' is a redirect (browser URL changes). For SPAs, always use 200 rewrites, not redirects.
Failure modes reference
| Failure | Symptom | Fix |
|---|---|---|
| Wrong platform (WEB vs WEB_COMPUTE) | SSR pages return blank HTML or 404 — Lambda@Edge functions not provisioned | Set platform: WEB_COMPUTE in the Amplify App for Next.js SSR; WEB is for static export only |
| Node.js version mismatch | Build fails with "unsupported engine" or native module compile errors — default is Node 16 | Add nvm use 20 (or desired version) as the first preBuild command in amplify.yml |
| Wrong baseDirectory for Vite/CRA | Deploy succeeds but site returns 403 — Amplify deployed an empty directory | Vite outputs to dist/, CRA to build/, Next.js (static) to out/ — set artifacts.baseDirectory to match; verify with ls in postBuild |
| SPA routing returns 404 | Direct navigation to non-root paths returns AccessDenied from CloudFront | Add the SPA rewrite rule in customRules mapping all unmatched paths to /index.html with status 200 |
| NEXT_PUBLIC_ variable undefined at runtime | SSR component uses variable that is undefined server-side | NEXT_PUBLIC_ variables are build-time embedded — also set the same variable without the prefix for server-side access; or use process.env in server components directly |
| ACM certificate stuck in PENDING_VALIDATION | Domain association shows PENDING_VALIDATION for more than 15 minutes | For external DNS: manually create the CNAME validation record shown in the Amplify console; for Route 53: check that Amplify has permission to update the hosted zone (it uses the account's IAM credentials) |
| Monorepo build installs all workspace deps | Build takes 5+ minutes downloading packages unrelated to the frontend | Use npm ci --workspace=apps/dashboard to install only the workspace's dependencies; cache node_modules/**/* to avoid re-download on unchanged packages |