diff --git a/cmd/fuserdb/main.go b/cmd/fuserdb/main.go
deleted file mode 100644
index 2c3dd24..0000000
--- a/cmd/fuserdb/main.go
+++ /dev/null
@@ -1,69 +0,0 @@
-package main
-
-import (
-	"bytes"
-	"errors"
-	"flag"
-	"fmt"
-	"os"
-	"path"
-	"strconv"
-
-	"git.gensokyo.uk/security/fortify/internal/fmsg"
-)
-
-func main() {
-	fmsg.SetPrefix("fuserdb")
-
-	const varEmpty = "/var/empty"
-
-	out := flag.String("o", "userdb", "output directory")
-	homeDir := flag.String("d", varEmpty, "parent of home directories")
-	shell := flag.String("s", "/sbin/nologin", "absolute path to subordinate user shell")
-	flag.Parse()
-
-	type user struct {
-		name string
-		fid  int
-	}
-
-	users := make([]user, len(flag.Args()))
-	for i, s := range flag.Args() {
-		f := bytes.SplitN([]byte(s), []byte{':'}, 2)
-		if len(f) != 2 {
-			fmsg.Fatalf("invalid entry at index %d", i)
-		}
-		users[i].name = string(f[0])
-		if fid, err := strconv.Atoi(string(f[1])); err != nil {
-			fmsg.Fatal(err.Error())
-		} else {
-			users[i].fid = fid
-		}
-	}
-
-	if err := os.MkdirAll(*out, 0755); err != nil && !errors.Is(err, os.ErrExist) {
-		fmsg.Fatalf("cannot create output: %v", err)
-	}
-
-	for _, u := range users {
-		fidString := strconv.Itoa(u.fid)
-		for aid := 0; aid < 10000; aid++ {
-			userName := fmt.Sprintf("u%d_a%d", u.fid, aid)
-			uid := 1000000 + u.fid*10000 + aid
-			us := strconv.Itoa(uid)
-			realName := fmt.Sprintf("Fortify subordinate user %d (%s)", aid, u.name)
-			var homeDirectory string
-			if *homeDir != varEmpty {
-				homeDirectory = path.Join(*homeDir, "u"+fidString, "a"+strconv.Itoa(aid))
-			} else {
-				homeDirectory = varEmpty
-			}
-
-			writeUser(userName, uid, us, realName, homeDirectory, *shell, *out)
-			writeGroup(userName, uid, us, nil, *out)
-		}
-	}
-
-	fmsg.Printf("created %d entries", len(users)*2*10000)
-	fmsg.Exit(0)
-}
diff --git a/cmd/fuserdb/payload.go b/cmd/fuserdb/payload.go
deleted file mode 100644
index dc8a072..0000000
--- a/cmd/fuserdb/payload.go
+++ /dev/null
@@ -1,64 +0,0 @@
-package main
-
-import (
-	"encoding/json"
-	"os"
-	"path"
-
-	"git.gensokyo.uk/security/fortify/internal/fmsg"
-)
-
-type payloadU struct {
-	UserName      string   `json:"userName"`
-	Uid           int      `json:"uid"`
-	Gid           int      `json:"gid"`
-	MemberOf      []string `json:"memberOf,omitempty"`
-	RealName      string   `json:"realName"`
-	HomeDirectory string   `json:"homeDirectory"`
-	Shell         string   `json:"shell"`
-}
-
-func writeUser(userName string, uid int, us string, realName, homeDirectory, shell string, out string) {
-	userFileName := userName + ".user"
-	if f, err := os.OpenFile(path.Join(out, userFileName), os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644); err != nil {
-		fmsg.Fatalf("cannot create %s: %v", userName, err)
-	} else if err = json.NewEncoder(f).Encode(&payloadU{
-		UserName:      userName,
-		Uid:           uid,
-		Gid:           uid,
-		RealName:      realName,
-		HomeDirectory: homeDirectory,
-		Shell:         shell,
-	}); err != nil {
-		fmsg.Fatalf("cannot serialise %s: %v", userName, err)
-	} else if err = f.Close(); err != nil {
-		fmsg.Printf("cannot close %s: %v", userName, err)
-	}
-	if err := os.Symlink(userFileName, path.Join(out, us+".user")); err != nil {
-		fmsg.Fatalf("cannot link %s: %v", userName, err)
-	}
-}
-
-type payloadG struct {
-	GroupName string   `json:"groupName"`
-	Gid       int      `json:"gid"`
-	Members   []string `json:"members,omitempty"`
-}
-
-func writeGroup(groupName string, gid int, gs string, members []string, out string) {
-	groupFileName := groupName + ".group"
-	if f, err := os.OpenFile(path.Join(out, groupFileName), os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644); err != nil {
-		fmsg.Fatalf("cannot create %s: %v", groupName, err)
-	} else if err = json.NewEncoder(f).Encode(&payloadG{
-		GroupName: groupName,
-		Gid:       gid,
-		Members:   members,
-	}); err != nil {
-		fmsg.Fatalf("cannot serialise %s: %v", groupName, err)
-	} else if err = f.Close(); err != nil {
-		fmsg.Printf("cannot close %s: %v", groupName, err)
-	}
-	if err := os.Symlink(groupFileName, path.Join(out, gs+".group")); err != nil {
-		fmsg.Fatalf("cannot link %s: %v", groupName, err)
-	}
-}
diff --git a/dist/install.sh b/dist/install.sh
index 0641226..bd1e8a2 100755
--- a/dist/install.sh
+++ b/dist/install.sh
@@ -4,8 +4,6 @@ cd "$(dirname -- "$0")" || exit 1
 install -vDm0755 "bin/fortify" "${FORTIFY_INSTALL_PREFIX}/usr/bin/fortify"
 install -vDm0755 "bin/fpkg" "${FORTIFY_INSTALL_PREFIX}/usr/bin/fpkg"
 
