One Dev's Journey Through Strapi, Railway, and Cloudinary

In theory, modern development tools are designed to streamline our workflow—automating deployments, managing dependencies, and simplifying integrations between our favorite platforms. In practice, however, those same tools sometimes introduce subtle, exasperating hurdles.

Header
Steve Jackson

Steve Jackson

Chief Data Officer

Steve has over 20 years experience with getting the most out of data platforms having made his clients 100s of millions in cost savings or sales directly attributable to his work. For the last 5 years he has been building an AI driven travel SaaS and vibe coding his way through all kinds of software development hell!

Platform Friction Debt


Introduction

I recently spent several days wrestling with the integration of Strapi (a headless CMS), Railway (a cloud hosting platform), and Cloudinary (a cloud-based media service). What began as a straightforward setup quickly evolved into an odyssey riddled with native module issues, environment variable misconfigurations, and platform-specific quirks.


The Setup: A Dream Stack (Until It Wasn’t)

I set out to build a modern CMS backend using the latest versions of everything:

  • Strapi (v5.x, the latest at the time)
  • Node.js (latest LTS via NVM)
  • Railway for hosting and PostgreSQL
  • Cloudinary for media uploads

The goal was simple: stay current, avoid tech debt, and keep the stack forward-compatible.

But very quickly, I hit a wall.


One Dev's Journey Through Strapi, Railway, and Cloudinary

The Pain Points

Native Module Mayhem with sharp

The first major obstacle came from the sharp module — a native dependency that powers image resizing in Strapi’s upload plugin.

On Apple Silicon with Node 20+, sharp repeatedly failed to install or load, throwing errors like: symbol not found in flat namespace ‘_ZTVN4vips7VOptionE’

I followed every solution:

  • Rebuilt sharp with --platform and --arch
  • Used npm rebuild sharp --unsafe-perm
  • Copied working vendor/ folders
  • Tried installing from mirrors and direct .dylib libraries

Nothing worked.

The irony? I was using Cloudinary, so I didn’t even need sharp. But Strapi still expected it and crashed without it.

After days of frustration, I gave up on using the latest Strapi and Node.


The Rollback That Worked

In the end, I reverted to a version I knew worked:

  • Strapi v4.25.10
  • Node.js v18.13.0 (via NVM)

And surprise — it worked immediately.
No more sharp errors, no more dynamic library complaints, no native module rebuild drama. We’re not on the latest stack but at least we’re up and running with a headless CMS.


PostgreSQL Pain: Environment Variable Limbo

Next came the Railway and PostgreSQL side of things.

I connected Strapi to a Railway-hosted Postgres database, only to be greeted with:

password authentication failed for user “postgres”

The issue? A subtle mismatch between:

  • POSTGRES_PASSWORD in the Postgres service
  • PGPASSWORD in the Strapi service
  • And escaping issues in DATABASE_URL

Even when all values looked identical, authentication failed—until I finally provisioned a completely new PostgreSQL instance, reset the variables, and cleanly linked it to Strapi.

That fixed it instantly.


Cloudinary Misadventures

Cloudinary uploads worked beautifully — images appeared in the Cloudinary dashboard, and URLs were correct. But thumbnails in Strapi’s admin UI were broken.

The culprit? Strapi’s admin was trying to load formats (e.g., thumbnail, small) from sharp, which was disabled. Since those didn’t exist, previews failed.

The fix?

// config/plugins.js

module.exports = ({ env }) => ({
  upload: {
    config: {
      provider: 'cloudinary',
      providerOptions: {
        cloud_name: env('CLOUDINARY_NAME'),
        api_key: env('CLOUDINARY_KEY'),
        api_secret: env('CLOUDINARY_SECRET'),
      },
      breakpoints: false, // disables sharp resizing
    },
  },
});

Previews now load directly from Cloudinary URLs.

🧾 What Is Platform Friction Debt?

While “tech debt” typically refers to compromises in code, this was something different.

This was platform friction debt — the cost of using modern tools with hidden assumptions, incomplete documentation, and fragile integrations.

Problem 1

  • Area: Sharp errors
  • Cause: Native module complexity on Apple Silicon
  • Debt type: Toolchain fragility

Problem 2

  • Area: Password mismatches
  • Cause: Escaped env vars, multiple variable sources
  • Debt type: Config integration debt

Problem 3

  • Area: Image preview fails
  • Cause: Admin expecting sharp breakpoints
  • Debt type: Plugin assumption debt

Problem 4

  • Area: Native build conflicts
  • Cause: Global vs local Node versions
  • Debt type: Environment debt

Problem 5

  • Area: Cloud deployments
  • Cause: Railway variable propagation confusion
  • Debt type: Platform abstraction debt

A Note to the Strapi Team

If you’re reading this: please make it easier to disable sharp for use cases where external providers like Cloudinary are being used. Relying on native modules when they’re not needed creates unnecessary pain — especially on non-Intel architectures.

Even a simple useSharp: false flag in config would save developers hours and in this case would allow me to use the latest version of your stack.

Final Takeaways

  • Native modules like sharp are fragile — use external providers where possible.
  • Railway makes things easier, but you must watch your environment variables like a hawk.
  • Strapi is powerful — but keep versioning under control and know when to downgrade when you have issues with things like sharp.

If you’re in the middle of a similar integration hell — you’re not alone. Platform friction debt is real. Hopefully this post saves you a few hours of painful trial and error.

One Dev's Journey Through Strapi, Railway, and Cloudinary