{ config, lib, pkgs, homeyConfig, ... }: # EmulatorJS — self-hosted web game emulator with per-user save sync. # # The Python sidecar (emulatorjs-server.py) serves: # / → game launcher (lists ROMs) # /roms/ → ROM listing (JSON) and ROM files # /play/ → EmulatorJS player page for a specific game # /saves/→ GET/PUT save file (SRAM), keyed by Remote-User header # /static/ → EmulatorJS JS/CSS/WASM assets (served from Nix store) # # Multi-device sync: saves are stored server-side keyed by Authelia # Remote-User header, so any device the user logs into shares the same save. # # EmulatorJS assets are fetched from GitHub at build time — no manual # download step. To upgrade, change `rev` and replace `hash` with the # value Nix reports after a failed build (same pattern as caddy.nix). # # Setup after first enable: # 1. Drop ROM files into /mnt/data/emulatorjs/roms/ # Supported: .gb .gbc .gba .nes .snes .sfc .md .gen .n64 .z64 # 2. Visit https://games. — saves go to # /mnt/data/emulatorjs/saves// automatically. let cfg = config.homey.emulatorjs; dataDir = config.homey.storage.mountPoint; domain = homeyConfig.domain; port = 8085; # EmulatorJS static assets fetched from GitHub at build time. # Pre-built WASM cores are committed to the repo so no build step is needed. # # To upgrade to a new version: # 1. Find the latest tag: # git ls-remote --tags https://github.com/EmulatorJS/EmulatorJS | tail -10 # 2. Prefetch the tarball hash: # nix-prefetch-url --unpack "https://github.com/EmulatorJS/EmulatorJS/archive/refs/tags/vX.Y.Z.tar.gz" # 3. Convert to SRI format: # nix hash convert --hash-algo sha256 --to sri # 4. Update rev and hash below. emulatorjsAssets = pkgs.fetchFromGitHub { owner = "EmulatorJS"; repo = "EmulatorJS"; rev = "v4.2.3"; hash = "sha256-hIgvcVNjl9qGJw0GgkkLQQyd2JhdUOVOQhLAeZuQcMk="; }; in { options.homey.emulatorjs.enable = lib.mkEnableOption "EmulatorJS self-hosted game emulator"; config = lib.mkIf cfg.enable { # ------------------------------------------------------------------------- # Dedicated system user so save-file ownership is stable across restarts # ------------------------------------------------------------------------- users.users.emulatorjs = { isSystemUser = true; group = "emulatorjs"; description = "EmulatorJS save-sync server"; }; users.groups.emulatorjs = {}; # ------------------------------------------------------------------------- # Launcher + save-sync server # ------------------------------------------------------------------------- systemd.services.emulatorjs = { description = "EmulatorJS launcher and save-sync server"; wantedBy = [ "multi-user.target" ]; after = [ "network.target" "mnt-data.mount" "systemd-tmpfiles-setup.service" ]; requires = [ "mnt-data.mount" ]; environment = { PORT = toString port; SAVE_DIR = "${dataDir}/emulatorjs/saves"; ROM_DIR = "${dataDir}/emulatorjs/roms"; # EmulatorJS assets live in the Nix store — read-only, no manual setup. # The data/ subdirectory contains loader.js and the emulator cores. STATIC_DIR = "${emulatorjsAssets}/data"; THUMBNAILS_DIR = "${dataDir}/emulatorjs/thumbnails"; }; serviceConfig = { ExecStart = "${pkgs.python3}/bin/python3 ${./emulatorjs-server.py}"; User = "emulatorjs"; Group = "emulatorjs"; Restart = "on-failure"; RestartSec = "5s"; NoNewPrivileges = true; ProtectSystem = "strict"; ProtectHome = true; PrivateTmp = true; ReadWritePaths = [ "${dataDir}/emulatorjs" ]; }; }; # ------------------------------------------------------------------------- # Caddy — one_factor SSO; Remote-User header forwarded to the sidecar # ------------------------------------------------------------------------- homey.caddy.virtualHosts = [{ subdomain = "games"; port = port; auth = true; }]; # ------------------------------------------------------------------------- # Storage directories (owned by the service user) # ------------------------------------------------------------------------- homey.storage.extraDirs = [ { path = "emulatorjs"; user = "emulatorjs"; group = "emulatorjs"; } { path = "emulatorjs/saves"; user = "emulatorjs"; group = "emulatorjs"; } { path = "emulatorjs/roms"; user = "emulatorjs"; group = "emulatorjs"; } { path = "emulatorjs/thumbnails"; user = "emulatorjs"; group = "emulatorjs"; } ]; # Only saves need backing up; ROMs and static assets are easy to restore homey.backup.extraPaths = [ "${dataDir}/emulatorjs/saves" ]; # ------------------------------------------------------------------------- # Authelia access control — one_factor for all household users # ------------------------------------------------------------------------- homey.authelia.accessControlRules = [{ priority = 57; domain = [ "games.${domain}" ]; policy = "one_factor"; }]; # ------------------------------------------------------------------------- # Uptime Kuma monitor # ------------------------------------------------------------------------- homey.monitoring.monitors = [{ name = "EmulatorJS"; url = "https://games.${domain}"; interval = 60; }]; }; }