One backend, three environments: the 4 KiB limit that redesigned my EAS setup

작성자

카테고리:

← 피드로
DEV Community · Ibukun Demehin · 2026-07-20 개발(SW)

Three build profiles, three backends. My Expo app’s development build should talk to my local sandbox, preview to the dev branch, and production to main. The obvious way to hand each build its backend config is an EAS environment variable — so I tried, and EAS stopped me cold:

Files can't be larger than 4 KiB. Provided value is 410 bytes above the limit.

Enter fullscreen mode Exit fullscreen mode

My config file was 4.5 KB. The same file in a more mature project of mine is 85 KB. The whole delivery mechanism was a dead end before I’d shipped a single build. Here’s the pattern that replaced it.

TL;DR — EAS “file” env vars cap at 4 KiB. Amplify’s amplify_outputs.json carries model_introspection (your whole typed schema) and grows unbounded with every model, so it blows past the cap. The fix: commit one outputs file per environment and copy the right one into place with an eas-build-pre-install hook keyed on the build profile.

(This is part 2 of Building CannyCart, a voice-first shopping app on Amplify Gen 2 + Expo. Part 1 covered the stack; this one is about getting the right backend into each mobile build.)

Why a mobile app has to bake its backend in

CannyCart has one Amplify Gen 2 backend definition — TypeScript, CDK underneath — deployed three separate times: a personal sandbox for local dev, the dev git branch, and the main git branch. Each is its own isolated stack: its own DynamoDB tables, its own Cognito pool, its own AppSync API.

The client has to know which one it’s talking to. That knowledge lives in amplify_outputs.json: endpoints, region, pool IDs, and a full description of the schema. The question is how that file gets into a build pointed at the right environment.

On the web side, this is a non-problem. Amplify Hosting builds the Next.js app in CI and runs ampx pipeline-deploy, which regenerates amplify_outputs.json fresh for that branch’s backend during every build. The web app never commits the file — it’s produced on the spot.

On mobile, EAS builds run on Expo’s servers. They install dependencies and bundle with Metro — but they do not have my AWS credentials, and they do not deploy or query my backend. There’s no ampx step in that pipeline to regenerate outputs. Which means:

The config has to already be in the repo when Metro bundles. Mobile bakes its backend in at commit time; it can’t fetch it at build time.

That single asymmetry is the whole reason this post exists.

The obvious mechanism, and the wall

EAS supports environment variables, including a file type — you register a file, and EAS materializes it into the build workspace. It’s exactly how you ship things like google-services.json. So the plan was obvious: register amplify_outputs.json as a file env var, one per environment. Done in five minutes.

Except:

Files can't be larger than 4 KiB. Provided value is 410 bytes above the limit.

Enter fullscreen mode Exit fullscreen mode

4 KiB is 4,096 bytes. My file was 4,506 — hence “410 bytes above.” And here’s the part that turned a quick fix into a redesign: that file only grows.

amplify_outputs.json isn’t just endpoints and IDs. It carries model_introspection — a complete, machine-readable description of every model in your schema, so the typed Amplify client can validate queries and shape results without a network round-trip. Add a model, the file gets bigger. At the time I hit this, CannyCart had a handful of models and the file was 4.5 KB. Today, after adding just two more (ShoppingList and ShoppingItem), it’s 13 KB. In a mature project of mine it’s 85 KB (84,689 bytes, to be exact).

So this was never a “trim some whitespace” problem. The 4 KiB ceiling and an artifact that scales with your schema are fundamentally incompatible. The file env var was the wrong tool.

The options I weighed

1. Regenerate outputs during the EAS build, like the web does. Clean in theory — no committed file, always fresh. It doesn’t work for mobile: EAS build servers have no AWS credentials and don’t run my backend deploy. ampx generate outputs needs cloud access I can’t (and really shouldn’t) hand to Expo’s CI. Rejected.

2. Shard the file across several env vars and reassemble it at build time. Technically possible. But it’s brittle, it’s still fundamentally unbounded (add enough models and you’re juggling five env vars instead of one), and you’re hand-marshalling a generated artifact through string-splitting. Rejected on sight.

3. Commit one outputs file per environment, and select the right one at build time. Boring. Slightly heretical (committing generated config — more on that below). And it works with four lines of shell. Chosen.

The pattern that shipped

Three committed files live at the repo root — one per environment:

amplify_outputs.development.json   → local sandbox
amplify_outputs.preview.json       → Amplify Hosting "dev" branch
amplify_outputs.production.json    → Amplify Hosting "main" branch

Enter fullscreen mode Exit fullscreen mode

(preview and production only appear once those branches are actually deployed; until then the build falls back to a committed root file.)

eas.json has a profile per environment:

{
  "build": {
    "development": {
      "developmentClient": true,
      "distribution": "internal",
      "environment": "development"
    },
    "preview":    { "distribution": "internal", "environment": "preview" },
    "production": { "environment": "production" }
  }
}

