BASE44DEVS

FIX · PLATFORM · MEDIUM

Base44 Unsupported Dependency Error — Transitive & Node-API Fix

Base44's unsupported dependency error fires when a package your project allows pulls in a transitive dependency the sandbox blocks, or when code reaches a Node-only API like fs, crypto, or a native binding the browser-shaped runtime does not expose. The package installs cleanly because the direct dep was on the allowlist — the failure surfaces later when the bundler walks the import graph or when the offending line first executes in production. Fix the base44 unsupported dependency error by inspecting the dependency tree with npm ls, identifying the offending transitive package or Node-only API, and choosing one of three paths: swap to a browser-compatible alternative such as bcryptjs for bcrypt or sql.js for sqlite3, polyfill the missing Node API with crypto-browserify, or move the unsupported code to an external backend function the sandbox calls over HTTP.

Last verified
2026-05-24
Category
PLATFORM
Difficulty
MODERATE
DIY possible
YES

Why base44 fails on unsupported dependencies

Base44's unsupported dependency error fires when a package your project allows pulls in a transitive dependency the sandbox blocks, or when code reaches a Node-only API like fs, crypto, or a native binding the browser-shaped runtime does not expose. The package installs cleanly because the direct dep was on the allowlist — the failure surfaces later when the bundler walks the import graph or when the offending line first executes in production. Fix the base44 unsupported dependency error by inspecting the dependency tree with npm ls, identifying the offending transitive package or Node-only API, and choosing one of three paths: swap to a browser-compatible alternative such as bcryptjs for bcrypt or sql.js for sqlite3, polyfill the missing Node API with crypto-browserify, or move the unsupported code to an external backend function the sandbox calls over HTTP.

You added a package. The install completed. The build failed with a message about an unsupported dependency or a module that cannot be resolved. Or worse, the build succeeded and a request crashes the moment it touches the affected code path with fs is not defined or crypto.randomBytes is not a function.

This is not the same error as a flat package rejection at install time. The platform allowed the direct dependency. Something inside it — a transitive package, a Node-only API call, a native binary — is what the sandbox refuses. The first-pass allowlist check looks at what you declared. The second-pass build and runtime checks look at what the code actually does. The gap between them is where this error lives.

The pattern repeats across dozens of audits. A team adds bcrypt to hash passwords. Install passes. Build fails because bcrypt ships a native .node binary the sandbox cannot load. Swap to bcryptjs and the next deploy works. A team adds node-rsa for a crypto routine. Install passes. The browser-shaped bundler refuses to resolve crypto because the polyfill is not wired in. Swap to jose and the problem disappears. The errors look mysterious until you see the same shape three times.

What causes the unsupported dependency error in base44

Four distinct root causes produce the same error class. The fix for each is different, so identifying which one you are seeing is the first real step.

1. Transitive dependency on the block list

Your direct dependency is allowed. The package it depends on is not. The Base44 allowlist enforces declared imports, but the bundler resolves the full graph at build time and will refuse a transitive package that violates the policy. The error message names the transitive package — not the direct dependency you installed — which makes the connection hard to see without npm ls.

A common shape: you install feathers-authentication (allowed) and the build fails on bcrypt (blocked). The package author chose bcrypt as a peer for password hashing. You never imported bcrypt directly. The bundler still walks to it and breaks.

2. Node-only API in the browser sandbox

The sandbox is browser-shaped. Many Node globals do not exist there: fs, child_process, cluster, dgram, dns, net, tls, process.binding, require('os'). Even partially-supported modules like crypto, buffer, and stream need explicit polyfills, and the polyfills are not always wired in by default.

A package that calls fs.readFileSync to load a config file at import time will pass install, fail the build, and produce a Module not found: 'fs' error. A package that calls process.binding('util') to detect its environment will fail at runtime with process.binding is not a function. These look like bugs in your code; they are usually bugs in your dependency's assumption about the host.

3. Native binary dependency

Packages with native bindings — sharp, bcrypt, sqlite3, canvas, ffi-napi, node-gyp-built modules — ship a compiled .node file or invoke node-gyp at install time to build one. The sandbox does not run node-gyp and cannot load a .node binary in a browser-shaped runtime. The error here is usually a gyp ERR! build error at install time or a Module did not self-register at runtime.

Sharp is the most common offender for image processing. Bcrypt is the most common for password hashing. Both have pure-JS alternatives (@resvg/resvg-js or browser-image-compression for sharp, bcryptjs for bcrypt) that resolve the problem in a single import swap.

4. ESM and CJS conflict

A package ships only as ESM and the bundler resolves it as CJS, or vice versa. The error is usually require is not defined (CJS code reaching ESM-only output) or Cannot use import statement outside a module (ESM code reaching CJS-only output). Base44's bundler is biased toward ESM for browser-shaped output; older CJS-only packages frequently misfire.

