{
  pkgs,
  unstablePkgs,
  ...
}: {
  services = {
    postgresql = {
      enable = true;
      package = pkgs.postgresql_14;
      ensureDatabases = ["gitea"];
      ensureUsers = [
        {
          name = "gitea";
          ensureDBOwnership = true;
        }
      ];
    };

    gitea = {
      enable = true;
      package = unstablePkgs.gitea;
      database = {
        type = "postgres";
        name = "gitea";
        user = "gitea";
      };
      appName = "Jory's Git";
      lfs.enable = true;
      settings = {
        server = {
          DOMAIN = "git.severijnse.eu";
          ROOT_URL = "https://git.severijnse.eu/";
          HTTP_PORT = 3000;
          SSH_PORT = 222;
          SSH_LISTEN_PORT = 2222;
          START_SSH_SERVER = true;
          SSH_USER = "git";
          BUILTIN_SSH_SERVER_USER = "git";
          LANDING_PAGE = "explore";
        };
        service = {
          DISABLE_REGISTRATION = true;
          REQUIRE_SIGNIN_VIEW = false;
        };
        repository = {
          DEFAULT_BRANCH = "main";
        };
        actions = {
          ENABLED = true;
        };
      };
    };

    gitea-actions-runner = {
      # nixos-24-05's gitea-actions-runner module hardcodes bin/act_runner,
      # but the current upstream package (1.0.3, matching gitea 1.26) ships
      # bin/gitea-runner. Wrap it so both names resolve.
      package = pkgs.runCommand "gitea-actions-runner-wrapped" {} ''
        mkdir -p $out/bin
        ln -s ${unstablePkgs.gitea-actions-runner}/bin/gitea-runner $out/bin/act_runner
      '';
      instances.default = {
        enable = true;
        name = "hetzner";
        url = "https://git.severijnse.eu";
        tokenFile = "/var/lib/secrets/gitea-runner-token";
        labels = ["native:host"];
        hostPackages = with pkgs; [
          bash
          coreutils
          curl
          gawk
          gitMinimal
          gnused
          nodejs
          wget
        ];
      };
    };
  };

  # Gitea connects to local Postgres via Unix socket (peer auth).
  # No password needed — the socket is at /run/postgresql by default.
  # createDatabase = true ensures the DB + user are set up automatically.

  # Gitea built-in SSH server: listens on high port 2222 (no privileged-cap needed),
  # while clone URLs advertise port 222. Firewall redirects 222 -> 2222.
  networking.firewall.allowedTCPPorts = [222 2222];

  # Redirect external git SSH (222) to Gitea's internal listener (2222)
  networking.firewall.extraCommands = ''
    ${pkgs.nftables}/bin/nft add table inet gitea-redirect 2>/dev/null || true
    ${pkgs.nftables}/bin/nft flush chain inet gitea-redirect prerouting 2>/dev/null || true
    ${pkgs.nftables}/bin/nft add chain inet gitea-redirect prerouting '{ type nat hook prerouting priority dstnat; }' 2>/dev/null || true
    ${pkgs.nftables}/bin/nft add rule inet gitea-redirect prerouting tcp dport 222 redirect to :2222 2>/dev/null || true
  '';

  # --- Gitea Actions self-hosted CI runner ---
  # The native runner only exposes `hostPackages` on PATH (see the list above),
  # which intentionally omits Nix. CI steps export the host's system Nix
  # (/run/current-system/sw/bin, i.e. Lix) onto PATH rather than installing a
  # second Nix client, so the running Lix daemon is used directly.
  # aarch64 builds run under QEMU user-emulation via boot.binfmt below.
  boot.binfmt.emulatedSystems = ["aarch64-linux"];
}
