{ description = "buildfor_life_repair - Vintage equipment inventory & repair tracker"; inputs = { nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; flake-utils.url = "github:numtide/flake-utils"; }; outputs = { self, nixpkgs, flake-utils }: flake-utils.lib.eachDefaultSystem (system: let pkgs = import nixpkgs { inherit system; }; nodejs = pkgs.nodejs_22; in { # Development shell: `nix develop` devShells.default = pkgs.mkShell { buildInputs = [ nodejs pkgs.postgresql_16 pkgs.pkg-config pkgs.vips # required by sharp ]; shellHook = '' echo "buildfor_life_repair dev shell" echo " node: $(node --version)" echo " npm: $(npm --version)" echo "" echo "Run 'npm install' then 'npm run dev' to start." ''; }; # Production package: `nix build` packages.default = pkgs.buildNpmPackage rec { pname = "buildfor_life_repair"; version = "0.1.0"; src = ./.; npmDepsHash = ""; # Run `nix build` once — it will fail and print the correct hash nodejs = pkgs.nodejs_22; nativeBuildInputs = [ pkgs.pkg-config pkgs.python3 # needed by sharp native rebuild ]; buildInputs = [ pkgs.vips pkgs.glib pkgs.expat ]; # SvelteKit needs sync before build preBuild = '' npx svelte-kit sync ''; npmBuildScript = "build"; installPhase = '' runHook preInstall mkdir -p $out/lib/buildfor_life_repair cp -r build/* $out/lib/buildfor_life_repair/ cp -r node_modules $out/lib/buildfor_life_repair/ cp package.json $out/lib/buildfor_life_repair/ mkdir -p $out/bin cat > $out/bin/buildfor_life_repair <<'WRAPPER' #!/usr/bin/env bash exec ${pkgs.nodejs_22}/bin/node $out/lib/buildfor_life_repair/index.js "$@" WRAPPER chmod +x $out/bin/buildfor_life_repair runHook postInstall ''; meta = with pkgs.lib; { description = "Vintage equipment inventory & repair tracker"; license = licenses.mit; platforms = platforms.linux; }; }; } ) // { # NixOS module for easy deployment nixosModules.default = { config, lib, pkgs, ... }: let cfg = config.services.buildfor-life-repair; in { options.services.buildfor-life-repair = { enable = lib.mkEnableOption "buildfor_life_repair inventory system"; port = lib.mkOption { type = lib.types.port; default = 3000; description = "Port to listen on"; }; host = lib.mkOption { type = lib.types.str; default = "0.0.0.0"; description = "Host to bind to"; }; databaseUrl = lib.mkOption { type = lib.types.str; default = ""; description = "PostgreSQL connection string. Leave empty if using environmentFile."; example = "postgresql://bflr:password@localhost:5432/buildfor_life_repair"; }; environmentFile = lib.mkOption { type = lib.types.nullOr lib.types.path; default = null; description = "Path to environment file with secrets (DATABASE_URL, etc). Takes precedence over databaseUrl."; }; baseUrl = lib.mkOption { type = lib.types.str; default = "http://localhost:${toString cfg.port}"; description = "Public base URL for QR code generation"; }; uploadDir = lib.mkOption { type = lib.types.str; default = "/var/lib/buildfor-life-repair/uploads"; description = "Directory for uploaded images and documents"; }; user = lib.mkOption { type = lib.types.str; default = "bflr"; description = "System user to run the service as"; }; group = lib.mkOption { type = lib.types.str; default = "bflr"; description = "System group to run the service as"; }; openFirewall = lib.mkOption { type = lib.types.bool; default = false; description = "Open the firewall port"; }; }; config = lib.mkIf cfg.enable { assertions = [ { assertion = cfg.databaseUrl != "" || cfg.environmentFile != null; message = "services.buildfor-life-repair: Either databaseUrl or environmentFile must be set."; } ]; users.users.${cfg.user} = { isSystemUser = true; group = cfg.group; home = "/var/lib/buildfor-life-repair"; createHome = true; }; users.groups.${cfg.group} = {}; systemd.services.buildfor-life-repair = { description = "buildfor_life_repair inventory system"; wantedBy = [ "multi-user.target" ]; after = [ "network.target" "postgresql.service" ]; environment = { NODE_ENV = "production"; PORT = toString cfg.port; HOST = cfg.host; BASE_URL = cfg.baseUrl; UPLOAD_DIR = cfg.uploadDir; } // lib.optionalAttrs (cfg.databaseUrl != "") { DATABASE_URL = cfg.databaseUrl; }; serviceConfig = { Type = "simple"; User = cfg.user; Group = cfg.group; WorkingDirectory = "/var/lib/buildfor-life-repair"; ExecStart = "${self.packages.${pkgs.system}.default}/bin/buildfor_life_repair"; Restart = "on-failure"; RestartSec = 5; # Hardening NoNewPrivileges = true; ProtectSystem = "strict"; ProtectHome = true; ReadWritePaths = [ cfg.uploadDir "/var/lib/buildfor-life-repair" ]; PrivateTmp = true; } // lib.optionalAttrs (cfg.environmentFile != null) { EnvironmentFile = cfg.environmentFile; }; }; systemd.tmpfiles.rules = [ "d ${cfg.uploadDir} 0750 ${cfg.user} ${cfg.group} -" "d ${cfg.uploadDir}/devices 0750 ${cfg.user} ${cfg.group} -" "d ${cfg.uploadDir}/components 0750 ${cfg.user} ${cfg.group} -" "d ${cfg.uploadDir}/documents 0750 ${cfg.user} ${cfg.group} -" ]; networking.firewall.allowedTCPPorts = lib.mkIf cfg.openFirewall [ cfg.port ]; }; }; }; }