Modernizing Steem.js — Day 1: Building a Safety Net & Modern Tooling
Welcome to a 7-part series documenting how I modernized Steem.js into a fully isomorphic library — one that runs unchanged on Node, browsers, edge functions (Cloudflare Workers / Vercel Edge), and Deno. Over the next six days I'll walk through each engineering phase, and on day seven I'll ship the final release.

Today is Phase 1: the foundation — the safety net and the modern build tooling that everything else stands on.
Why start with a safety net?
Steem.js does real cryptography: it derives keys, signs transactions, and encrypts memos. If a "modernization" changes even a single byte of a signature or a serialized transaction, every existing account and transaction breaks. That's not a bug you can patch later — it's a silent disaster.
So before touching any crypto, I froze the current behavior into golden vectors.
Golden-vector regression gate
test/golden.test.js records byte-exact outputs from the existing library and asserts them forever after:
// Frozen forever — any later change must reproduce these exactly
const wifActive = '5HsARJZiSjTxTQhjbAeZgD1KLhqUvewkfWWPAMBiCTZvhBvL1Qp';
const postingPub = 'STM7Fbx298R8Dnk1VUbNaKxwHy24rdKsP6Z8g64mJdBAF4M35jBGr';
const signature = '206ae22f1fcd2d7e29410d7411a07c00a20d76f94ec6a78fed045d150f44baff…';
const txHex = 'd2042e16000080009265010005616c69636503626f620d746573742d7065726d6c696e6b102700';
They're nobody's real keys — they're synthetic test fixtures.
These cover WIFs, public keys, a canonical signature, serialized transaction hex, and an encrypted-memo roundtrip. Every single phase after this had to keep these green. It's the difference between confident refactoring and crossing your fingers.
Out with webpack 4 + Babel, in with tsup + Vitest
The old build was webpack 4 + Babel producing a CommonJS lib/ and a hand-tuned browser bundle. Modern bundlers (Vite, esbuild) expect proper ESM with an exports map — the old setup gave them neither.
The new toolchain:
- tsup (esbuild) produces four outputs from one source:
dist/index.mjs— ESMdist/index.js— CommonJSdist/steem.min.js— minified IIFE (globalsteem) for<script>/ CDN usersdist/index.d.ts— TypeScript types
- Vitest replaces
babel-node+ mocha — native ESM, and it handles the mixed ESM/CJS source plus ESM-only@noblepackages without ceremony.
A real exports map
{
"type": "commonjs",
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.mjs",
"require": "./dist/index.js"
}
},
"engines": { "node": ">=18" }
}
This is what lets import steem from '@steemit/steem-js' and const steem = require('@steemit/steem-js') both resolve correctly, and what lets Vite tree-shake the package instead of choking on it.
The result
By the end of Phase 1, nothing about how you use the library changed — but the project now builds with modern tooling, ships types, and is guarded by a byte-exact regression gate. That gate made every later phase safe.
Tomorrow: Phase 2 — replacing the entire crypto stack with @noble, the riskiest and most important phase.
Links
- 🛠️ Code (fork): https://github.com/blazeapps007/steem-js (
BlazeDevelopmentbranch) - 📖 Documentation: https://blazeapps007.github.io/steem-js/
Support Secure Steem Development
If you value proactive engineering, UX polish, and performance optimizations for the STEEM ecosystem, please consider supporting my witness: blaze.apps
🗳️ Vote Here:
Vote for blaze.apps Witness