-install -vDm0755 "bin/fuserdb" "${FORTIFY_INSTALL_PREFIX}/usr/libexec/fortify/fuserdb"
-
 install -vDm6511 "bin/fsu" "${FORTIFY_INSTALL_PREFIX}/usr/bin/fsu"
 if [ ! -f "${FORTIFY_INSTALL_PREFIX}/etc/fsurc" ]; then
     install -vDm0400 "fsurc.default" "${FORTIFY_INSTALL_PREFIX}/etc/fsurc"
diff --git a/nixos.nix b/nixos.nix
index cd4cf98..84e58c7 100644
--- a/nixos.nix
+++ b/nixos.nix
@@ -7,6 +7,7 @@
 
 let
   inherit (lib)
+    mkMerge
     mkIf
     mkDefault
     mapAttrs
@@ -19,6 +20,10 @@ let
     ;
 
   cfg = config.environment.fortify;
+
+  getsubuid = fid: aid: 1000000 + fid * 10000 + aid;
+  getsubname = fid: aid: "u${toString fid}_a${toString aid}";
+  getsubhome = fid: aid: "${cfg.stateDir}/u${toString fid}/a${toString aid}";
 in
 
 {
@@ -33,23 +38,12 @@ in
       group = "root";
     };
 
-    environment.etc = {
-      fsurc = {
-        mode = "0400";
-        text = foldlAttrs (
-          acc: username: fid:
-          "${toString config.users.users.${username}.uid} ${toString fid}\n" + acc
-        ) "" cfg.users;
-      };
-
-      userdb.source = pkgs.runCommand "fortify-userdb" { } ''
-        ${cfg.package}/libexec/fuserdb -o $out ${
-          foldlAttrs (
-            acc: username: fid:
-            acc + " ${username}:${toString fid}"
-          ) "-s /run/current-system/sw/bin/nologin -d ${cfg.stateDir}" cfg.users
-        }
-      '';
+    environment.etc.fsurc = {
+      mode = "0400";
+      text = foldlAttrs (
+        acc: username: fid:
+        "${toString config.users.users.${username}.uid} ${toString fid}\n" + acc
+      ) "" cfg.users;
     };
 
     systemd.services.nix-daemon.unitConfig.RequiresMountsFor = [ "/etc/userdb" ];
@@ -114,8 +108,8 @@ in
                     confinement = {
                       app_id = aid;
                       inherit (app) groups;
-                      username = "u${toString fid}_a${toString aid}";
-                      home = "${cfg.stateDir}/u${toString fid}/a${toString aid}";
+                      username = getsubname fid aid;
+                      home = getsubhome fid aid;
                       sandbox = {
                         inherit (app)
                           userns
@@ -173,7 +167,9 @@ in
                   };
                 in
                 pkgs.writeShellScriptBin app.name ''
-                  exec fortify app ${pkgs.writeText "fortify-${app.name}.json" (builtins.toJSON conf)} $@
+                  exec fortify${
+                    if app.verbose then " -v" else ""
+                  } app ${pkgs.writeText "fortify-${app.name}.json" (builtins.toJSON conf)} $@
                 ''
               ) cfg.apps;
             in
@@ -208,13 +204,57 @@ in
           mergeAttrsList (
             # aid 0 is reserved
             imap1 (aid: app: {
-              "u${toString fid}_a${toString aid}" = app.extraConfig // {
-                home.packages = app.packages;
-              };
+              ${getsubname fid aid} = mkMerge [
+                (cfg.home-manager (getsubname fid aid) (getsubuid fid aid))
+                app.extraConfig
+                { home.packages = app.packages; }
+              ];
             }) cfg.apps
           )
           // acc
         ) privPackages cfg.users;
       };
