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 –
- They are platform-specific ELF/Mach-O/PE shared objects, not JavaScript.
- Node.js loads them via
dlopenat runtime from a filesystem path — a path insidenode_modules, which does not exist on the end user's machine. - esbuild marks all
*.nodeimports as external and leaves the original filesystem path in the bundle. The binary builds successfully, but crashes withMODULE_NOT_FOUNDthe 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
| Package | What it does | Pure-JS alternative |
|---|---|---|
argon2 | Argon2 password hashing | argon2-wasm-pro, hash-wasm |
bcrypt | bcrypt password hashing | bcryptjs (drop-in) |
sodium-native | libsodium bindings | libsodium-wrappers |
Databases and storage
| Package | What it does | Pure-JS alternative |
|---|---|---|
better-sqlite3 | SQLite driver | sql.js (WASM), or avoid local DB entirely |
sqlite3 | SQLite driver | Same as above |
leveldown | LevelDB bindings | level with memory-level backend |
lmdb | LMDB bindings | No direct equivalent; use a pure-JS in-memory store |
Serialization and parsing
| Package | What it does | Pure-JS alternative |
|---|---|---|
protobufjs with native acceleration | Protocol Buffers | Use protobufjs without the optional native path — the pure-JS fallback is automatic |
@grpc/grpc-js | gRPC (used internally by terrably) | Already bundled by terrably — do not add it as a direct dependency |
Compression
| Package | What it does | Pure-JS alternative |
|---|---|---|
zstd / node-zstd | Zstandard compression | fzstd (WASM) |
lz4 | LZ4 compression | lz4js |
Miscellaneous
| Package | What it does | Pure-JS alternative |
|---|---|---|
fsevents | macOS 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-features | CPU feature detection | Use os.cpus() or hard-code the target architecture |
re2 | Google RE2 regex engine | Built-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 –
| Package | Spawned binary | Notes |
|---|---|---|
esbuild | esbuild / esbuild.exe | Used for build tooling only — should never be a runtime dependency of your provider |
@tailwindcss/oxide | Rust-based CSS engine | Not applicable to provider logic |
puppeteer | Chromium executable | Not 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 argon2Once you find the parent package, check whether it –
- Offers an optional native path with a pure-JS fallback (e.g.
protobufjs) — in which case simply don't install the optional native accelerator. - Exports a separate pure-JS package (e.g.
bcrypt→bcryptjs). - 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