{ description = "Rats with Gats Remote Play Web App"; inputs = { nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; }; outputs = { self, nixpkgs, ... }: let # Systems to support supportedSystems = [ "x86_64-linux" "aarch64-linux" "x86_64-darwin" "aarch64-darwin" ]; # Helper to generate an attrset for all supported systems forAllSystems = f: nixpkgs.lib.genAttrs supportedSystems (system: f system); in { packages = forAllSystems (system: let pkgs = nixpkgs.legacyPackages.${system}; python = pkgs.python3; # Build the Svelte frontend frontend = pkgs.buildNpmPackage { pname = "pirats-frontend"; version = "0.1.0"; src = ./frontend; npmDepsHash = "sha256-S2CXmFgovGD40wS4bTsiVF84J3zEQK6qrg7EPy+ojPY="; installPhase = '' mkdir -p $out cp -r dist $out/ ''; }; in { default = python.pkgs.buildPythonApplication { pname = "pirats"; version = "0.1.0"; src = ./.; format = "pyproject"; nativeBuildInputs = with python.pkgs; [ setuptools wheel ]; propagatedBuildInputs = with python.pkgs; [ alembic fastapi sqlmodel uvicorn websockets python-multipart httpx ]; nativeCheckInputs = with python.pkgs; [ pytestCheckHook ]; preBuild = '' mkdir -p src/pirats/static cp -r ${frontend}/dist/* src/pirats/static/ ''; pythonImportsCheck = [ "pirats" ]; }; pirats = self.packages.${system}.default; } ); devShells = forAllSystems (system: let pkgs = nixpkgs.legacyPackages.${system}; in { default = pkgs.mkShell { inputsFrom = [ self.packages.${system}.default ]; packages = with pkgs; [ pkgs.python3.pkgs.pytest pkgs.nodejs ]; }; } ); # NixOS Module to run the application as a service nixosModules.default = { config, lib, pkgs, ... }: let cfg = config.services.pirats; in { options.services.pirats = { enable = lib.mkEnableOption "Rats with Gats remote play service"; package = lib.mkOption { type = lib.types.package; default = self.packages.${pkgs.system}.default; description = "The pirats package to use."; }; host = lib.mkOption { type = lib.types.str; default = "127.0.0.1"; description = "The host address to bind to."; }; port = lib.mkOption { type = lib.types.port; default = 8000; description = "The port to listen on."; }; databasePath = lib.mkOption { type = lib.types.str; default = "/var/lib/pirats/rats_with_gats.db"; description = "Path to the SQLite database file."; }; logFile = lib.mkOption { type = lib.types.str; default = "/var/log/pirats/pirats.log"; description = '' Path to the server log file (rotating, 10 MB x 5 backups). stderr is also captured by the systemd journal. The default lives under the service's LogsDirectory (/var/log/pirats), which the sandboxed DynamicUser can write to; a custom path elsewhere must be made writable by the service separately. ''; }; logLevel = lib.mkOption { type = lib.types.enum [ "debug" "info" "warning" "error" "critical" ]; default = "info"; description = "Minimum severity of log messages to record."; }; maxBodyBytes = lib.mkOption { type = lib.types.ints.positive; default = 1048576; description = "Reject request bodies larger than this many bytes (HTTP 413)."; }; openFirewall = lib.mkOption { type = lib.types.bool; default = false; description = "Open ports in the firewall for the service."; }; devModeDefault = lib.mkOption { type = lib.types.bool; default = false; description = "Whether new games start with Dev Mode (testing shortcuts) enabled."; }; purge = { enable = lib.mkOption { type = lib.types.bool; default = true; description = "Periodically purge old finished/inactive games."; }; intervalHours = lib.mkOption { type = lib.types.numbers.positive; default = 24; description = "How often (in hours) the purge task runs."; }; finishedDays = lib.mkOption { type = lib.types.numbers.positive; default = 14; description = "Purge games that finished more than this many days ago."; }; inactiveDays = lib.mkOption { type = lib.types.numbers.positive; default = 30; description = "Purge games with no activity for this many days."; }; }; }; config = lib.mkIf cfg.enable { networking.firewall.allowedTCPPorts = lib.optionals cfg.openFirewall [ cfg.port ]; systemd.services.pirats = { description = "Rats with Gats Web Application"; after = [ "network.target" ]; wantedBy = [ "multi-user.target" ]; environment = { DATABASE_URL = "sqlite:///${cfg.databasePath}"; # Unset means "local checkout" and defaults Dev Mode on, so the # service must always set it explicitly. PIRATS_DEV_MODE = lib.boolToString cfg.devModeDefault; PIRATS_LOG_FILE = cfg.logFile; PIRATS_LOG_LEVEL = cfg.logLevel; PIRATS_PURGE_ENABLED = lib.boolToString cfg.purge.enable; PIRATS_PURGE_INTERVAL_HOURS = toString cfg.purge.intervalHours; PIRATS_PURGE_FINISHED_DAYS = toString cfg.purge.finishedDays; PIRATS_PURGE_INACTIVE_DAYS = toString cfg.purge.inactiveDays; PIRATS_MAX_BODY_BYTES = toString cfg.maxBodyBytes; }; serviceConfig = { ExecStart = "${cfg.package}/bin/pirats --host ${cfg.host} --port ${toString cfg.port}"; Restart = "always"; # Sandboxing and security DynamicUser = true; StateDirectory = "pirats"; # Creates /var/log/pirats owned by the dynamic user and adds it to # ReadWritePaths, so the default logFile under it is writable # despite ProtectSystem=strict. LogsDirectory = "pirats"; WorkingDirectory = "/var/lib/pirats"; ProtectSystem = "strict"; ProtectHome = true; PrivateTmp = true; PrivateDevices = true; }; }; }; }; }; }