A user on the feedback board described the pattern: install passes, the runtime first-load throws require is not defined, and the workaround was to wrap the import in a dynamic import() rather than a top-level require.

Sources: feedback.base44.com posts on "unsupported dependency", "bcrypt fails", and "sharp not working", Node.js compatibility notes in the Vite docs (vitejs.dev/guide/troubleshooting), docs.base44.com on platform runtime constraints.

How to confirm an unsupported dependency error in base44 (reproduction)

Run these steps in order. Each one rules out a possibility before the next.

  1. Open the build log or browser console and copy the full error verbatim. Note the exact module name, the file path the bundler was resolving, and whether the error fired at install, build, or runtime.
  2. Re-run npm install from the project root. If the install fails with a rejection error, you are in the install-time rejection mode, not this one — see the npm package rejected fix.
  3. If install passes but build or runtime fails, run npm ls <offending-package> from the project root. Note the dependency chain printed. The grand-parent at the top is the package you actually need to replace or contain.
  4. Read the error class. Module not found on a sub-package is a transitive issue. fs is not defined, crypto.randomBytes is not a function, or process.binding errors are Node-API misses. gyp ERR! or .node binary errors are native bindings. require is not defined or Cannot use import statement is an ESM/CJS conflict.
  5. Run npm run build locally and confirm the error reproduces in the production build. The dev server resolves dependencies more permissively than the production build — a problem that only shows in prod is almost always a build-time resolution issue.
  6. If the package is one of the well-known offenders (bcrypt, sharp, sqlite3, canvas, pg-native, node-rsa), the fix is a known swap. Jump straight to the fix section below.

How to fix base44 unsupported dependency error — step-by-step

Apply the fix for the cause you confirmed. Applying all four blindly will leave the project in a broken intermediate state.

Fix for cause 1 — swap the transitive dependency

If npm ls shows that a direct dependency you control depends on the blocked package, look for an alternative version or a fork that does not pull in the offender. For feathers-authentication depending on bcrypt, switching to @feathersjs/authentication-local with an explicit bcryptjs peer dep resolves the issue. The pattern: pick the parent package, identify the swap, install the swap, and remove the parent.

If no clean swap exists, you can sometimes override the transitive version via npm's overrides field in package.json. This forces npm to use a compatible version of the transitive package even when the parent declares a different one.

{
  "overrides": {
    "bcrypt": "npm:bcryptjs@^2.4.3"
  }
}

Whether Base44's bundler honors overrides depends on the platform's package-resolution rules. Test in a non-production project first.

Fix for cause 2 — swap to a browser-compatible alternative

The common swaps are:

// BEFORE — Node-only crypto
import bcrypt from "bcrypt";
const hash = await bcrypt.hash(password, 10);

// AFTER — pure-JS, browser-compatible
import bcrypt from "bcryptjs";
const hash = await bcrypt.hash(password, 10);
// BEFORE — Node-only JWT library
import jwt from "jsonwebtoken";
const token = jwt.sign(payload, secret);

// AFTER — pure-JS JWT for both Node and browser
import { SignJWT } from "jose";
const token = await new SignJWT(payload)
  .setProtectedHeader({ alg: "HS256" })
  .sign(new TextEncoder().encode(secret));
// BEFORE — Node-only image processing
import sharp from "sharp";
const buffer = await sharp(input).resize(800).toBuffer();

// AFTER — WASM-based, works in browser sandbox
import { Resvg } from "@resvg/resvg-js";
const buffer = new Resvg(input, { fitTo: { mode: "width", value: 800 } })
  .render()
  .asPng();
// BEFORE — native SQLite driver
import Database from "better-sqlite3";
const db = new Database("app.db");

// AFTER — WASM SQLite for browser-shaped runtimes
import initSqlJs from "sql.js";
const SQL = await initSqlJs();
const db = new SQL.Database();

The swap table is short because the offenders are predictable. Memorize four or five common ones and most unsupported-dependency errors resolve in under ten minutes.

Fix for cause 3 — polyfill the missing Node API

When no swap exists but the underlying API is pure JavaScript, install the polyfill and wire it into the bundler. Common polyfills:

Node modulePolyfill package
cryptocrypto-browserify
bufferbuffer
streamstream-browserify
pathpath-browserify
utilutil
processprocess

Wiring the polyfill into the bundler usually means adding a resolve.alias or a define block in the build config. Base44 does not expose Vite or webpack config directly, so this step depends on whether the agent will accept and apply a build-config override. Frame the prompt as "add an alias from crypto to crypto-browserify in the bundler resolve config." If the agent declines or the override does not take effect, polyfilling is not viable on this platform and you should jump to the off-platform path.