+
+    users =
+      let
+        getuser = fid: aid: {
+          isSystemUser = true;
+          createHome = true;
+          description = "Fortify subordinate user ${toString aid} (u${toString fid})";
+          group = getsubname fid aid;
+          home = getsubhome fid aid;
+          uid = getsubuid fid aid;
+        };
+        getgroup = fid: aid: { gid = getsubuid fid aid; };
+      in
+      {
+        users = foldlAttrs (
+          acc: _: fid:
+          mkMerge [
+            (mergeAttrsList (
+              # aid 0 is reserved
+              imap1 (aid: _: {
+                ${getsubname fid aid} = getuser fid aid;
+              }) cfg.apps
+            ))
+            { ${getsubname fid 0} = getuser fid 0; }
+            acc
+          ]
+        ) { } cfg.users;
+
+        groups = foldlAttrs (
+          acc: _: fid:
+          mkMerge [
+            (mergeAttrsList (
+              # aid 0 is reserved
+              imap1 (aid: _: {
+                ${getsubname fid aid} = getgroup fid aid;
+              }) cfg.apps
+            ))
+            { ${getsubname fid 0} = getgroup fid 0; }
+            acc
+          ]
+        ) { } cfg.users;
+      };
   };
 }
diff --git a/options.nix b/options.nix
index 616b24c..578c7fd 100644
--- a/options.nix
+++ b/options.nix
@@ -26,6 +26,17 @@ in
         '';
       };
 
+      home-manager = mkOption {
+        type =
+          let
+            inherit (types) functionTo attrsOf anything;
+          in
+          functionTo (functionTo (attrsOf anything));
+        description = ''
+          Target user shared home-manager configuration.
+        '';
+      };
+
       apps = mkOption {
         type =
           let
@@ -50,6 +61,8 @@ in
                 '';
               };
 
