{ pkgs, lib, unstablePkgs, ... }: let rev = "6225e0fca02c02544341c92ecdc9634a9a15f45c"; src = pkgs.fetchgit { url = "https://git.severijnse.eu/jory/virtualcam-website.git"; rev = rev; sha256 = "17ihw2bhsp89nczljz6xzwlvxyzgsdn62ywmchp5blzd6jkxd3w0"; }; # Patch the app to be fully dynamic and drop the Google-font download so the # sandboxed Nix build needs neither a database nor network access. srcPatched = pkgs.applyPatches { name = "virtualcam-website-patched"; src = src; patches = [./virtualcam-layout.patch ./virtualcam-build.patch]; }; # Build the Next.js app entirely in Nix (offline npm deps from the lockfile). app = unstablePkgs.buildNpmPackage { pname = "virtualcam-website"; version = "0.1.0"; src = srcPatched; npmDepsHash = "sha256-52ugs4ydwxGXLIhF/6P8uO400x3BRYk4NUt2Swob3cY="; nodejs = unstablePkgs.nodejs; buildPhase = '' runHook preBuild npx prisma generate npm run build runHook postBuild ''; installPhase = '' runHook preInstall mkdir -p $out cp -r .next node_modules public prisma src package.json package-lock.json \ prisma.config.ts next.config.ts tsconfig.json postcss.config.mjs $out/ runHook postInstall ''; APP_URL = "https://virtualcam.severijnse.eu"; # Only used to satisfy prisma generate / next build metadata resolution. DATABASE_URL = "postgresql://virtualcam@localhost/virtualcam?host=/run/postgresql&schema=public"; # Use the nixpkgs-bundled Prisma engine so the sandboxed offline build does # not need to download it from binaries.prisma.sh. Version matches ^7.8.0. PRISMA_SCHEMA_ENGINE_BINARY = "${unstablePkgs.prisma-engines}/bin/schema-engine"; NEXT_TELEMETRY_DISABLED = "1"; CI = "true"; }; dbUrl = "postgresql://virtualcam@localhost/virtualcam?host=/run/postgresql&schema=public"; # The repo's seed uses tsx (a devDependency buildNpmPackage drops) plus the # "@/..." path alias. nixpkgs ships tsx, which honours tsconfig paths, so we # add it to the service path rather than working around the missing dep. seed = pkgs.writeShellScript "virtualcam-seed" '' set -euo pipefail export DATABASE_URL="${dbUrl}" export PRISMA_SCHEMA_ENGINE_BINARY="${unstablePkgs.prisma-engines}/bin/schema-engine" export HOME=/var/lib/virtualcam cd ${app} tsx prisma/seed.ts ''; # One shared PostgreSQL server (existing system postgres). Each service gets # its own database + role. virtualcam authenticates over the Unix socket via # peer auth: the systemd service runs as OS user `virtualcam`, which matches # the database role `virtualcam`, so no password is stored anywhere. migrate = pkgs.writeShellScript "virtualcam-migrate" '' set -euo pipefail export DATABASE_URL="${dbUrl}" # Use the local Prisma engine; no network download needed at runtime. export PRISMA_SCHEMA_ENGINE_BINARY="${unstablePkgs.prisma-engines}/bin/schema-engine" cd ${app} ./node_modules/.bin/prisma migrate deploy ''; in { users = { users.virtualcam = { isSystemUser = true; group = "virtualcam"; description = "virtualcamera website service user"; }; groups.virtualcam = {}; }; services.postgresql = { ensureDatabases = ["virtualcam"]; ensureUsers = [ { name = "virtualcam"; ensureDBOwnership = true; } ]; }; systemd = { services = { virtualcam-migrate = { description = "Virtualcam Prisma migrations"; after = ["postgresql.service"]; requires = ["postgresql.service"]; wantedBy = ["multi-user.target"]; serviceConfig = { Type = "oneshot"; User = "virtualcam"; Group = "virtualcam"; StateDirectory = "virtualcam"; StateDirectoryMode = "0750"; ExecStart = "${migrate}"; }; }; virtualcam-seed = { description = "Virtualcam catalog seed"; after = ["virtualcam-migrate.service"]; requires = ["virtualcam-migrate.service"]; wantedBy = ["multi-user.target"]; path = [unstablePkgs.nodejs unstablePkgs.tsx]; serviceConfig = { Type = "oneshot"; User = "virtualcam"; Group = "virtualcam"; ExecCondition = "!/var/lib/virtualcam/.seeded"; ExecStart = "${seed}"; ExecStartPost = "${pkgs.coreutils}/bin/touch /var/lib/virtualcam/.seeded"; StateDirectory = "virtualcam"; StateDirectoryMode = "0750"; }; }; virtualcam = { description = "Virtualcamera website (Next.js)"; after = ["postgresql.service" "virtualcam-migrate.service" "virtualcam-seed.service"]; requires = ["postgresql.service" "virtualcam-migrate.service" "virtualcam-seed.service"]; wantedBy = ["multi-user.target"]; path = [unstablePkgs.nodejs]; serviceConfig = { User = "virtualcam"; Group = "virtualcam"; WorkingDirectory = "${app}"; ExecStart = "${app}/node_modules/.bin/next start -p 3001 -H 127.0.0.1"; Restart = "on-failure"; RestartSec = 5; StateDirectory = "virtualcam"; StateDirectoryMode = "0750"; Environment = [ "DATABASE_URL=${dbUrl}" "APP_URL=https://virtualcam.severijnse.eu" "PAYMENTS_MODE=shkeeper" "ADMIN_EMAILS=jory@severijnse.eu" "SMTP_HOST=localhost" "SMTP_PORT=587" "SMTP_USER=jory@severijnse.eu" "SMTP_FROM=noreply@severijnse.eu" "NODE_ENV=production" "NEXT_TELEMETRY_DISABLED=1" "HOME=/var/lib/virtualcam" ]; }; }; }; }; # Serve behind Caddy on 127.0.0.1:3000 (virtualHost wired in caddy.nix). networking.firewall.allowedTCPPorts = []; }