{
  pkgs,
  lib,
  unstablePkgs,
  ...
}: let
  rev = "0e7135918c251ce6bcc2ead1d3f4cf0075ae2c55";
  apiRev = "a73f4c4be840444072b0a7f5458438a34b470ea9";

  # Private repositories are fetched over SSH (port 2222). nix-daemon runs as
  # root and uses /root/.ssh (identity materialized by the git-ssh-key unit),
  # so the source ends up in the store without any Nix-native credentials.
  src = builtins.fetchGit {
    url = "ssh://git@git.severijnse.eu:2222/jory/virtualcam-website.git";
    rev = rev;
  };

  apiSrc = builtins.fetchGit {
    url = "ssh://git@git.severijnse.eu:2222/jory/virtualcam-api.git";
    rev = apiRev;
  };

  # 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 = src;
    npmDepsHash = "sha256-0g98Jh/RwoicjrfiSbfqNo331k3ab8hINjV6dHHN0y4=";
    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";
  };

  # License validation / admin API (Go/Fiber), serving on host port 3004.
  # go.mod demands go 1.26.5 but the pinned nixpkgs only has 1.26.4; the code
  # uses nothing newer, so the directive is relaxed to match the toolchain.
  api = unstablePkgs.buildGoModule {
    pname = "virtualcam-api";
    version = "0.1.0";
    src = apiSrc;
    vendorHash = "sha256-uvHClXHw9ycoIf6qBZmV2O3CSyIxCgnSPCSALM07qg8=";
    go = unstablePkgs.go_1_26;
    postPatch = ''
      sed -i 's/^go 1\.26\.5$/go 1.26.4/' go.mod
    '';
  };

  dbUrl = "postgresql://virtualcam@localhost/virtualcam?host=/run/postgresql&schema=public";
  # lib/pq parses the DSN differently from node-postgres: a hostname in the URL
  # authority wins over a `host=` query param (so it would go over TCP and fail
  # password auth), and lib/pq rejects unknown URL params like `schema`. Use a
  # keyword DSN: unix-socket + peer auth + no SSL, matching the OS user.
  apiDbUrl = "host=/run/postgresql user=virtualcam dbname=virtualcam sslmode=disable";

  # Root-only runtime environment file holding the secrets both services need.
  secretsFile = ../../secrets/secrets.yaml;
  envFile = "/var/lib/virtualcam/environment";

  # Materialize the license signing key and admin token from sops into a
  # root-only file (0600). systemd reads environmentFiles before dropping
  # privileges, so the service users never need to read it themselves.
  writeSecrets = pkgs.writeShellScript "virtualcam-write-secrets" ''
    set -euo pipefail
    install -d -o virtualcam -g virtualcam -m 0750 /var/lib/virtualcam
    : > "${envFile}"
    chmod 0600 "${envFile}"
    ${pkgs.sops}/bin/sops --decrypt --input-type yaml --output-type yaml ${secretsFile} |
      ${pkgs.gnused}/bin/sed -nE \
        's/^virtualcam_license_signing_key: (.*)/LICENSE_SIGNING_KEY=\1/p; s/^virtualcam_admin_token: (.*)/ADMIN_TOKEN=\1/p' \
        >> "${envFile}"
  '';

  # Materialize the nix-daemon's SSH identity so private-repo fetches keep
  # working after the one-off bootstrap copy in /root/.ssh.
  writeGitSshKey = pkgs.writeShellScript "git-ssh-key" ''
    set -euo pipefail
    install -d -m 0700 /root/.ssh
    ${pkgs.sops}/bin/sops --decrypt --extract '["git_ssh_key_b64"]' \
      --input-type yaml --output-type yaml ${secretsFile} |
      ${pkgs.coreutils}/bin/base64 -d > /root/.ssh/id_ed25519
    chmod 0600 /root/.ssh/id_ed25519
    cat > /root/.ssh/known_hosts <<'EOF'
    git.severijnse.eu ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIAzIVo9Jdp8kwnWmTn26Fj68baJjwDphYw/0HTH5BzYY
    [git.severijnse.eu]:2222 ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQDGcPKlvdxApYRCZuQxrljEdETrxAu0JsEJ+28zFaMfA3P69J9vEkqqAZZ0Uq3rtE8ZqPBxeDh7JYn2XChb+QVGjOHVWMCWqcdD43ws26eY91aHGYnO2kW+sKSxIh803tdifIp2q8VRnaHVXcLHLx2rmwmvqIfMLnEEShxAt4Q846DianBoA7S8heoBvMLi8AAO09sVEgMPWm8ULkoRq0poHslsKkdYKlWzecP85tPo/tlCEjQq5cC5+xidH9TS4ILxVIXGUfErZzbWZq5LtaRmqiU5U1+zzdb83oJvHFtTZ8wHwZX+VE4jZzKw7wdFPk+dJZp/3ZAVva6fLSjlpyyDFNWMcf2/18Gs6YioD1n2fr1dXruZiXNMnI19Dzyp+8yWIPbAeWyVx3BoCrcjIApEDMLMo7ut0l6RfNy80hYpY91VBrpsjSRLB4uCVDsqhTLPTrFUlu+9YY9lSHEUVLRylOLK54TbY089hbHOB/UdpyCiXpI0ds3ww1UmnDQSFjwNknc+wTXRvzEz+VsX2nAzfO+5NWq2jHNn4wH6GuAinAxSVg1A7Ub9VSmPelRiTzK6ZnplPm762MxcPFmo+y/5cszumUwypcyTS8MJIC57S/kabfsnjdt97K+9eT9X2LDZ2uIQgDS8qEgvbnvfXVmMoqakgBLYG7Embqc8BDzNsQ==
    EOF
    chmod 0600 /root/.ssh/known_hosts
    test -f /root/.ssh/id_ed25519.pub || ${pkgs.openssh}/bin/ssh-keygen -y -f /root/.ssh/id_ed25519 > /root/.ssh/id_ed25519.pub
  '';

  # 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 services run 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 services user";
    };
    groups.virtualcam = {};
  };

  services.postgresql = {
    ensureDatabases = ["virtualcam"];
    ensureUsers = [
      {
        name = "virtualcam";
        ensureDBOwnership = true;
      }
    ];
  };

  systemd = {
    services = {
      git-ssh-key = {
        description = "Materialize nix-daemon git SSH key from sops";
        wantedBy = ["multi-user.target"];
        environment.SOPS_AGE_KEY_FILE = "/etc/age/keys.txt";
        serviceConfig = {
          Type = "oneshot";
          Environment = ["HOME=/root"];
          ExecStart = "${writeGitSshKey}";
        };
      };

      virtualcam-secrets = {
        description = "Materialize virtualcam secrets from sops";
        wantedBy = ["multi-user.target"];
        # The age key lives in /etc/age/keys.txt; the service must know where it
        # is and needs a HOME for age to report its user config directory.
        environment.SOPS_AGE_KEY_FILE = "/etc/age/keys.txt";
        serviceConfig = {
          Type = "oneshot";
          Environment = ["HOME=/root"];
          ExecStart = "${writeSecrets}";
        };
      };

      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" "virtualcam-secrets.service"];
        requires = ["postgresql.service" "virtualcam-migrate.service" "virtualcam-seed.service" "virtualcam-secrets.service"];
        wantedBy = ["multi-user.target"];
        path = [unstablePkgs.nodejs];
        serviceConfig = {
          User = "virtualcam";
          Group = "virtualcam";
          WorkingDirectory = "${app}";
          EnvironmentFile = [envFile];
          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"
          ];
        };
      };

      virtualcam-api = {
        description = "Virtualcamera license API (Go/Fiber)";
        after = ["postgresql.service" "virtualcam-migrate.service" "virtualcam-secrets.service"];
        requires = ["postgresql.service" "virtualcam-migrate.service" "virtualcam-secrets.service"];
        wantedBy = ["multi-user.target"];
        serviceConfig = {
          User = "virtualcam";
          Group = "virtualcam";
          WorkingDirectory = "${api}";
          EnvironmentFile = [envFile];
          ExecStart = "${api}/bin/virtualcam-api";
          Restart = "on-failure";
          RestartSec = 5;
          StateDirectory = "virtualcam";
          StateDirectoryMode = "0750";
          Environment = [
            "PORT=3004"
            "DATABASE_URL=${apiDbUrl}"
            "CORS_ORIGINS=https://virtualcam.severijnse.eu"
            "HOME=/var/lib/virtualcam"
          ];
        };
      };
    };
  };

  # Served behind Caddy (virtualHosts wired in caddy.nix).
  networking.firewall.allowedTCPPorts = [];
}
