Guide · AWS Amplify · Environment Variables

Amplify Environment Variables and Secrets for MCP Server Dashboards — Build-Time, Runtime, and SSM Integration

Amplify Hosting exposes environment variables to your MCP server dashboard build process as shell environment variables available in every build phase. The scoping model has two dimensions: level (app-level variables are inherited by all branches; branch-level variables override app-level for a specific branch) and sensitivity (plain variables are visible in the console after save; secret variables are write-only — saved values cannot be retrieved via the console or API). Build-time variables are embedded in the JavaScript bundle during npm run build — they are permanent and visible in the browser's source. Runtime variables for SSR (Next.js API routes, server components) are injected into the Lambda@Edge function environment at deploy time and are not visible client-side. The critical operational trap: Amplify does not automatically re-deploy when you update an environment variable — you must trigger a new build (aws amplify start-job) for the change to take effect. For secrets management in production, the recommended pattern is to store sensitive values in AWS SSM Parameter Store (SecureString) and fetch them at build time via aws ssm get-parameter --with-decryption in the preBuild phase.

TL;DR

Set non-sensitive variables at app level, override per branch. Use the SECRET type for API keys and tokens — they are masked in build logs and not retrievable after save. Prefix variables with REACT_APP_ (CRA) or NEXT_PUBLIC_ (Next.js) for client-side bundle embedding; omit the prefix for server-side-only access. For secrets, use SSM Parameter Store SecureString and fetch via AWS CLI in preBuild — this avoids storing secrets in Amplify's environment variable store entirely. Trigger a redeploy after variable updates with aws amplify start-job --job-type RELEASE.

App-level vs branch-level variable scoping

Amplify has a two-level variable hierarchy. App-level variables apply to every branch, including auto-created branches and PR previews. Branch-level variables override app-level values for a specific branch. Variables are merged at build time — if a variable exists at both levels, the branch-level value wins.

# Set app-level variables (all branches inherit)
aws amplify update-app \
  --app-id $APP_ID \
  --environment-variables \
    REACT_APP_SUPPORT_EMAIL=support@alivemcp.com,\
    REACT_APP_VERSION=1.0.0,\
    NODE_OPTIONS=--max-old-space-size=4096

# Override for the main (production) branch
aws amplify update-branch \
  --app-id $APP_ID \
  --branch-name main \
  --environment-variables \
    REACT_APP_API_URL=https://api.alivemcp.com,\
    REACT_APP_ENV=production

# Override for develop (staging) branch
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

# IMPORTANT: updating variables does NOT trigger a rebuild
# Always start a new job after variable changes:
aws amplify start-job \
  --app-id $APP_ID \
  --branch-name main \
  --job-type RELEASE

Branch-level --environment-variables is a complete replacement, not a merge — if you call update-branch with only one variable, you lose all previously set branch-level variables. Always include all branch-level variables in each update-branch call, or use the console to add individual variables. The app-level variables are unaffected by branch-level updates.

Secret variables and masked values

Variables marked as secrets in Amplify are stored encrypted and masked in build logs. Once saved, the value cannot be retrieved via the Amplify console or API — only the variable name is visible. This is appropriate for API keys, webhook secrets, and OAuth client secrets used during the build process.

# The Amplify console marks variables as secret via the UI toggle.
# Via CLI, pass the value via a shell variable (not plaintext in the command):

# Fetch from an existing secret store (e.g., 1Password, Vault):
STRIPE_KEY=$(op item get "Amplify Stripe Key" --fields credential)

# Set as branch-level variable — the value won't appear in shell history
# if passed via a variable (not a literal string):
aws amplify update-branch \
  --app-id $APP_ID \
  --branch-name main \
  --environment-variables \
    REACT_APP_STRIPE_PUBLISHABLE_KEY=$STRIPE_KEY

# For truly sensitive values (private keys, DB passwords):
# Do NOT store in Amplify env vars — use SSM Parameter Store instead (see below)

# Verify the variable exists (name only — value is masked):
aws amplify get-branch \
  --app-id $APP_ID \
  --branch-name main \
  --query 'branch.environmentVariables' \
  --output json
# Output: { "REACT_APP_API_URL": "...", "REACT_APP_STRIPE_PUBLISHABLE_KEY": "*****" }

Amplify secret variables are masked in build logs — the string ***** replaces the value if it's printed via echo or logged during the build. However, masking is value-based — if your build script uses the value as part of a URL or other string, the composed string may not be masked. Never log environment variable values directly in your build scripts.

Build-time vs runtime variable access (Next.js)

In Next.js, variables fall into three categories based on their prefix and how they're used. Understanding this distinction prevents the common mistake of deploying a Next.js app with undefined variables at runtime.

# Category 1: NEXT_PUBLIC_ — build-time embedded, client-visible
# Available in browser and server components, embedded in bundle
NEXT_PUBLIC_API_URL=https://api.alivemcp.com
NEXT_PUBLIC_POSTHOG_KEY=phc_abc123

# In code:
const apiUrl = process.env.NEXT_PUBLIC_API_URL;  // works in browser AND server

# Category 2: No prefix — runtime-only, server-side
# Available in API routes, Server Actions, and server components
# NOT available in client components or browser
DATABASE_URL=postgres://...
INTERNAL_API_SECRET=secret-value

# In code (server-side only):
// pages/api/sessions.ts or app/api/sessions/route.ts
export async function GET() {
  const db = await connect(process.env.DATABASE_URL);  // works server-side
  // process.env.DATABASE_URL is undefined in browser — correct behavior
}

