EmulatorJS

Self-hosted in-browser retro game emulator

Game Engines & Frameworks ★ 4.2k stars Easy setup GPL-3.0

EmulatorJS is a self-hostable, browser-based emulator front-end supporting dozens of retro consoles. It runs entirely client-side and can be deployed as a static site to play ROMs in any modern browser.

Key features

  • In-browser multi-console emulation
  • Static site deployment
  • Dozens of supported systems
  • Save states in browser

Pros & cons

Strengths

  • Pure client-side
  • Easy to embed

Trade-offs

  • No library management
  • ROMs not included

EmulatorJS replaces

Last reviewed Sep 13, 2026 · 856 words

There is no server to run. EmulatorJS is a folder of JavaScript and WebAssembly cores that any static web host can serve, and every frame of emulation happens in the visitor's browser. That is its strength and the source of every confusion about it: people arrive expecting a Plex for games and find a library you drop into an HTML page. Read this as "how a self-hoster uses EmulatorJS" rather than "how to install it", because the second question is a cp -r and the first is where the decisions are.

What the folder gives you

EmulatorJS bundles RetroArch-derived libretro cores compiled to WebAssembly: NES, SNES, Game Boy family, Nintendo 64, Nintendo DS, Sega systems, PlayStation 1, arcade via MAME and FinalBurn Neo, and more, several dozen systems in all. On top sits a player interface with a virtual gamepad for touch screens, gamepad API support for real controllers, save states, cheats, shaders and netplay. GPL-3.0, 4,141 stars, in development since 2020.

The minimum page is short. The embed needs a container element, a few globals, and the loader script:

<div id="game"></div>
<script>
  EJS_player = "#game";
  EJS_core = "snes";
  EJS_gameUrl = "/roms/game.sfc";
  EJS_pathtodata = "/emulatorjs/data/";
</script>
<script src="/emulatorjs/data/loader.js"></script>

EJS_pathtodata is the setting that matters for self-hosting. By default many tutorials point it at the project's CDN, which means your "self-hosted" emulator fetches its cores from someone else's server on every load. Download the data folder from the releases and serve it yourself; it is a few hundred megabytes with all cores included, and it makes the whole thing work offline on a LAN with no outbound traffic.

Serving it: static host, or the linuxserver image

Any web server works. A Caddy file-server block, an nginx root, or the same host that serves your other static sites. The one requirement worth knowing is that some cores want multi-threading, which browsers only allow when the page is served with the cross-origin isolation headers (Cross-Origin-Opener-Policy: same-origin and Cross-Origin-Embedder-Policy: require-corp). Without them, everything still runs, just single-threaded, which is fine for 8-bit and 16-bit systems and noticeable on PlayStation and N64.

The Docker route most people find is the linuxserver.io emulatorjs image. It adds a small management interface on port 3000 for scanning a ROM folder and generating the launcher pages, and serves the resulting site on port 80. It is convenient, and it is also where the catalogue's "no library management" caveat comes from: the manager is a build step, not a browsing experience. There is no metadata, no cover art scraping to speak of, no per-user anything. It generates a menu and gets out of the way.

Saves live in the browser, and that is a trap

Save states and in-game saves go to the browser's IndexedDB for the page's origin. Clear site data, switch browsers, open the same page from a different hostname, or let Safari's storage eviction run, and the saves are gone. For a casual afternoon this is fine. For a 60-hour RPG it is a disaster waiting on a cache clear. EmulatorJS offers export and import of saves through its menu, so the workable habit is exporting after each session, which nobody keeps up. This is the strongest argument for a front-end that stores saves server-side.

RomM is the better front-end once you have a library

RomM embeds EmulatorJS as its browser player and adds what EmulatorJS deliberately lacks: a scanned library with IGDB metadata and cover art, multiple users, and saves and states stored on the server so they follow you between devices. If you have more than a shelf's worth of games, RomM is the safer pick and EmulatorJS becomes an implementation detail you never see. Gaseous Server fills a similar role. Bare EmulatorJS is the right choice for embedding one or two games in a personal site, for a kiosk, or for a LAN box where you want zero moving parts.

Two closing notes on legality and content. EmulatorJS ships no games; you supply ROM files you have dumped from cartridges and discs you own. Several systems, PlayStation 1 in particular, also require BIOS files from your own hardware, set via EJS_biosUrl. Hosting a public page full of commercial ROMs is the fastest way to get a takedown notice and a cancelled hosting account, so keep it private or behind Tailscale.

What I'd do

For a personal retro corner with a handful of favourites, self-host the data folder, serve it from Caddy with the cross-origin isolation headers, and export saves after any session that matters. For a real collection, install RomM, let it handle metadata and server-side saves, and never touch an EmulatorJS config directly. Either way, keep it off the public internet; the game engines category covers the dedicated-server side of self-hosted gaming, which is a different problem from this one.

Similar game engines & frameworks apps