
@cesdk/node-native
Native Node.js bindings for the IMG.LY Creative Engine, the core of CE.SDK.
A drop-in alternative to @cesdk/node
(WASM) that loads the engine as a platform-native N-API addon. The native
build adds video and audio export, GPU-accelerated rendering, and removes the
WASM 4 GB linear-memory ceiling.
For full Creative Engine documentation see
https://img.ly/docs/cesdk/web/guides/headless/setup_node.
When to choose @cesdk/node-native
Pick @cesdk/node-native if you need any of the following; otherwise stay on
@cesdk/node (WASM), which has wider platform coverage:
- Video and audio export (MP4 / H.264 via VideoToolbox on macOS, GStreamer
on Linux).
- Working memory above ~4 GB. WASM linear memory is capped; native uses
the full process address space.
- Hardware-accelerated rendering for image export on Metal (macOS) or EGL
(Linux). CPU fallback is automatic when no GPU is available.
For raw call overhead the native addon also avoids the WASM↔JS boundary, but
the practical impact is workload-dependent — measure on your own scenes
before relying on a specific multiplier.
Supported platforms
| macOS | arm64 | ≥ 20 | macOS 12 (Monterey) |
| macOS | x64 | ≥ 20 | macOS 12 (Monterey) |
| Linux | x64 | ≥ 20 | glibc ≥ 2.39 (Ubuntu 24.04, Debian 13, RHEL 10) |
Not supported in this release:
- Windows
- Linux
arm64
- musl-based distributions (Alpine, etc.)
- AWS Lambda Node.js runtimes (AL2 / AL2023 ship glibc 2.26 / 2.34 — use
@cesdk/node for serverless until a manylinux build lands)
npm install @cesdk/node-native succeeds on unsupported hosts (the main
package itself has no platform filter), but the binary-carrying sibling is
filtered out by npm's os / cpu / libc constraints and the first
require('@cesdk/node-native') throws a clear error pointing at
@cesdk/node (WASM). This is intentional so that conditional installs in
Dockerfiles and CI don't fail at install time.
Installation
npm install @cesdk/node-native
npm install @cesdk/node-native@dev
Usage
ES Modules:
import CreativeEngine from '@cesdk/node-native';
const engine = await CreativeEngine.init({ license: 'YOUR_LICENSE_KEY' });
const scene = engine.scene.create();
const page = engine.block.create('page');
engine.block.appendChild(scene, page);
engine.block.setWidth(page, 1920);
engine.block.setHeight(page, 1080);
const png = await engine.block.export(page, 'image/png', {});
engine.dispose();
CommonJS:
const CreativeEngine = require('@cesdk/node-native');
CreativeEngine.init({ license: 'YOUR_LICENSE_KEY' }).then(async (engine) => {
engine.dispose();
});
Always call engine.dispose() when done. The engine runs an internal
update timer (driving rendering and export), so until you dispose it the
Node.js event loop stays alive and the process will not exit on its own — a
short-lived script or a Lambda handler that skips dispose() will appear to
hang until timeout.
License & evaluation mode
The CreativeEditor SDK is a commercial product. Purchase a license at
https://img.ly/pricing.
You can run the engine in evaluation mode by passing an empty string as
the license key. In evaluation mode the engine prints an IMG.LY banner on
stderr and watermarks output. There are three equivalent ways to enter it:
await CreativeEngine.init({ license: '' });
const engine = await CreativeEngine.init();
engine.editor.unlockWithLicense('');
engine.editor.getActiveLicense();
Linux runtime dependencies
The Linux platform package statically links libc++ / libc++abi /
libstdc++ into the addon, so a stock Ubuntu 24.04 / Debian 13 install
needs only the engine's runtime dependencies. On ubuntu:24.04 and
debian:13 the relevant packages are present by default; minimal base
images (slim, distroless) need them installed explicitly.
Image / scene / archive workflows depend on the standard graphics
stack:
apt-get install -y \
libcurl4 \
libssl3 \
libfontconfig1 \
libfreetype6 \
libuuid1 \
libxcb1 \
libgl1 \
libegl1
Video and audio export additionally needs GStreamer 1.x plus the
common plugin sets:
apt-get install -y \
libgstreamer1.0-0 \
gstreamer1.0-plugins-base \
gstreamer1.0-plugins-good \
gstreamer1.0-plugins-bad \
gstreamer1.0-plugins-ugly \
gstreamer1.0-libav \
gstreamer1.0-gl
A future release will offer a self-contained GStreamer bundle shipped
inside the Linux platform package; until then, system GStreamer is
required.
Supported Docker base images
The Linux platform package ships with a glibc ≥ 2.39 floor (the
libc: ["glibc"] npm filter blocks Alpine/musl at install time;
older-glibc systems install successfully but fail at first
require('@cesdk/node-native') with version 'GLIBC_2.XX' not found).
ubuntu:24.04 (and :24.04-slim) | yes | 2.39 | Reference image |
debian:13 (trixie, and :13-slim) | yes | 2.41 | |
node:22-trixie | yes | 2.41 | |
gcr.io/distroless/nodejs22-debian13 | yes | 2.41 | Recommended distroless |
node:22-alpine | no | musl | Filtered at install |
amazonlinux:2 / :2023 | no | 2.26 / 2.34 | Below floor; also AWS Lambda base |
public.ecr.aws/lambda/nodejs:22 | no | 2.34 | Below floor; see Lambda note below |
Use @cesdk/node (WASM) on every "no" row above.
AWS Lambda
The Linux platform package targets glibc 2.39+; AWS Lambda's Amazon Linux
2 / 2023 base images ship glibc 2.26 / 2.34, both below the floor. A
Lambda function that does require('@cesdk/node-native') will fail at
load with GLIBC_2.38 not found. Use @cesdk/node (WASM) on Lambda.
The apps/cesdk_web_examples/cookbooks-aws-lambda/ cookbook documents
the WASM-on-Lambda path.
Bundlers (webpack / esbuild / Next.js / Vite)
@cesdk/node-native ships a native .node binary that bundlers cannot
inline. Mark the package external and have the bundler emit the
.node + the assets/ resource bundle into the output as-is.
webpack / Next.js (server-side / API routes)
module.exports = {
webpack(config, { isServer }) {
if (isServer) {
config.externals = [
...(config.externals ?? []),
'@cesdk/node-native',
'@cesdk/node-native-darwin-arm64',
'@cesdk/node-native-darwin-x64',
'@cesdk/node-native-linux-x64',
];
}
return config;
},
experimental: {
serverComponentsExternalPackages: ['@cesdk/node-native'],
},
};
esbuild
await esbuild.build({
platform: 'node',
external: [
'@cesdk/node-native',
'@cesdk/node-native-darwin-arm64',
'@cesdk/node-native-darwin-x64',
'@cesdk/node-native-linux-x64',
],
});
Vite (server / SSR)
export default defineConfig({
ssr: {
external: ['@cesdk/node-native'],
noExternal: [],
},
});
In every bundler the engine resource bundle (@cesdk/node-native/assets/)
must reach the runtime layout intact. Either keep node_modules/ next to
the bundle output, or copy node_modules/@cesdk/node-native/assets/
into the deploy artifact and point CreativeEngine.init({ baseURL }) at
that location explicitly.
Yarn Plug'n'Play
Yarn PnP virtualises the node_modules graph through .pnp.cjs; the
default mode keeps optional dependencies inside the zip cache, which
breaks dlopen against the platform sibling package's .node because
the dynamic linker cannot read into a zip filesystem. The fix is to opt
the platform package out of PnP zipping by declaring it unplugged:
nodeLinker: pnp
pnpEnableInlining: false
// package.json
{
"dependencies": { "@cesdk/node-native": "^1.78.0-nightly.20260701" },
"dependenciesMeta": {
"@cesdk/node-native-darwin-arm64": { "unplugged": true },
"@cesdk/node-native-darwin-x64": { "unplugged": true },
"@cesdk/node-native-linux-x64": { "unplugged": true }
}
}
The unplugged setting causes Yarn to extract the matching platform
sibling into .yarn/unplugged/, where the .node resolves to a real
filesystem path that dlopen can consume. The nodeLinker: node-modules
mode (Yarn classic compatibility) doesn't need this — it materialises a
real node_modules/ tree on disk and works out of the box.
Migration from @cesdk/node
The Block, Scene, Editor, Asset, Event, and Variable APIs are the same. For
the vast majority of code paths, @cesdk/node-native is a drop-in
replacement: change the import and run.
- import CreativeEngine from '@cesdk/node';
+ import CreativeEngine from '@cesdk/node-native';
Binary-returning methods (block.export, block.exportVideo,
block.exportAudio, block.saveToArchive, scene.saveToArchive) return
Blob on both packages. block.export() accepts both the WASM-shaped
(block, { mimeType, ... }) and the positional (block, mimeType, options)
signatures, so existing call sites don't need rewriting.
Default asset-source helpers (engine.asset.addDefaultAssetSources,
engine.asset.addDemoAssetSources) are available with the same options
(baseURL, excludeAssetSourceIds, sceneMode).
Intentional divergence
engine.update() is public on @cesdk/node-native (the wrapper pumps
frames internally during exports; you only need it if you're driving
frame-perfect work manually). On WASM it's private — the browser's
animation frame drives it.
Swapping in for tooling that pins @cesdk/node
For test runs or third-party packages that import @cesdk/node by literal
name (e.g. internal importers/exporters), point your bundler/test runner's
module alias at @cesdk/node-native — its export shape matches @cesdk/node,
so no call sites need to change:
resolve: { alias: { '@cesdk/node': '@cesdk/node-native' } }
In application code, prefer importing @cesdk/node-native directly.
Video export
@cesdk/node-native is the first IMG.LY-distributed Node binding to expose
video export. The two known foot-guns:
-
options.duration defaults to 0 for parity with the WASM
@cesdk/node package. The encoder treats 0 as "render a single frame",
which is rarely the intended outcome. Always pass an explicit duration:
await engine.block.exportVideo(page, 'video/mp4', onProgress, {
duration: 3.0,
framerate: 30,
});
-
Scenes loaded from an archive must wait for AV resources before
exportVideo can make progress; otherwise the encoder spins on resources
that haven't been fetched and the progress callback never fires. Pass
waitForResources: true when loading a video scene whose assets aren't
already in memory, or follow up with engine.block.forceLoadAVResource
(or forceLoadResources) on each video-fill block before calling
exportVideo:
const scene = await engine.scene.loadFromArchiveURL(file_url, {
waitForResources: true,
});
const [page] = engine.block.findByType('page');
const mp4 = await engine.block.exportVideo(page, 'video/mp4', onProgress, {
duration: engine.block.getTotalSceneDuration(scene),
framerate: 30,
});
The wrapper pumps update() internally during init(), so unlike
@cesdk/node (WASM), you do not need to drive a setInterval pump
loop around exportVideo. The export awaits naturally. Only call
engine.update() directly if you're driving frame-perfect work outside
of the built-in export path.
A runnable end-to-end example is in examples/video-export-3s.js of the
source repository.
Cross-platform lockfiles (CI pitfalls)
@cesdk/node-native ships a different .node per platform via npm's
optionalDependencies (the same pattern as esbuild, sharp, etc.). One
rough edge to call out:
npm ci on CI using a lockfile generated on a different architecture
succeeds at install but fails with ERR_DLOPEN_FAILED at runtime.
Recommended mitigations (pick one):
npm install --include=optional @cesdk/node-native on CI. Re-resolves
optional dependencies for the actual host. Slightly slower than npm ci,
robust.
- Generate the lockfile on the same OS/arch as CI (a
node:22 Docker
image for Linux consumers).
- Per-platform lockfiles. Over-engineering for most projects.
Docker images that bake @cesdk/node-native into a final artifact should
always install inside the target image, never copy node_modules/ from a
different-arch host.
Advanced: override the binary path
For air-gapped builds or custom distributions, set
CESDK_NATIVE_BINARY_PATH to an absolute path to the .node binary. The
loader uses it directly, bypassing require.resolve:
CESDK_NATIVE_BINARY_PATH=/opt/cesdk/cesdk_native.node node app.js
Set DEBUG=cesdk:native (or CESDK_DEBUG=1) to log loader resolution steps
when triaging install issues.
Skipping the integrity check (cold-start-sensitive deploys)
On first init() the loader streams a SHA-256 over the ~32 MB .node and
compares it to a checksum sidecar — a corruption/truncation guard (it is not
anti-substitution; that's npm provenance). This adds a few tens of milliseconds
to each cold start. For latency-sensitive serverless cold paths (e.g. AWS Lambda)
where the artifact is already integrity-checked by the platform, you can skip it:
CESDK_SKIP_BINARY_VERIFY=1 node app.js
Leave verification on by default; only opt out when cold-start latency matters
and the binary's integrity is guaranteed by other means.
Known limitations
- IMG.LY banner and engine diagnostics on stderr. The native engine
prints a one-time license banner and occasional engine logs (e.g.
EventSubscriptionService::initEventCallbacks errorStateChanged) to
stderr. Capture or redirect stderr (node app.js 2>/dev/null,
child_process.spawn(..., { stdio: ['inherit', 'inherit', 'ignore'] }))
if it interferes with machine-readable output. A setLogLevel API is
tracked for a future release.
- macOS binaries are not code-signed. Plain
node doesn't enforce
signatures on dlopen, so the addon loads out of the box for normal
Node.js usage. Electron apps that notarize the containing app must sign
the nested cesdk_native.node with their own Developer ID in their
afterSign hook before notarization. Hosts that enforce strict library
validation (some Electron variants, hardened runtime CI runners) will
reject the unsigned binary; full Developer ID + notarization on our side
is tracked as a follow-up.
- Per-block error inspection. The engine emits a
errorStateChanged
log line when a block transitions to an error state (e.g. a video-fill
resource fails to load). Read the per-block state via
engine.block.getState(blockId) after update() to inspect the failure;
a top-level error subscriber is tracked as a follow-up.
Troubleshooting
Failed to load the @cesdk/node-native addon for ...
Most commonly, npm skipped optionalDependencies during install. Reinstall:
npm install --include=optional @cesdk/node-native
For pnpm and Yarn, regenerate the lockfile on the target platform/arch
(easiest via a Docker build), or use per-platform lockfiles. Neither
package manager has a direct equivalent of npm's --include=optional.
Linux: version 'GLIBC_2.XX' not found
This build targets glibc ≥ 2.39 — see "Supported platforms". Stay on
@cesdk/node (WASM) for older systems until a manylinux build lands.
macOS: library load disallowed by system policy
The published binary is not code-signed. npm install doesn't add the
com.apple.quarantine xattr so this error doesn't show up there, but if
you obtained the package via a browser download or a sideloaded zip, the
quarantine flag will block dlopen. Strip it with:
xattr -d com.apple.quarantine node_modules/@cesdk/node-native-*/cesdk_native.node
License
The CreativeEditor SDK is a commercial product. Purchase a license at
https://img.ly/pricing.