# Category 3: Variables read at build time via next.config.ts
// next.config.ts
const nextConfig = {
  env: {
    BUILD_TIME_COMMIT: process.env.AWS_JOB_ID,  // injected by Amplify
    AMPLIFY_BRANCH: process.env.AWS_BRANCH
  }
};
// These are embedded in the bundle as literals at build time

For SSR in Amplify, runtime variables (no NEXT_PUBLIC_ prefix) must be set on the Amplify App (not just the branch), because the Lambda@Edge function environment is populated from app-level variables. Branch-level variables without NEXT_PUBLIC_ prefix do NOT flow to the Lambda@Edge runtime — this is the most common source of "environment variable undefined in production" bugs with Amplify SSR.

SSM Parameter Store integration for secrets

For secrets that must not be stored in Amplify's variable store — database connection strings, private keys, third-party API secrets — use AWS SSM Parameter Store SecureString values. Fetch them during the preBuild phase and export them as environment variables for the rest of the build.

# Store secrets in SSM Parameter Store (done once, outside Amplify):
aws ssm put-parameter \
  --name /alivemcp/prod/db-url \
  --value "postgres://user:pass@db.alivemcp.com/mcpdb" \
  --type SecureString \
  --key-id alias/aws/ssm   # use AWS managed key (or your own CMK)

aws ssm put-parameter \
  --name /alivemcp/prod/stripe-secret-key \
  --value "sk_live_..." \
  --type SecureString

# amplify.yml — fetch secrets in preBuild
version: 1
frontend:
  phases:
    preBuild:
      commands:
        - nvm use 20
        - npm ci
        - |
          # Fetch from SSM and export as env vars
          export DATABASE_URL=$(aws ssm get-parameter \
            --name /alivemcp/prod/db-url \
            --with-decryption \
            --query Parameter.Value \
            --output text)
          export STRIPE_SECRET_KEY=$(aws ssm get-parameter \
            --name /alivemcp/prod/stripe-secret-key \
            --with-decryption \
            --query Parameter.Value \
            --output text)
    build:
      commands:
        - npm run build   # DATABASE_URL and STRIPE_SECRET_KEY are available here
  artifacts:
    baseDirectory: dist
    files:
      - '**/*'

The Amplify build container runs with an IAM role that you can extend. To allow the build to call ssm:GetParameter on your parameters, add an IAM policy to the Amplify service role. In the Amplify console, go to App settings → General → Service role and attach a policy with ssm:GetParameter and kms:Decrypt (for SecureString values encrypted with a customer-managed KMS key).

Amplify-injected build variables

Amplify automatically injects several variables into the build container that you can use in build scripts and embed into the bundle for debugging and telemetry.

# Variables automatically injected by Amplify into every build:
AWS_APP_ID           # Amplify App ID (e.g., d1abc23def456)
AWS_BRANCH           # Branch being built (e.g., main, develop, pr-47)
AWS_REGION           # AWS region of the Amplify app
AWS_JOB_ID           # Build job ID (unique per build)
AWS_COMMIT_ID        # Git commit SHA that triggered the build

# Use in amplify.yml build phase:
# build:
#   commands:
#     - echo "Building branch $AWS_BRANCH at commit $AWS_COMMIT_ID"
#     - VITE_BUILD_ID=$AWS_JOB_ID npm run build

# Embed in the React app for runtime debugging:
# In vite.config.ts:
import { defineConfig } from 'vite';
export default defineConfig({
  define: {
    __BUILD_BRANCH__: JSON.stringify(process.env.AWS_BRANCH),
    __BUILD_COMMIT__: JSON.stringify(process.env.AWS_COMMIT_ID),
    __BUILD_JOB__: JSON.stringify(process.env.AWS_JOB_ID)
  }
});

Embedding AWS_BRANCH in the bundle is useful for debugging PR preview environments — you can display the branch name in the UI so testers know which PR they're reviewing. Embedding AWS_COMMIT_ID enables linking a production bug to the exact commit that introduced it. Both are non-sensitive values that are safe to expose to the browser.

Failure modes reference

FailureSymptomFix
Variable update doesn't take effectNew variable value not reflected after updating in consoleAmplify does not auto-redeploy on variable change — trigger a new build with aws amplify start-job --job-type RELEASE
Branch-level variable update wipes othersPreviously set branch variables disappear after CLI updateupdate-branch --environment-variables replaces all branch vars — always include all variables in a single call
SSR runtime variable undefinedNext.js API route cannot read process.env.MY_VAR at runtimeNo-prefix runtime vars must be at app level (not branch level) — branch-level variables don't flow to Lambda@Edge runtime
SSM fetch fails in buildpreBuild phase exits with AccessDeniedException calling ssm:GetParameterAttach an IAM policy to the Amplify service role granting ssm:GetParameter on the specific parameter ARNs; add kms:Decrypt if using CMK-encrypted SecureString
NEXT_PUBLIC_ variable undefined client-sideprocess.env.NEXT_PUBLIC_API_URL is undefined in the browserNEXT_PUBLIC_ variables are embedded at build time — ensure the variable is set in Amplify before the build runs, not just in the shell running amplify commands
Secret value exposed in build logAPI key appears in plain text in Amplify build logAmplify masks exact secret values — if the secret is composed into a larger string before logging, the composed string may not be masked; remove echo/console.log of secret-derived values