This post was generated by AI and reviewed, edited, and annotated by a human

Some years ago, kartiknair, built a lovely little thing called Noize — a single-page app that plays looping background noise (rain, a café, a medieval town, brown noise) over pixel-art scenes, so you can relax or focus. This week I decided to actually get it running again and put it somewhere permanent. It’s now live at aryamans.me/noize.

to provide some more context, the original project was hosted on noize.ml, and seemed to be a passion project by kartik. i have no idea who he is, but i had ended up building a personal attachment to the app over the years - much of my high school was sppent with this website in the background.

The interesting part isn’t the app — it’s that the app was, for all practical purposes, dead code. It couldn’t be built on any modern machine, half its dependencies were pointing at services that no longer exist, and the audio was streaming off someone else’s account. So this is less “I built an app” and more “I performed an autopsy and then a resurrection.” I did the whole thing with Claude Code as a pair.

The corpse

The first thing I did was try to run it. npm install, npm start, and immediately:

Error: error:0308010C:digital envelope routines::unsupported

If you’ve touched an old JavaScript project on a recent Node, you know this one. The project was a Create React App build on [email protected], which is webpack 4 underneath. Webpack 4 uses MD4 hashing for its module IDs, and MD4 lives in OpenSSL’s legacy provider — which OpenSSL 3 (shipping in modern Node) removed. My machine runs Node 26. So the build reaches for a hash algorithm that no longer exists and dies before it produces a single file.

There’s a well-known band-aid: NODE_OPTIONS=--openssl-legacy-provider, which re-enables the dead algorithm and lets the old build limp along. It works. I chose not to use it, because it keeps you permanently tethered to a stack that’s already deprecated — you’re not fixing the project, you’re just teaching your machine to speak a dead language.

While poking around, the rest of the autopsy turned up more that had quietly rotted:

  • firebase@7 was listed as a dependency but wasn’t imported anywhere in the source. Pure dead weight.
  • Analytics ran through react-ga with a Universal Analytics ID. Google shut UA down in July 2023, so that code had been talking to a corpse for years.
  • The audio wasn’t Firebase at all — it was streaming from hardcoded Backendless CDN URLs pointing at kartiknair’s account. The app only worked as long as someone else’s free tier kept serving my requests.
  • The media loading used webpack’s require(\${isrc}.mp4`)` — a dynamic-require trick that’s a webpack-only feature and won’t survive a move to any other bundler.
  • The git remote still pointed at the original author’s repo.

So it wasn’t just “old.” It was a build that couldn’t build, wired to three dead or borrowed services. Before writing any code I wrote a PLAN.md documenting all of this, because the fix for each finding informs the others — and it’s a lot easier to reason about a resurrection when the whole body is on the table.

The decision: migrate, don’t medicate

The cleanest realization was that a single move fixed almost everything at once. Instead of nursing CRA back to health, I migrated the whole thing to Vite 6 + React 18. Vite runs natively on Node 26 (no legacy-provider incantations), it’s the current default for React SPAs, and — importantly for where I wanted this to live — it emits plain static files that GitHub Pages serves directly.

The migration itself was mostly mechanical:

  • Rewrote package.json: dropped react-scripts, firebase, react-ga, the ancient testing libs, and gh-pages. Added vite and @vitejs/plugin-react. The dependency tree went from a sprawling CRA install to 65 packages, 0 vulnerabilities.
  • Moved public/index.html to the project root (Vite’s convention), dropped the %PUBLIC_URL% placeholders, and added a real module entry point: <script type="module" src="/src/index.jsx">.
  • Swapped ReactDOM.render for React 18’s createRoot wrapped in StrictMode, and deleted the old service worker and test scaffolding.

The one genuinely non-mechanical bit was that webpack require(). Vite doesn’t do dynamic require, so I replaced it with Vite’s own import.meta.glob, which resolves a folder of assets to hashed URLs at build time:

// src/media.js
const modules = import.meta.glob("./images/*.{mp4,gif}", {
  eager: true,
  query: "?url",
  import: "default",
});

export function mediaUrl(isrc, ext) {
  return modules[`${isrc}.${ext}`];
}

The components went from require(\${isrc}.mp4`)tomediaUrl(isrc, “mp4”)`, and Vite now fingerprints all ten background videos (and their GIF fallbacks) into the build output properly. I deliberately didn’t rewrite the class components into hooks — they still work fine under React 18, and rewriting working UI just to chase a style is how you introduce bugs into a resurrection for no reason.

Cutting the borrowed cord: hosting the audio

The migration got it building. It didn’t fix the fact that the actual sound — the whole point of the app — was still streaming off kartiknair’s Backendless account. That’s fragile and it’s not really mine to depend on.

i honestly do not know why the originals were still up - surely backendless would’ve shut it down by now? 🤷‍♂️

I first looked at whether Backendless was something I could just sign up for myself. It’s free to start, but it’s a whole backend-as-a-service platform, which is a comically heavy thing to adopt when all I need is to serve ten static MP3s that never change. So I downloaded all ten files (about 538MB) and went looking for the least-effort permanent home.

The answer turned out to be GitHub Release assets, which are a genuinely underrated free CDN for exactly this case. If you attach files to a release on a public repo, GitHub serves them with no bandwidth charges, up to 2GB per file, and — the part that matters for audio — the download endpoint returns a 302 redirect to storage that honors HTTP range requests (206 Partial Content). Range support is what lets a browser seek and stream a track instead of downloading the whole thing first. I created an audio-v1 release, uploaded the ten MP3s, and pointed the app at them:

`https://github.com/Aryaman73/noize/releases/download/audio-v1/a${i}.mp3`

No account to maintain, no free tier to lapse, no bill. The audio now lives in the same repo as the code that plays it.

massive props to AI for this, i could not think of using github releases as a way to host audio 🧠

Landing it at a custom path

The last question was where to actually host it. My personal site lives at aryamans.me, which is a custom domain on top of aryaman73.github.io (a GitHub Pages user site). I wanted Noize at aryamans.me/noize, and I genuinely wasn’t sure that was possible — I assumed my personal site, being the thing that owns the domain, might swallow every path underneath it.

It turns out GitHub Pages handles this more gracefully than I expected. When you set a custom domain on your user site, that domain automatically extends to your project pages: a repo named noize gets served at customdomain.com/noize, routed purely by repo name. I didn’t have to touch my personal site’s config at all. The two are separate Pages deployments that happen to share a domain, and GitHub routes between them by the repo path.

To deploy, I added a GitHub Actions workflow that builds the Vite app and publishes the dist/ folder to Pages on every push to master. The one Vite setting that makes this work at a sub-path is base: './' in the config — it makes all the asset URLs relative, so the same build works whether it’s served from a domain root or from /noize/. Enabled Pages via the API, pushed, watched the Action go green, and the site came up at both aryamans.me/noize and the aryaman73.github.io/noize fallback.

Wrapping up

While I was in there I also gave it a proper dark mode (system-default with a saved override) and rewrote the About page to credit kartiknair as the original author and be honest that this is a modernized recreation, not my original idea. The art and the sounds are all still theirs and the creators they credited; I mostly did carpentry.

The thing I keep coming back to is how little was actually wrong. A dead app that won’t build on any current machine sounds like a big problem, but the root cause was one removed hash function, and everything downstream — the borrowed audio, the dead analytics, the deprecated bundler — was just accumulated rot that a single migration swept out. Old code isn’t usually broken in some deep way. It’s broken because the ground underneath it moved, and nobody was standing there when it did.

The rebuilt source is on GitHub, and you can go listen to a thunderstorm at aryamans.me/noize.