Enter fullscreen mode Exit fullscreen mode

The glue is an eas-build-pre-install hook — an npm script EAS runs automatically before it installs dependencies. In the mobile app’s package.json:

{
  "scripts": {
    "eas-build-pre-install": "sh scripts/eas-select-outputs.sh"
  }
}

Enter fullscreen mode Exit fullscreen mode

And the script itself, which copies the file matching $EAS_BUILD_PROFILE over the root amplify_outputs.json that Metro will read:

# eas-build-pre-install: copy the committed amplify_outputs.<env>.json
# matching this build profile over the root amplify_outputs.json.
case "$EAS_BUILD_PROFILE" in
  development|development-simulator) ENV_NAME=development ;;
  preview)    ENV_NAME=preview ;;
  production) ENV_NAME=production ;;
  *)          ENV_NAME="" ;;
esac

if [ -n "$ENV_NAME" ] && [ -f "$ROOT/amplify_outputs.$ENV_NAME.json" ]; then
  echo "using amplify_outputs.$ENV_NAME.json (profile: $EAS_BUILD_PROFILE)"
  cp "$ROOT/amplify_outputs.$ENV_NAME.json" "$ROOT/amplify_outputs.json"
else
  echo "no per-env file for '$EAS_BUILD_PROFILE'; using committed amplify_outputs.json"
fi

Enter fullscreen mode Exit fullscreen mode

That’s the entire mechanism. $EAS_BUILD_PROFILE is set by EAS to whatever profile you’re building, the hook picks the matching file, and by the time Metro bundles, amplify_outputs.json is the right one. No size limit, because git doesn’t care that the file is 13 KB or 85 KB.

Build profile Outputs file Backend development, development-simulator amplify_outputs.development.json local sandbox preview amplify_outputs.preview.json Amplify Hosting dev branch production amplify_outputs.production.json Amplify Hosting main branch

Keeping the files honest

Committing a generated file to git is normally a smell — generated artifacts drift from their source and bloat diffs. I accept it here for two reasons: outputs are not secret (endpoints and public pool IDs, no keys), and on EAS there’s genuinely no build-time way to regenerate them. Given the constraint, a committed file is the honest option, not a shortcut.

The cost is discipline: regenerate the per-env file after any backend change, or you ship stale config. So there’s a refresh script per environment:

// package.json
"outputs:development": "npx ampx generate outputs --profile cannycart && cp amplify_outputs.json amplify_outputs.development.json",
"outputs:preview":     "npx ampx generate outputs --branch dev  --app-id $AMPLIFY_APP_ID --out-dir .amplify/outputs-tmp && mv .amplify/outputs-tmp/amplify_outputs.json amplify_outputs.preview.json",
"outputs:production":  "npx ampx generate outputs --branch main --app-id $AMPLIFY_APP_ID --out-dir .amplify/outputs-tmp && mv .amplify/outputs-tmp/amplify_outputs.json amplify_outputs.production.json"

Enter fullscreen mode Exit fullscreen mode

Sandbox changed? npm run outputs:development. A branch backend redeployed? The preview/production variants pull that branch’s outputs into a temp dir so they don’t clobber your local sandbox file.

And the discipline gap is real — it bit me this week. I added the two shopping models to the schema, deployed the sandbox, and moved on. My committed amplify_outputs.development.json still described the schema from before those models: 4.5 KB on disk while the live sandbox output was 13 KB. A development build in that state would have bundled a typed client that didn’t know ShoppingList or ShoppingItem existed. One npm run outputs:development and a commit fixed it — but nothing warned me. I caught it by eyeballing file sizes. A pre-commit check that diffs each per-env file against a fresh generate is now on my list; committing generated config buys you the 4 KiB escape but hands you back a freshness problem to automate.

When you’d choose differently

This has held up well — the copy hook is four lines of case, and every build deterministically gets its environment’s backend. Nothing leaks, because there are no secrets in outputs to leak.

You’d choose differently in two cases. If your outputs somehow did carry secrets, committing is off the table and you’d need a real secrets mechanism instead. And if you could run your backend deploy inside your mobile CI — regenerating at build time the way Amplify Hosting does for the web — that’s cleaner than committing a generated file at all. For an EAS build that can’t touch your AWS account, though, baking it in is the move.

Next up

The evening every icon in the app vanished at once:

TypeError: Cannot read properties of undefined (reading 'transformFile')

Enter fullscreen mode Exit fullscreen mode

The trail runs through an icon library that reinvented itself between major versions and ends at a babel package published in 2017, silently hoisted to the top of my node_modules. That’s post 3.

If you’ve run multi-environment EAS + Amplify Gen 2 a different way — especially if you found a build-time regeneration path I missed — I’d genuinely like to hear it.

원문에서 계속 ↗

코멘트

답글 남기기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다