Polyfills are slow. A crypto-browserify hash is 10-50x slower than the Node-native equivalent. Use this for low-frequency code only — config decoding at startup is fine; per-request password hashing is not.

Fix for cause 4 — resolve the ESM/CJS conflict

For require is not defined errors, the package is CJS-only and the bundler is producing ESM. Wrap the import in a dynamic import():

// BEFORE — top-level require, fails in ESM output
const pkg = require("legacy-cjs-package");

// AFTER — dynamic import that the bundler can handle
const pkg = await import("legacy-cjs-package").then((m) => m.default ?? m);

For Cannot use import statement outside a module errors, the package is ESM-only and the bundler is producing CJS. Check whether the package publishes a CJS build alongside ESM (most do — look at the package's exports field in its package.json). If not, the only fix is finding an alternative or asking the package author to publish a dual build.

Move unsupported code to an external backend function

When no swap, polyfill, or import dance works, the code does not belong in the sandbox. Move it to an external worker exposed over HTTP.

// In your Base44 app — call an external worker instead of running locally
const response = await fetch("https://image-worker.yourdomain.com/resize", {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({ url: inputUrl, width: 800 }),
});
const { resizedUrl } = await response.json();
// On the external worker (Vercel function, Cloudflare Worker, Render service)
import sharp from "sharp"; // Works here — full Node runtime
export async function POST(req: Request) {
  const { url, width } = await req.json();
  const input = await fetch(url).then((r) => r.arrayBuffer());
  const output = await sharp(Buffer.from(input)).resize(width).toBuffer();
  // Upload output to storage, return the URL
  return Response.json({ resizedUrl: await uploadAndReturnUrl(output) });
}

This pattern adds operational surface — one more service to deploy and monitor — but unblocks anything the sandbox refuses. It is also future-proof against the next allowlist tightening, because anything off-platform is independent of Base44's evolving restrictions. The decoupling pattern is the same shape as the vendor-lock-in escape, just for a single unsupported dependency instead of the whole SDK surface.

Pin the swap and document the constraint

Once the swap works, pin the exact version in package.json and add a comment near the import explaining why this package was chosen. The AI agent will re-suggest the original Node-only package on the next nearby prompt unless the constraint is visible in the code itself. This is the same regression pattern documented in the AI agent regression fix — without an in-code signal the agent reverts your decision within days.

// REQUIRED: bcryptjs not bcrypt — the sandbox blocks native bindings.
// Do not switch back to bcrypt even if the agent suggests it.
import bcrypt from "bcryptjs";

How long does it take to fix base44 unsupported dependency error?

Timeline depends on which cause you hit.

  • Transitive dependency swap (cause 1): 15-30 minutes when a clean alternative exists. Longer when you need to fork the parent package or wire up an overrides block and retest.
  • Browser-compatible swap (cause 2): 10-20 minutes for a well-known offender like bcrypt or sharp. The swap is one import change and a rebuild. Most projects with a documented swap path are back online within half an hour.
  • Polyfill (cause 3): 30-90 minutes including testing. Polyfilling requires bundler config access, which on Base44 means working through the agent. Expect 2-4 failed attempts before the override sticks. If the platform refuses the config change, escalate to the off-platform path instead.
  • ESM/CJS resolution (cause 4): 20-45 minutes. Dynamic import() wrappers are usually fast. Finding a dual-build alternative when the package is ESM-only takes longer.
  • Off-platform worker (any cause): 2-6 hours for a first-time deploy. Faster on subsequent unsupported deps because the worker infrastructure is reusable — one Cloudflare Worker can host five different unblock endpoints.

Most teams resolve their first unsupported-dependency error in under an hour. Recurring errors usually mean the agent is re-introducing the same Node-only package on every prompt — fix the in-code comment and the regression stops.

DIY vs hire decision

DIY this if: The offending package is a well-known offender (bcrypt, sharp, sqlite3, canvas, jsonwebtoken, node-fetch), a clean swap exists, and you are comfortable with npm ls, package-lock.json, and editing imports. Most teams solve this themselves once they have seen the pattern once.

Hire help if: You have multiple unsupported-dependency errors stacking up, the polyfill path keeps getting rejected by the platform, or the right answer is an off-platform worker and you do not have prior experience deploying one. The fix-sprint engagement maps the dependency tree, applies the swap or builds the worker, and pins the constraint in code so it does not regress.

If the same error class keeps recurring on different packages, the underlying problem is that the application is reaching for Node-runtime capabilities the sandbox cannot host. At that point the right conversation is not about this dependency but about whether the workload belongs on the platform at all. See the vendor-lock-in fix for the decoupling pattern that precedes a partial migration.

Need help with a complex dependency tree?

For one-off unsupported-dependency errors, our fix-sprint engagement maps the tree, applies the swap, validates the production build, and pins the constraint so the agent does not regress. For projects with multiple stacking unsupported deps or an architectural dependency mismatch, the right shape is a small audit followed by an off-platform worker buildout.

Start a fix sprint for an unsupported dependency error

QUERIES

Frequently asked questions

Q.01What's the difference between an unsupported dependency error and an npm package rejected error in Base44?
A.01

They fire at different points in the build. An npm-package-rejected error happens at install time — you ask the agent or the package manager to add a direct dependency, the platform inspects the allowlist, and the install fails outright before any code touches it. An unsupported dependency error happens later, when the bundler walks the import graph or when the runtime hits a code path. The direct dependency was allowed; a transitive package it pulled in is not, or the package compiles but calls a Node-only API the sandbox does not expose. Reproduction order matters: if install completes but the build or first request fails, you are in the unsupported-dependency mode, not the package-rejection mode. See [the npm package rejected fix](/fix/base44-npm-package-rejected-error) for the install-time case.

Q.02Why does a package that installs successfully still fail at runtime in Base44?
A.02

The Base44 allowlist checks direct dependencies at install but cannot fully predict what the package will do once it executes. Three common surprises: the package imports a transitive dependency that is itself blocked when the bundler resolves it, the package calls Node-specific globals like process.binding or require('fs') that the sandbox does not expose to browser-shaped bundles, or the package ships a native .node binary that needs platform-specific build tooling the sandbox does not run. The install path validates the package.json declaration. The build and runtime paths validate the actual execution. The error you see — Module not found, Cannot resolve, fs is not defined, or a generic unsupported dependency — is the second-pass check finding what the first pass missed.

Q.03How do I find which transitive dependency is breaking the build?
A.03

Run npm ls <suspect-package> from the project root to print the dependency chain that pulled the offending package in. If you don't know the suspect package name yet, read the error message carefully — the bundler usually names the file path and the resolved package, both of which appear in node_modules. Cross-reference that path with package-lock.json to find the parent that depends on it. For deeper trees, npm why <package> and npm ls --all show the full hierarchy. The most common offenders are crypto utilities (bcrypt, node-rsa), file-system helpers (chokidar, glob), image processors (sharp, canvas), and database drivers (pg-native, mysql2 with C-bindings). When the chain ends at a single grand-parent, that grand-parent is your swap target.

Q.04Can I polyfill Node APIs to make a package work in the Base44 sandbox?
A.04

Sometimes, and only for pure-JavaScript APIs. Polyfills exist for crypto (crypto-browserify), buffer (buffer), stream (stream-browserify), path (path-browserify), and util (util). The sandbox bundler may or may not let you wire them in — Base44 does not expose webpack or vite config directly, so polyfill installation often requires asking the agent to add a build-config override and hoping the allowlist permits it. Polyfills do not help when the underlying need is a native binding (sharp, bcrypt, sqlite3, ffi-napi) because the binding is C code compiled for the host OS, not JavaScript. For those, the only fix is swapping to a pure-JS alternative or moving the work off-platform. Polyfills are also slow — a crypto-browserify call is 10-50x slower than the Node-native crypto.

Q.05Why does the same package work locally but fail in Base44 production?
A.05

Your local machine runs the full Node.js runtime with file-system access, native bindings, and unrestricted dependency resolution. Base44's sandbox is a constrained browser-shaped environment that exposes a subset of Node globals and blocks transitive imports the allowlist did not anticipate. A package that imports fs only in an unused code path will work locally — Node never reaches the import — but fail in Base44 because the bundler statically analyzes the import graph at build time and refuses to resolve the missing module. This is why npm install and npm run dev pass locally while a deployment to Base44 fails on the same code. The lesson: test the production build path, not just the dev server, before relying on a new dependency.

Q.06When should I move the unsupported code to a backend function instead of swapping the package?
A.06

When no browser-compatible swap exists, when the polyfill would be too slow, or when the work is genuinely server-side. Image processing with sharp, PDF generation with puppeteer, native database drivers, video encoding with ffmpeg, and machine-learning inference with onnxruntime-node all belong outside the sandbox. The right shape is a small external worker — a Vercel function, a Cloudflare Worker, a Render service, or a Fly.io app — that exposes an HTTP endpoint. Your Base44 app calls the endpoint with the input data and reads the result. This pattern adds operational surface (one more service to deploy and monitor) but unblocks any dependency the sandbox refuses. It also future-proofs against the next allowlist tightening — anything off-platform is independent of Base44's evolving restrictions.

NEXT STEP

Need this fix shipped this week?

Book a free 15-minute call or order a $497 audit. We will respond within one business day.