DEV Community

Cover image for steamworks-ffi-node: A Steamworks SDK Library for JavaScript Game Frameworks
Artur Khutak
Artur Khutak

Posted on • Edited on

steamworks-ffi-node: A Steamworks SDK Library for JavaScript Game Frameworks

steamworks-ffi-node is an open source and zero-compilation Steamworks SDK wrapper for Node.js, Electron, and JavaScript game frameworks. It covers achievements, leaderboards, cloud saves, Steam Workshop, Steam Input, and cross-platform Steam Overlay — all in TypeScript with no native build tools required.

How It Started

I was building an Electron app for release on Steam. Everything went smoothly until I needed to add Steam integrations: achievements, leaderboards, cloud saves, and Steam Overlay support.

The first thing I found were two existing libraries: greenworks and steamworks.js. I tried both, but both had significant drawbacks.

greenworks disappointed me because of missing Steam integrations, the need to compile the correct library version, and its focus on NW.js.

steamworks.js had poor maintenance, lacked documentation, and had Steam Overlay issues — it only worked on Windows.

With the rise of AI coding agents, I decided to build my own library. That's how steamworks-ffi-node was born — a Node.js wrapper for Steamworks SDK that uses FFI instead of native C++ compilation.


The Problem: Steam SDK and JavaScript Don't Mix Well

Valve provides Steamworks SDK as a set of C++ headers and dynamic libraries (steam_api64.dll, libsteam_api.so, libsteam_api.dylib). To call these functions from
Node.js, there are two classic approaches:

  1. Native Node.js addon in C++ (Node-API / NAN) — compiled separately for each platform and Node version.
  2. FFI (Foreign Function Interface) — call library functions directly from JavaScript, with no compilation step.

greenworks and steamworks.js chose the first approach. steamworks-ffi-node chose the second.


Overview of Existing Libraries

greenworks

greenworks is the oldest library in this space, originally developed by Greenheart Games for their game Game Dev Tycoon, later open-sourced.

Approach: Native C++ addon built with NAN (Native Abstractions for Node.js).

Issues I encountered:

  • Maintained on a best-effort basis
  • No TypeScript typings
  • Supports Steamworks SDK v1.62 but parts of the API remain uncovered
  • High focus on NW.js
  • Last release: v0.22.0 (September 2025), 61 open issues

steamworks.js

steamworks.js is a more modern alternative, built with Rust via NAPI-RS.

What's good: No manual compilation needed, TypeScript typings included, modern API design.

Limitations I hit:

  • Tied to specific Node.js versions via NAPI ABI
  • Significantly limited API coverage
  • Maintainer activity currently absent
  • Last release: v0.4.0 (two years ago on npm), 52 open issues

The Technical Decision: Why FFI Over Native Addons

How Koffi Works

Koffi is an FFI library for Node.js that lets you call functions from native dynamic libraries directly from JavaScript — no C++ code required.

import koffi from 'koffi';
// Load the Steam API library
const lib = koffi.load('steam_api64');
// Declare a function signature
const SteamAPI_Init = lib.func('bool SteamAPI_Init()');
// Call it directly from JavaScript!
const result = SteamAPI_Init();
Enter fullscreen mode Exit fullscreen mode

No Compilation on Install

The FFI approach means the library is pure JavaScript/TypeScript that installs as a regular npm package. No node-gyp, no Visual Studio Build Tools, no Xcode.

Node.js Version Independence

Native addons are tied to the ABI of a specific V8/Node version. Upgrading Electron forces recompilation or waiting for a new release. Koffi FFI calls .so/.dll directly. The Steam binary ABI doesn't change. Upgrading Electron doesn't break the integration.

Easy to Contribute

The entire library is written in TypeScript, so there's no separate compilation step. Adding a new Steamworks function is straightforward.


Tricky Technical Challenges

The Struct Return ABI Problem on Linux

This was the most interesting bug I fixed in version 0.9.3. The Steam Input API returns structures directly from functions.

On Windows (MSVC x64 ABI), small structs are returned via registers RAX/RDX. On Linux (System V x86_64 ABI), small structs up to 16 bytes return in RAX:RDX registers. Large structs (like InputMotionData_t at 40 bytes) return via a hidden pointer passed as the first argument.

My incorrect code passed the buffer as the second argument — the function wrote 40 bytes into ISteamInput , causing memory corruption and segfaults.

// INCORRECT — worked only on Windows
const buf = Buffer.alloc(8);
this.steamLib.func('SteamAPI_ISteamInput_GetDigitalActionData', 'void', 
  ['void*', 'void*', 'uint64', 'uint64'])(iface, buf, handle, actionHandle);
Enter fullscreen mode Exit fullscreen mode

The fix: declare structs through Koffi and use the correct return type. Koffi automatically selects the correct calling convention depending on platform and struct size.

