All articles written by AI. Learn more about our AI journalism
All posts
Building in Public·

Infrastructure Week: Bun Migration, Prisma 7, and the Features That Shipped Anyway

This was supposed to be a quiet week. Ship a few features, improve some UX, maybe fix that canonical URL issue Google Search Console had been complaining about.

Instead I ended up with 34 commits, most of them variations of "fix Docker build" and "fix Prisma generate." The kind of week where you look at the git log and think: what was I doing?

I was migrating to Bun.

Why Bun in the First Place

The honest answer: I wanted to see if the performance claims were real.

BuzzRAG runs as a monorepo with a Next.js web app and a separate worker process handling background jobs. The worker does transcription, article generation, social media posting—CPU-bound stuff that runs continuously.

Node.js was fine. But "fine" doesn't make you refactor your entire build pipeline on a Tuesday afternoon.

Bun's pitch is speed. Faster installs, faster runtime, native TypeScript support. For a side project where I'm paying for Railway compute, shaving milliseconds off every request adds up.

The migration started clean. Replace npm with bun. Update the scripts. Watch the lockfile regenerate.

Then I hit ytdl-core.

When Dependencies Fight Your Runtime

ytdl-core is a popular library for downloading YouTube videos. I use it to fetch audio for transcription. It works great on Node.js.

It does not work on Bun.

Deep in its internals, ytdl-core uses undici—Node's native HTTP client. Bun has its own HTTP implementation. They don't agree on how certain things should work.

So I ripped it out and replaced it with yt-dlp, the CLI tool. More of a workaround than a fix, but it works everywhere, handles edge cases ytdl-core couldn't, and doesn't care what JavaScript runtime is calling it.

This is the part nobody warns you about with runtime migrations. Your code is probably fine. Your dependencies are the landmines.

The Docker Build Saga

Local development switched to Bun in about an hour. Getting Docker to cooperate took two days.

The commit messages tell the story:

  • Fix bun.lock for cross-platform compatibility
  • Remove --frozen-lockfile from Dockerfiles for cross-platform compatibility
  • Fix Prisma generate: use bun add for @prisma/client before generate
  • Fix Prisma client path for bun (.bun directory)
  • Fix worker: install Prisma client with npm in runner stage

Each one is a bug I only discovered when Railway rejected the build.

The Prisma client path was the worst. Bun stores dependencies in .bun instead of node_modules. Prisma's codegen didn't know where to look. I had to explicitly tell it where the client lived, then realized the runner stage needed npm to install it because Bun's workspace resolution wasn't available after the build stage.

At some point I pinned the Prisma CLI to 5.22.0 because the latest version introduced new incompatibilities. Solving problems by not upgrading is a valid strategy.

Then Prisma 7 Dropped

Mid-migration, I decided to also upgrade Prisma from v5 to v7.

In retrospect, this was unwise.

Prisma 7 has breaking changes. The provider syntax changed. The output path is now required. The database URL moved from the schema file to a runtime adapter. The whole thing went Rust-free, which is good for bundle size but means internals work differently.

I touched 136 files. Most of that was updating imports to pull from a shared package, but some was genuine adapter plumbing. The web app and worker now both use PrismaPg with pg.Pool instead of the old connection string approach.

The payoff: smaller builds, faster cold starts, native ESM support. But if I'd known the scope, I probably would have done it in a separate week.

Features That Actually Shipped

Despite the infrastructure chaos, real features went out:

AI Curation Scoring. Stories now get 0-100 scores based on topic freshness, content depth, key learnings quality, unique insights, channel authority, and site alignment. Each factor is 10-20 points. The idea is to surface the most article-worthy transcriptions automatically instead of manually scanning everything.

Twitter Reply Detection. The worker now polls every 5 minutes for replies to our tweets. When someone responds, it generates three AI reply suggestions: thankful, engaging, and informative. Human picks which one to post (or writes something else). Same pattern as article generation—AI drafts, human decides.

Better Article Endings. This one came from reader feedback via Bert and Navilos. The AI was defaulting to generic "Conclusion" headers. Now it's prohibited from using that word entirely and instead uses ending strategies like callbacks, future questions, or clean stops. Small change, noticeable improvement.

Tweet Scheduling UX. Added Now, +5 min, and -5 min buttons for quick time adjustments. Before, you had to use a date picker to change the post time. Now you can rapidly adjust without precision clicking.

Also fixed: www redirect was breaking API POST requests (307s fighting with Next.js), canonical URLs were wrong for Google Search Console, OAuth flow broke because /auth routes were getting redirected, and the worker wasn't actually saving curation scores to the database.

What I Learned

Migrating runtimes is easier when you're not also upgrading your ORM.

Docker builds fail in ways that local development can't predict. Just accept that you'll iterate on the Dockerfile in production.

Dependencies that work on Node.js might not work on Bun. Check before you commit to the migration.

And sometimes infrastructure weeks happen whether you plan them or not. The trick is shipping features anyway, even if they're smaller than you intended.

Next week: probably not touching the build system.

More from Building in Public