+              verbose = mkEnableOption "launchers with verbose output";
+
               id = mkOption {
                 type = nullOr str;
                 default = null;
diff --git a/test.nix b/test.nix
index 47f7985..e40ae72 100644
--- a/test.nix
+++ b/test.nix
@@ -44,7 +44,6 @@ nixosTest {
           # For glinfo and wayland-info:
           mesa-demos
           wayland-utils
-          alacritty
 
           # For D-Bus tests:
           libnotify
@@ -111,6 +110,43 @@ nixosTest {
         enable = true;
         stateDir = "/var/lib/fortify";
         users.alice = 0;
+
+        home-manager = _: _: { home.stateVersion = "23.05"; };
+
+        apps = [
+          {
+            name = "ne-foot";
+            verbose = true;
+            share = pkgs.foot;
+            packages = [ pkgs.foot ];
+            command = "foot";
+            capability = {
+              dbus = false;
+              pulse = false;
+            };
+          }
+          {
+            name = "pa-foot";
+            verbose = true;
+            share = pkgs.foot;
+            packages = [ pkgs.foot ];
+            command = "foot";
+            capability.dbus = false;
+          }
+          {
+            name = "x11-alacritty";
+            verbose = true;
+            share = pkgs.alacritty;
+            packages = [ pkgs.alacritty ];
+            command = "alacritty";
+            capability = {
+              wayland = false;
+              x11 = true;
+              dbus = false;
+              pulse = false;
+            };
+          }
+        ];
       };
 
       imports = [
@@ -176,16 +212,18 @@ nixosTest {
         machine.screenshot(name)
 
 
-    def check_state(command, enablements):
+    def check_state(name, enablements):
         instances = json.loads(machine.succeed("sudo -u alice -i XDG_RUNTIME_DIR=/run/user/1000 fortify --json ps"))
         if len(instances) != 1:
             raise Exception(f"unexpected state length {len(instances)}")
         instance = next(iter(instances.values()))
 
-        if instance['config']['command'] != command:
+        config = instance['config']
+
+        if len(config['command']) != 1 or not(config['command'][0].startswith("/nix/store/")) or not(config['command'][0].endswith(f"{name}-start")):
             raise Exception(f"unexpected command {instance['config']['command']}")
 
-        if instance['config']['confinement']['enablements'] != enablements:
+        if config['confinement']['enablements'] != enablements:
             raise Exception(f"unexpected enablements {instance['config']['confinement']['enablements']}")
 
 
@@ -212,60 +250,60 @@ nixosTest {
     # Create fortify uid 0 state directory:
     machine.succeed("install -dm 0755 -o u0_a0 -g users /var/lib/fortify/u0")
 
-    # Start fortify outside Wayland session:
+    # Start fortify permissive defaults outside Wayland session:
     print(machine.succeed("sudo -u alice -i fortify -v run -a 0 touch /tmp/success-bare"))
     machine.wait_for_file("/tmp/fortify.1000/tmpdir/0/success-bare")
 
-    # Start fortify within Wayland session:
+    # Start fortify permissive defaults within Wayland session:
     fortify('-v run --wayland --dbus notify-send -a "NixOS Tests" "Test notification" "Notification from within sandbox." && touch /tmp/dbus-done')
     machine.wait_for_file("/tmp/dbus-done")
     collect_state_ui("dbus_notify_exited")
     machine.succeed("pkill -9 mako")
 
-    # Start a terminal (foot) within fortify:
-    fortify("run --wayland foot")
-    wait_for_window("u0_a0@machine")
+    # Start app (foot) with Wayland enablement:
+    swaymsg("exec ne-foot")
+    wait_for_window("u0_a1@machine")
     machine.send_chars("clear; wayland-info && touch /tmp/success-client\n")
-    machine.wait_for_file("/tmp/fortify.1000/tmpdir/0/success-client")
-    collect_state_ui("foot_wayland_permissive")
-    check_state(["foot"], 1)
+    machine.wait_for_file("/tmp/fortify.1000/tmpdir/1/success-client")
+    collect_state_ui("foot_wayland")
+    check_state("ne-foot", 1)
     # Verify acl on XDG_RUNTIME_DIR:
-    print(machine.succeed("getfacl --absolute-names --omit-header --numeric /run/user/1000 | grep 1000000"))
+    print(machine.succeed("getfacl --absolute-names --omit-header --numeric /run/user/1000 | grep 1000001"))
     machine.send_chars("exit\n")
     machine.wait_until_fails("pgrep foot")
     # Verify acl cleanup on XDG_RUNTIME_DIR:
-    machine.wait_until_fails("getfacl --absolute-names --omit-header --numeric /run/user/1000 | grep 1000000")
+    machine.wait_until_fails("getfacl --absolute-names --omit-header --numeric /run/user/1000 | grep 1000001")
 
-    # Start a terminal (foot) within fortify from a terminal:
-    swaymsg("exec foot $SHELL -c '(fortify run --wayland foot) & sleep 1 && fortify show $(fortify ps --short) && touch /tmp/ps-show-ok && cat'")
-    wait_for_window("u0_a0@machine")
+    # Start app (foot) with Wayland enablement from a terminal:
+    swaymsg("exec foot $SHELL -c '(ne-foot) & sleep 1 && fortify show $(fortify ps --short) && touch /tmp/ps-show-ok && cat'")
+    wait_for_window("u0_a1@machine")
     machine.send_chars("clear; wayland-info && touch /tmp/success-client-term\n")
-    machine.wait_for_file("/tmp/fortify.1000/tmpdir/0/success-client-term")
+    machine.wait_for_file("/tmp/fortify.1000/tmpdir/1/success-client-term")
     machine.wait_for_file("/tmp/ps-show-ok")
-    collect_state_ui("foot_wayland_permissive_term")
-    check_state(["foot"], 1)
+    collect_state_ui("foot_wayland_term")
+    check_state("ne-foot", 1)
     machine.send_chars("exit\n")
     wait_for_window("foot")
     machine.send_key("ctrl-c")
     machine.wait_until_fails("pgrep foot")
 
     # Test PulseAudio (fortify does not support PipeWire yet):
-    fortify("run --wayland --pulse foot")
-    wait_for_window("u0_a0@machine")
+    swaymsg("exec pa-foot")
+    wait_for_window("u0_a2@machine")
     machine.send_chars("clear; pactl info && touch /tmp/success-pulse\n")
-    machine.wait_for_file("/tmp/fortify.1000/tmpdir/0/success-pulse")
+    machine.wait_for_file("/tmp/fortify.1000/tmpdir/2/success-pulse")
     collect_state_ui("pulse_wayland")
-    check_state(["foot"], 9)
+    check_state("pa-foot", 9)
     machine.send_chars("exit\n")
     machine.wait_until_fails("pgrep foot")
 
     # Test XWayland (foot does not support X):
-    fortify("run -X alacritty")
-    wait_for_window("u0_a0@machine")
+    swaymsg("exec x11-alacritty")
+    wait_for_window("u0_a3@machine")
     machine.send_chars("clear; glinfo && touch /tmp/success-client-x11\n")
-    machine.wait_for_file("/tmp/fortify.1000/tmpdir/0/success-client-x11")
-    collect_state_ui("alacritty_x11_permissive")
-    check_state(["alacritty"], 2)
+    machine.wait_for_file("/tmp/fortify.1000/tmpdir/3/success-client-x11")
+    collect_state_ui("alacritty_x11")
+    check_state("x11-alacritty", 2)
     machine.send_chars("exit\n")
     machine.wait_until_fails("pgrep alacritty")