Skip to content

keycloak-keywind-theme-overlay

Overlays

Package the third-party Tailwind Keycloak theme Keywind from source with buildNpmPackage, and expose it through an overlay so services.keycloak.themes can consume it.

Problem

Keycloak's login pages are ugly by default, and Keywind is a nice Tailwind restyle — but it ships as source, not as a Nix package. You have to build the theme jar yourself, and NixOS's services.keycloak.themes.<name> wants something very specific in return: a directory that is one theme flavour, not the jar and not the jar's directory tree.

The insight / trap

Keywind builds in two npm steps:

  1. npm run build — compiles the Tailwind/FreeMarker assets.
  2. npm run build:jar — packs them into out/keywind.jar, a standard Keycloak theme jar.

A Keycloak theme jar has the layout:

theme/<themeName>/<themeType>/...   e.g.  theme/amora/login/...

services.keycloak.themes.<name> = pkg; expects pkg to point at the innermost directory — the one that directly contains theme.properties, login/ (or the flavour templates), resources/, messages/, etc. It does not want the jar, and it does not want the theme/ wrapper or the intermediate <themeName>/ directory.

So after build:jar you must unzip the jar and copy only theme/amora/login to $out. That login subdirectory is the entire output Keycloak actually consumes. Ship the wrapper instead and Keycloak silently fails to find the theme — no error, the theme just never appears.

(amora is the internal name Keywind's build bakes into the jar. It is not the name you pick in Keycloak — that comes from the attribute name you inherit into services.keycloak.themes.)

Usage

Wire the overlay into your nixpkgs and inherit the theme into Keycloak:

{ pkgs, ... }:
{
  nixpkgs.overlays = [ (import ./default.nix).overlay ];

  services.keycloak = {
    enable = true;
    # ... hostname, database, etc. ...
    themes = {
      inherit (pkgs.keycloak-themes) keywind;
    };
  };
}

Then, in a realm's Login theme dropdown, select keywind.

Or build the bare package without the overlay:

pkgs.callPackage (import ./default.nix).default { }

Options

The package function takes two knobs (both with sensible defaults) so you can retarget a fork without editing the logic:

Option Default Meaning
themeType "login" Which Keycloak theme flavour to keep out of the jar.
themeName "amora" The name Keywind's build packs the theme under inside the jar (theme/<themeName>/). An implementation detail of the source, not the Keycloak-facing name.

Caveats

  • Pin and audit the source. Bump rev/hash/npmDepsHash to a commit you have reviewed. To find the correct hashes, build once with a placeholder hash and copy the got: value Nix prints.
  • npmDepsHash must match the locked package-lock.json at the pinned rev. If you change the revision you must refresh this hash too.
  • Keywind currently provides only a login theme. If you point themeType at a flavour the jar doesn't contain, the cp -a in installPhase will fail the build — which is the safe outcome.

Source

overlays/keycloak-keywind-theme-overlay/default.nix
# keycloak-keywind-theme-overlay
#
# Package the third-party Tailwind Keycloak theme "Keywind" from source with
# buildNpmPackage, and expose it in an overlay so it can be consumed by
# services.keycloak.themes.
#
# Two things live in this file:
#
#   1. keywindPackage — the package function (callPackage-style). Build the
#      theme jar from source, unzip it, and keep ONLY the login theme subdir.
#      See the comment on installPhase for the trap.
#
#   2. overlay — a trivial overlay that publishes the built theme under
#      pkgs.keycloak-themes.keywind so downstream modules can reference it.
#
# Usage (flake or configuration.nix):
#
#   nixpkgs.overlays = [ (import ./default.nix).overlay ];
#
#   services.keycloak.themes = {
#     inherit (pkgs.keycloak-themes) keywind;
#   };
#
# then in a realm's login theme dropdown pick "keywind".

let
  # ---------------------------------------------------------------------------
  # The package itself.
  #
  # Pin `rev`/`hash`/`npmDepsHash` to a commit you have audited. The values
  # below are a working example; bump them for a newer Keywind and let Nix
  # tell you the correct hashes (build once with a fake hash, copy the
  # "got:" value nix prints).
  # ---------------------------------------------------------------------------
  keywindPackage =
    {
      lib,
      fetchFromGitHub,
      buildNpmPackage,
      nodejs,
      unzip,
      # Which Keycloak theme *type* to keep out of the built jar. Keywind ships
      # a login theme; keep it configurable in case an upstream fork adds more.
      themeType ? "login",
      # The theme name Keywind's build packs into the jar under theme/<name>/.
      # Upstream calls it "amora". This is an implementation detail of the
      # source, NOT the name users pick in Keycloak (that comes from the
      # attribute you inherit into services.keycloak.themes).
      themeName ? "amora",
    }:
    buildNpmPackage {
      pname = "keywind";
      version = "0.2.0";

      src = fetchFromGitHub {
        owner = "lukin";
        repo = "keywind";
        rev = "a47de9ed208521b2395d8a9edf9b8ef3b6654778";
        hash = "sha256-wl+Lma6bPtpuh5RXeDI15X3VZ6gdsiFP0jv/R3bySWs=";
      };

      npmDepsHash = "sha256-w4xlQSyCpmv1bF8Igcr9t3q0UBwEynOkYTi+TfC13CA=";

      buildInputs = [ nodejs ];

      # Two steps: `build` compiles the Tailwind/FreeMarker assets, `build:jar`
      # packs them into out/keywind.jar (a standard Keycloak theme jar).
      buildPhase = ''
        runHook preBuild
        npm run build
        npm run build:jar
        runHook postBuild
      '';

      # THE TRAP / the whole point of this recipe:
      #
      # `services.keycloak.themes.<name> = pkg;` expects `pkg` to be a directory
      # that IS a single theme flavour — i.e. the directory that contains
      # theme.properties, login/ templates, resources/, messages/ … . It does
      # NOT want the jar, and it does NOT want the jar's top-level `theme/`
      # wrapper with an intermediate <name>/ directory.
      #
      # Keycloak's own theme jar layout is:  theme/<themeName>/<themeType>/...
      # We unzip the jar and copy ONLY theme/<themeName>/<themeType> to $out.
      # That inner <themeType> (login) directory is the entire output Keycloak
      # actually consumes. Ship the wrapper and Keycloak silently fails to find
      # the theme.
      installPhase = ''
        runHook preInstall

        mkdir target
        ${unzip}/bin/unzip out/keywind.jar -d target
        rm -rf out

        mkdir $out
        cp -a target/theme/${themeName}/${themeType} $out

        runHook postInstall
      '';

      meta = with lib; {
        description = "A Tailwind.css theme for Keycloak";
        homepage = "https://github.com/lukin/keywind";
        license = licenses.asl20;
        platforms = platforms.all;
      };
    };

  # ---------------------------------------------------------------------------
  # The overlay. Publishes pkgs.keycloak-themes.keywind.
  # ---------------------------------------------------------------------------
  overlay = final: _prev: {
    keycloak-themes = (_prev.keycloak-themes or { }) // {
      keywind = final.callPackage keywindPackage { };
    };
  };
in
{
  inherit keywindPackage overlay;

  # Convenience: `(import ./default.nix).default` gives the plain package,
  # buildable with `nix-build -A default` against a pkgs set via callPackage,
  # or just import the overlay above.
  default = keywindPackage;
}