terrably
Development

Native addons

How native .node addons are handled differently by the Node.js SEA and Bun build paths.

The constraint — Node.js only

When building with Node.js, terrably produces a Single Executable Application (SEA) – your provider's JavaScript and all of its dependencies are bundled by esbuild and injected into a Node.js executable at compile time.

Native addons (.node files) are compiled C/C++ shared libraries. They cannot be embedded into a SEA binary because –

  1. They are platform-specific ELF/Mach-O/PE shared objects, not JavaScript.
  2. Node.js loads them via dlopen at runtime from a filesystem path — a path inside node_modules, which does not exist on the end user's machine.
  3. esbuild marks all *.node imports as external and leaves the original filesystem path in the bundle. The binary builds successfully, but crashes with MODULE_NOT_FOUND the first time Terraform invokes it.

terrably catches this before the compile step by scanning esbuild's metafile for unresolved non-builtin imports. If any are found, the build fails immediately with a list of the offending packages –

✗ Native addons are not supported in terrably builds.
  The following imports cannot be bundled into a Node.js Single Executable Application –
    • argon2  (imported by dist/src/main.js)
    • @napi-rs/canvas  (imported by dist/src/main.js)

  To fix this, replace each native dependency with a pure-JS alternative.
  Alternatively, check `pnpm why <package>` to find what pulls them in.

Bun — native addons work

When building with Bun, bun build --compile embeds native .node addons directly into the output binary. No changes to your dependency tree are needed.

If your provider uses a native addon and portability across platforms matters, keep in mind that the addon must be compiled for each target OS/architecture separately — cross-compilation via --target will produce a binary for the right platform, but the addon inside it must also have been compiled for that platform. In practice this means building each target binary on a matching native runner (or in a matching Docker image) rather than purely cross-compiling from one machine.

Pure-JS alternatives (Node.js)

If you need to support the Node.js build path, replace native packages with their pure-JS equivalents.

Cryptography

PackageWhat it doesPure-JS alternative
argon2Argon2 password hashingargon2-wasm-pro, hash-wasm
bcryptbcrypt password hashingbcryptjs (drop-in)
sodium-nativelibsodium bindingslibsodium-wrappers

Databases and storage

PackageWhat it doesPure-JS alternative
better-sqlite3SQLite driversql.js (WASM), or avoid local DB entirely
sqlite3SQLite driverSame as above
leveldownLevelDB bindingslevel with memory-level backend
lmdbLMDB bindingsNo direct equivalent; use a pure-JS in-memory store

Serialization and parsing

PackageWhat it doesPure-JS alternative
protobufjs with native accelerationProtocol BuffersUse protobufjs without the optional native path — the pure-JS fallback is automatic
@grpc/grpc-jsgRPC (used internally by terrably)Already bundled by terrably — do not add it as a direct dependency

Compression

PackageWhat it doesPure-JS alternative
zstd / node-zstdZstandard compressionfzstd (WASM)
lz4LZ4 compressionlz4js

Miscellaneous

PackageWhat it doesPure-JS alternative
fseventsmacOS file-system events (optional dep of many watchers)Usually an optional peer dep — it won't appear unless you install a watcher package, which you shouldn't need in a provider
cpu-featuresCPU feature detectionUse os.cpus() or hard-code the target architecture
re2Google RE2 regex engineBuilt-in RegExp (V8 regex, linear-time not guaranteed but sufficient for provider logic)

Spawned native executables (both runtimes)

A handful of popular packages ship a platform binary that is extracted from node_modules and invoked via child_process.spawn — not a .node addon at all. Neither build path can embed these, and terrably cannot detect them at build time.

Well-known examples –

PackageSpawned binaryNotes
esbuildesbuild / esbuild.exeUsed for build tooling only — should never be a runtime dependency of your provider
@tailwindcss/oxideRust-based CSS engineNot applicable to provider logic
puppeteerChromium executableNot applicable to provider logic

If you accidentally add one of these as a runtime dependency, your provider will build successfully but crash at runtime with a spawn ENOENT or similar error when the package tries to locate its binary. Check your dependency tree with –

pnpm why <suspect-package>

How to diagnose a transitive dependency (Node.js)

If terrably build fails and you didn't intentionally add a native package, the addon is a transitive dependency of something you did add. Find the culprit –

# List all packages that depend on the named native package
pnpm why <native-package-name>

# Alternatively, trace from the package name in the error message
# e.g. "• argon2 (imported by dist/src/main.js)" → pnpm why argon2
pnpm why argon2

Once you find the parent package, check whether it –

  1. Offers an optional native path with a pure-JS fallback (e.g. protobufjs) — in which case simply don't install the optional native accelerator.
  2. Exports a separate pure-JS package (e.g. bcryptbcryptjs).
  3. Has no alternative — in which case switch to the Bun build path, or restructure your provider logic to avoid that dependency entirely.

Last updated on

On this page