The Incident: A Plot Twist I Didn't See Coming
It's 2 AM on a Tuesday. I'm shipping a beautiful new map routing feature for our real estate app. You know, the kind of feature that makes product managers cry tears of joy—property seekers can now calculate routes from their location to listings. Chef's kiss.
I push to Koyeb (because I'm fancy and like their UI):
git push origin main
Koyeb's build logs start scrolling. Then I see something... familiar:
* Running scripts
* Running `npm run build`
> flexdown@0.0.2 build
> tsc
src/modules/map/map.controller.ts(32,13): error TS1434: Unexpected keyword or identifier.
src/modules/map/map.controller.ts(32,22): error TS1005: ';' expected.
src/modules/map/map.controller.ts(32,27): error TS1434: Unexpected keyword or identifier.
! Failed to execute build script
!
! The Heroku Node.js npm Install buildpack allows customization...
!
! If the issue persists and you think you found a bug in the buildpack,
! reproduce the issue locally with a minimal example. Open an issue in
! the buildpack's GitHub repository and include the details here:
! __https://github.com/heroku/buildpacks-nodejs/issues__
ERROR: failed to build: exit status 1
Build failed ❌
Wait. WAIT.
"The Heroku Node.js npm Install buildpack"???
I'm on Koyeb, not Heroku. Why is Heroku mentioned in my Koyeb logs?
Googles frantically
Oh. OH.
Koyeb uses Heroku buildpacks under the hood. They're literally running the same build system.
So when I thought I was using a "modern alternative to Heroku," I was actually just using... Heroku with extra steps and a prettier dashboard.
This is like ordering an off-brand soda and finding out it's made in the same factory as Coca-Cola.
Meanwhile, on my Mac:
$ npm run build
✅ Build successful
The Realization: I'm not fighting Koyeb. I'm fighting Heroku's buildpack, which Koyeb borrowed. And both my Mac and these platforms are Unix/LF based, so... what's the actual problem?
Initial Investigation: The "But We're All Unix!" Phase
The Setup
- My Mac: macOS (Unix-based, LF line endings)
- Koyeb: Linux containers (Unix-based, LF line endings)
- Koyeb's buildpack: Heroku's Node.js buildpack (also Unix/LF)
We're all in the Unix family. We should be getting along!
Hypothesis 1: Line Endings (Immediately Rejected)
First instinct: "Maybe CRLF vs LF?"
$ file src/modules/map/map.controller.ts
src/modules/map/map.controller.ts: UTF-8 Unicode text
LF confirmed. Not Windows CRLF nonsense.
$ git config core.autocrlf
input # Correct for Unix systems
So... not line endings.
Status: Hypothesis dead on arrival.
Hypothesis 2: Heroku Buildpack Caching
I noticed this in the logs:
! If the issue persists and you think you found a bug in the buildpack...
! __https://github.com/heroku/buildpacks-nodejs/issues__
Wait, so Koyeb is using Heroku's buildpack, which means:
- Heroku's caching behavior
- Heroku's dependency resolution
- Heroku's build environment quirks
This isn't a Koyeb problem. This is a Heroku buildpack problem running on Koyeb infrastructure.
Hypothesis 3: TypeScript Version Mismatch (BINGO)
# My local environment
$ npx tsc --version
Version 5.3.3
# My package.json
$ cat package.json | grep typescript
"typescript": "^5.3.0"
That ^ symbol. That little caret of chaos.
What it means:
^5.3.0= "any version from 5.3.0 up to (but not including) 5.4.0"- My Mac: installed TypeScript 5.3.3 (latest in the 5.3.x range)
- Koyeb/Heroku buildpack: cached TypeScript 5.3.0 from a week ago
The Heroku buildpack caches dependencies aggressively. Once it builds your app with TypeScript 5.3.0, it keeps using that version until you explicitly clear the cache or update your lock file.
So while I'm living my best life with 5.3.3 locally, Koyeb (via Heroku buildpack) is stuck in the past with 5.3.0.
Root Cause Analysis: The Crime Scene at Line 32
Let me show you the code that made Heroku's buildpack (running on Koyeb) lose its mind:
if (!userLocation || !propertyLocation) {
res.status(400).json({
success: false,
message: 'User location and property location are required'
});
return;
// add this just to push a new update
}
That comment? That's the villain.
What TypeScript 5.3.0 (Heroku Buildpack) Sees:
[Parser] return statement detected
[Parser] blank line detected
[Parser] comment token detected
[Parser] closing brace detected
[Parser Brain]:
"Is this comment part of return?"
"Is it standalone?"
"Is it attached to the next statement?"
"SYNTAX ERROR: EXISTENTIAL CRISIS MODE"
ERROR TS1434: Unexpected keyword or identifier
The parser literally cannot decide what to do with a comment that has whitespace before it and a closing brace after it.
What TypeScript 5.3.3 (My Mac) Sees:
[Parser] return statement
[Parser] whitespace (whatever, it's fine)
[Parser] comment token (cool, comments are legal)
[Parser] closing brace
[Parser Brain]: "Everything's fine, moving on."
BUILD SUCCESSFUL ✅
The difference: Between 5.3.0 and 5.3.3, TypeScript improved its whitespace handling around comments in control flow contexts. My local version is chill. The buildpack's cached version is NOT.
Why This Is a Heroku Buildpack Issue
The error message literally tells us:
! The Heroku Node.js npm Install buildpack allows customization...
!
! If the issue persists and you think you found a bug in the buildpack...
Koyeb is using Heroku's buildpack, which means:
- Aggressive caching: Once it installs
^5.3.0and gets 5.3.0, it caches that FOREVER - Semver resolution: Respects package-lock.json, but if that's not committed or gets regenerated, it picks whatever version is available at build time
- Build reproducibility: Supposed to be deterministic, but only if your lock file is committed
Technical Deep Dive: Unix, LF, and Why None of That Matters Here
The Red Herring: Line Endings
People (including me at 2 AM) love to blame line endings:
- Windows: CRLF (
\r\n) - Unix (Mac/Linux): LF (
\n)
But here's the thing: This issue has nothing to do with line endings.
Both macOS and Linux use LF. The Heroku buildpack runs on Linux (LF). My code has LF. Everyone's using LF.
The TypeScript parser doesn't care about the literal bytes \r\n vs \n. It cares about the Abstract Syntax Tree (AST) structure.
What Actually Matters: Parser State Machine
TypeScript's parser is a state machine that goes:
Source Code
↓
Scanner/Lexer (tokenizes: keywords, symbols, whitespace, comments)
↓
Parser (builds AST) ← WE DIE HERE
↓
Binder (creates symbol tables)
↓
Type Checker (validates types)
↓
Emitter (outputs JavaScript)
My code dies in Phase 2: Parser trying to build the AST.
The parser sees:
return; ← Statement node (complete)
← Whitespace (creates separation)
// comment ← Comment node (ambiguous association)
} ← Block end (unexpected context)
TypeScript 5.3.0 parser logic:
if (previousNode === ReturnStatement && currentToken === Comment) {
if (hasWhitespaceBetween && nextToken === CloseBrace) {
// Unclear if comment belongs to return or block
throw TS1434("Unexpected keyword or identifier");
}
}
TypeScript 5.3.3 parser logic:
if (previousNode === ReturnStatement && currentToken === Comment) {
// Comments can float between statements, it's fine
parseCommentNode();
continue;
}
This is a parser improvement, not a line ending issue.
Why Heroku Buildpack Caching Screwed Me
Heroku's buildpack (which Koyeb uses) caches aggressively:
# First build
- Detects package.json: "typescript": "^5.3.0"
- Resolves to TypeScript 5.3.0 (current at the time)
- Caches node_modules with TypeScript 5.3.0
- Creates layer cache
# Second build (my deploy)
- Detects package.json hasn't changed
- Uses cached node_modules (TypeScript 5.3.0)
- Never checks if 5.3.3 exists
- Builds with old version
- FAILS because parser doesn't like my comment
Meanwhile, my Mac:
$ npm install
- Resolves "^5.3.0"
- Finds TypeScript 5.3.3 (latest)
- Installs 5.3.3
- Builds successfully
The problem: Semver ranges + aggressive caching = version drift between local and production.
Resolution Strategy: Fixing the Heroku Buildpack Chaos
The Immediate Fix (Tactical)
Step 1: Delete the cursed comment
// BEFORE (breaks Heroku buildpack)
return;
// add this just to push a new update
// AFTER (works everywhere)
return;
Why this works: Removes the parser ambiguity entirely.
Step 2: Deploy
git add .
git commit -m "fix: remove comment that TypeScript 5.3.0 hates"
git push origin main
Koyeb (via Heroku buildpack):
✅ Build succeeded
✅ Deployed
Victory! But we're not done.
The Long-term Fix (Strategic)
1. Pin TypeScript to Exact Version
// package.json - BEFORE
{
"devDependencies": {
"typescript": "^5.3.0" // ❌ Allows 5.3.x updates
}
}
// package.json - AFTER
{
"devDependencies": {
"typescript": "5.3.3" // ✅ Exact version only
}
}
This tells the Heroku buildpack: "Install THIS EXACT VERSION, not whatever you feel like caching."
2. Lock Node.js and npm Versions
{
"engines": {
"node": "18.19.1",
"npm": "10.2.4"
}
}
The Heroku buildpack respects the engines field and will use these exact versions.
3. Regenerate and Commit Lock File
# Delete everything
rm -rf node_modules package-lock.json
# Fresh install with exact versions
npm install
# Commit the lock file
git add package.json package-lock.json
git commit -m "chore: pin TypeScript to 5.3.3"
git push origin main
Now package-lock.json has TypeScript 5.3.3 locked in. The Heroku buildpack will respect this.
4. Clear Koyeb's Build Cache
In Koyeb dashboard:
- Go to your service settings
- Find "Build Cache" or "Clear Cache"
- Click it
- Redeploy
Or, force a clean build:
# Add empty commit to trigger rebuild
git commit --allow-empty -m "chore: clear Koyeb cache"
git push origin main
5. Verify Versions Match
# Local
$ npx tsc --version
Version 5.3.3
# In Koyeb logs after deploy
[Building] Installing dependencies...
[Building] typescript@5.3.3 installed
[Building] Running npm run build...
✅ Build successful
The Senior Engineer Wisdom™ I Gained
1. Koyeb Uses Heroku Under the Hood (Mind = Blown)
When I saw this in the logs:
! The Heroku Node.js npm Install buildpack allows customization...
I realized: Koyeb isn't a Heroku alternative. It's a Heroku buildpack wrapper with a prettier UI.
This changes everything:
- Same caching behavior
- Same build quirks
- Same documentation applies
- Same best practices
2. Semver Ranges + Caching = Pain
"typescript": "^5.3.0" // ❌ "Surprise me with any 5.3.x version!"
"typescript": "5.3.3" // ✅ "I know exactly what I want"
For build tools (TypeScript, Webpack, etc.), always pin exact versions.
For runtime dependencies, use ~ (patch updates only):
"express": "~4.18.0" // Allows 4.18.x patches, not 4.19.0
3. Unix/LF Doesn't Guarantee Compatibility
All Unix systems use LF, but that doesn't mean code will build identically:
- Different TypeScript versions
- Different Node.js versions
- Different npm versions
- Different package resolution
Environment parity requires version parity, not just OS parity.
4. Comments Are Not Harmless
A single comment with weird whitespace broke production.
Delete throwaway comments. Use Git for notes:
git commit --allow-empty -m "trigger rebuild for cache refresh"
5. Read Your Build Logs Carefully
That Heroku buildpack error message was THE clue that Koyeb uses Heroku under the hood. I almost missed it.
Epilogue: A Letter to Koyeb
Dear Koyeb,
I love your UI. I love your developer experience. I love that you're trying to be better than Heroku.
But... you're literally running Heroku buildpacks.
Could you maybe... mention this more prominently in the docs? Like, right at the top?
"⚠️ We use Heroku buildpacks, so Heroku best practices apply."
Would've saved me 4 hours at 2 AM.
Still love you though,
A Tired Developer
P.S. - Your dashboard is prettier than Heroku's. That counts for something.
TL;DR (For the Skimmers)
What happened:
- Deployed to Koyeb
- Got TypeScript build errors
- Worked fine locally (Mac)
The twist:
- Koyeb uses Heroku buildpacks under the hood
- Error message literally said "Heroku Node.js npm Install buildpack"
- Same caching behavior as Heroku
Why it broke:
- Local Mac: TypeScript 5.3.3
- Koyeb (via Heroku buildpack): TypeScript 5.3.0 (cached)
- Different parser behavior around comments with whitespace
The villain:
return;
// add this just to push a new update ← This comment
The fix:
- Deleted the comment
- Pinned TypeScript:
"typescript": "5.3.3" - Pinned Node.js in
enginesfield - Committed
package-lock.json - Cleared Koyeb build cache
Key lessons:
- Koyeb = Heroku buildpacks + nice UI
- Pin your build tools (no
^or~) - Use
npm cifor deterministic builds - Unix/LF doesn't guarantee build compatibility
- Comments can break production
The Meme That Sums It Up
Me: "I'll use Koyeb instead of Heroku"
Koyeb build logs: "Heroku Node.js buildpack..."
Me: "Wait—"
Koyeb: "Always has been 🔫"
Written at 5:47 AM after discovering Koyeb's secret. If you're debugging a similar issue, now you know: it's Heroku all the way down.
Using Koyeb and hit a weird build issue? Drop a comment. Let's compare Heroku buildpack war stories. 😅