// CORRECT: let Koffi handle the ABI
const InputMotionData_t = koffi.struct('InputMotionData_t', {
rotQuatX: 'float',
rotQuatY: 'float',
rotQuatZ: 'float',
rotQuatW: 'float',
posAccelX: 'float',
posAccelY: 'float',
posAccelZ: 'float',
rotVelX: 'float',
rotVelY: 'float',
rotVelZ: 'float',
});
const GetMotionData = lib.func(
'InputMotionData_t SteamAPI_ISteamInput_GetMotionData(void*, uint64)'
);
Enter fullscreen mode Exit fullscreen mode

Native Steam Overlay for Electron

The standard Steam Overlay (Shift+Tab) doesn't work in Electron games out of the box — Steam doesn't know how to inject its rendering into Chromium.

I implemented native C++ modules for each platform:

  • macOS: Metal rendering via MTKView and CAMetalLayer
  • Windows: OpenGL via Win32 window hooks
  • Linux/SteamOS: OpenGL 3.3 with GLX (tested on Steam Deck Desktop Mode)

This is the only part requiring native compilation, but prebuilt binaries ship via npm.

Steam Overlay integration for Electron applications design


Library Architecture

Each manager is responsible for one section of the Steamworks API, making it easy to add new APIs without modifying existing code.

Key API modules currently supported:


Getting Started

Install the package via npm — no C++ compiler, no Rust toolchain, no native build step required:

npm install steamworks-ffi-node
Enter fullscreen mode Exit fullscreen mode

Then download the Steamworks SDK redistributables and place them at steamworks_sdk/redistributable_bin in your project root. That's the only manual step.

Here's a complete example covering the most common Steam integrations — achievements, stats, cloud saves, leaderboards, rich presence, and the Steam overlay:

import SteamworksSDK, {
  LeaderboardSortMethod,
  LeaderboardDisplayType,
  LeaderboardUploadScoreMethod,
} from "steamworks-ffi-node";

const steam = SteamworksSDK.getInstance();

// Initialize Steamworks SDK — no steam_appid.txt file required
steam.init({ appId: YOUR_STEAM_APP_ID });

// Required for all async Steam callbacks (achievements, leaderboards, cloud)
setInterval(() => steam.runCallbacks(), 1000);

// Get current player info
const { personaName, steamId } = steam.getStatus();
console.log(`Logged in as: ${personaName} (${steamId})`);

// Unlock a Steam achievement
await steam.achievements.unlockAchievement("ACH_FIRST_WIN");

// Read and update a Steam stat
const kills = steam.stats.getStatInt("total_kills") ?? 0;
steam.stats.setStatInt("total_kills", kills + 1);
steam.stats.storeStats(); // Commit to Steam servers

// Save game data to Steam Cloud (syncs across all player devices)
steam.cloud.fileWrite("save.json", Buffer.from(JSON.stringify({ level: 5 })));

// Submit a score to a Steam leaderboard
const board = await steam.leaderboards.findOrCreateLeaderboard(
  "HighScores",
  LeaderboardSortMethod.Descending,
  LeaderboardDisplayType.Numeric
);
if (board) {
  await steam.leaderboards.uploadLeaderboardScore(
    board.handle, 1200, LeaderboardUploadScoreMethod.KeepBest
  );
}

// Show custom status in the Steam friends list
steam.richPresence.setRichPresence("status", "In Game — Level 5");

// Open the Steam overlay from your Electron / Node.js game
steam.overlay.activateGameOverlay("Achievements");
Enter fullscreen mode Exit fullscreen mode

Comparison Table

Feature greenworks steamworks.js steamworks-ffi-node
Approach C++ NAN addon Rust NAPI-RS Koffi FFI
Compilation needed Yes Precompiled No
TypeScript No Yes Yes
Node version lock Yes Yes (NAPI ABI) No
API coverage Partial Limited Extensive
Steam Overlay support only NW.js Windows only (partial Linux) for Electron Win, macOS, Linux for Electron
Active maintenance Best-effort Inactive Active

Games Already Using steamworks-ffi-node


Conclusion

I gained invaluable experience working with the FFI approach, Steamworks SDK, and solving challenges like the Steam Input ABI bug and cross-platform Steam Overlay.

If you're building a game on Electron or another JS framework and need Steam integration, you now have a choice between three approaches:

  • greenworks — for basic functionality if you don't mind C++ compiler dependencies
  • steamworks.js — for limited API needs with a Rust implementation
  • steamworks-ffi-node — for comprehensive API coverage, zero compilation, TypeScript support, and active development

Also if you will plan to use library or face any issues do not hesitate to drop a question or comment here or create a Github issue. Will be happy for any feedback from you!

If this was useful — drop a star on GitHub!

Links:

Top comments (0)