diff --git a/README.md b/README.md
index bfda243..281aa8a 100644
--- a/README.md
+++ b/README.md
@@ -121,6 +121,7 @@ That being said, I am personally using all the blocks and services in this proje
 - [`jellyfin.nix`](./modules/services/jellyfin.nix) for watching media https://jellyfin.org/.
 - [Nextcloud Server](https://shb.skarabox.com/services-nextcloud.html) for private documents, contacts, calendar, etc https://nextcloud.com.
 - [`vaultwarden.nix`](./modules/services/vaultwarden.nix) for passwords https://github.com/dani-garcia/vaultwarden.
+- [`audiobookshelf.nix`](./modules/services/audiobookshelf.nix) for hosting podcasts and audio books https://www.audiobookshelf.org/.
 
 ## Demos
 
diff --git a/flake.nix b/flake.nix
index 2c171bb..86a25f8 100644
--- a/flake.nix
+++ b/flake.nix
@@ -46,6 +46,7 @@
         modules/services/jellyfin.nix
         modules/services/nextcloud-server.nix
         modules/services/vaultwarden.nix
+        modules/services/audiobookshelf.nix
       ];
     in
       {
@@ -99,6 +100,7 @@
               tests = pkgs.callPackage ./test/modules/lib.nix {};
             };
           }
+          // (vm_test "audiobookshelf" ./test/vm/audiobookshelf.nix)
           // (vm_test "authelia" ./test/vm/authelia.nix)
           // (vm_test "jellyfin" ./test/vm/jellyfin.nix)
           // (vm_test "ldap" ./test/vm/ldap.nix)
diff --git a/modules/services/audiobookshelf.nix b/modules/services/audiobookshelf.nix
new file mode 100644
index 0000000..c9a1140
--- /dev/null
+++ b/modules/services/audiobookshelf.nix
@@ -0,0 +1,160 @@
+{ config, pkgs, lib, ... }:
+
+let
+  cfg = config.shb.audiobookshelf;
+
+  contracts = pkgs.callPackage ../contracts {};
+
+  fqdn = "${cfg.subdomain}.${cfg.domain}";
+in
+{
+  options.shb.audiobookshelf = {
+    enable = lib.mkEnableOption "selfhostblocks.audiobookshelf";
+
+    subdomain = lib.mkOption {
+      type = lib.types.str;
+      description = "Subdomain under which audiobookshelf will be served.";
+      example = "abs";
+    };
+
+    domain = lib.mkOption {
+      type = lib.types.str;
+      description = "domain under which audiobookshelf will be served.";
+      example = "mydomain.com";
+    };
+
+    webPort = lib.mkOption {
+      type = lib.types.int;
+      description = "Audiobookshelf web port";
+      default = 8113;
+    };
+
+    ssl = lib.mkOption {
+      description = "Path to SSL files";
+      type = lib.types.nullOr contracts.ssl.certs;
+      default = null;
+    };
+
+    extraServiceConfig = lib.mkOption {
+      type = lib.types.attrsOf lib.types.str;
+      description = "Extra configuration given to the systemd service file.";
+      default = {};
+      example = lib.literalExpression ''
+      {
+        MemoryHigh = "512M";
+        MemoryMax = "900M";
+      }
+      '';
+    };
+
+    oidcProvider = lib.mkOption {
+      type = lib.types.str;
+      description = "OIDC provider name";
+      default = "Authelia";
+    };
+
+    authEndpoint = lib.mkOption {
+      type = lib.types.str;
+      description = "OIDC endpoint for SSO";
+      example = "https://authelia.example.com";
+    };
+
+    oidcClientID = lib.mkOption {
+      type = lib.types.str;
+      description = "Client ID for the OIDC endpoint";
+      default = "audiobookshelf";
+    };
+
+    oidcAdminUserGroup = lib.mkOption {
+      type = lib.types.str;
+      description = "OIDC admin group";
+      default = "audiobookshelf_admin";
+    };
+
+    oidcUserGroup = lib.mkOption {
+      type = lib.types.str;
+      description = "OIDC user group";
+      default = "audiobookshelf_user";
+    };
+
+    ssoSecretFile = lib.mkOption {
+      type = lib.types.path;
+      description = "File containing the SSO shared secret.";
+    };
+
+    logLevel = lib.mkOption {
+      type = lib.types.nullOr (lib.types.enum ["critical" "error" "warning" "info" "debug"]);
+      description = "Enable logging.";
+      default = false;
+      example = true;
+    };
+  };
+
+  config = lib.mkIf cfg.enable (lib.mkMerge [{
+
+    services.audiobookshelf = {
+      enable = true;
+      openFirewall = true;
+      dataDir = "audiobookshelf";
+      host = "127.0.0.1";
+      port = cfg.webPort;
+    };
+
+    services.nginx.enable = true;
+    services.nginx.virtualHosts."${fqdn}" = {
+      http2 = true;
+      forceSSL = !(isNull cfg.ssl);
+      sslCertificate = lib.mkIf (!(isNull cfg.ssl)) cfg.ssl.paths.cert;
+      sslCertificateKey = lib.mkIf (!(isNull cfg.ssl)) cfg.ssl.paths.key;
+
+      # https://github.com/advplyr/audiobookshelf#nginx-reverse-proxy
+      extraConfig = ''
+        set $audiobookshelf 127.0.0.1;
+        location / {
+             proxy_set_header X-Forwarded-For    $proxy_add_x_forwarded_for;
+             proxy_set_header  X-Forwarded-Proto $scheme;
+             proxy_set_header  Host              $host;
+             proxy_set_header Upgrade            $http_upgrade;
+             proxy_set_header Connection         "upgrade";
+
+             proxy_http_version                  1.1;
+
+             proxy_pass                          http://$audiobookshelf:${builtins.toString cfg.webPort};
+             proxy_redirect                      http:// https://;
+           }
+      '';
+    };
+
+    shb.authelia.oidcClients = [
+      {
+        id = cfg.oidcClientID;
+        description = "Audiobookshelf";
+        secret.source = cfg.ssoSecretFile;
+        public = false;
+        authorization_policy = "one_factor";
+        redirect_uris = [ 
+        "https://${cfg.subdomain}.${cfg.domain}/auth/openid/callback" 
+        "https://${cfg.subdomain}.${cfg.domain}/auth/openid/mobile-redirect" 
+        ];
+      }
+    ];
+    
+    # We want audiobookshelf to create files in the media group and to make those files group readable.
+    users.users.audiobookshelf = {
+      extraGroups = [ "media" ];
+    };
+    systemd.services.audiobookshelfd.serviceConfig.Group = lib.mkForce "media";
+    systemd.services.audiobookshelfd.serviceConfig.UMask = lib.mkForce "0027";
+
+    # We backup the whole audiobookshelf directory and set permissions for the backup user accordingly.
+    users.groups.audiobookshelf.members = [ "backup" ];
+    users.groups.media.members = [ "backup" ];
+    shb.backup.instances.audiobookshelf = {
+      sourceDirectories = [
+        /var/lib/${config.services.audiobookshelf.dataDir}
+      ];
+    };
+  } {
+    systemd.services.audiobookshelfd.serviceConfig = cfg.extraServiceConfig;
+  }]);
+}
diff --git a/test/vm/audiobookshelf.nix b/test/vm/audiobookshelf.nix
new file mode 100644
index 0000000..101bb73
--- /dev/null
+++ b/test/vm/audiobookshelf.nix
@@ -0,0 +1,243 @@
+{ pkgs, lib, ... }:
+{
+  basic = pkgs.nixosTest {
+    name = "audiobookshelf-basic";
+
+    nodes.server = { config, pkgs, ... }: {
+      imports = [
+        {
+          options = {
+            shb.backup = lib.mkOption { type = lib.types.anything; };
+            shb.authelia = lib.mkOption { type = lib.types.anything; };
+          };
+        }
+        ../../modules/services/audiobookshelf.nix
+      ];
+
+      shb.audiobookshelf = {
+        enable = true;
+        domain = "example.com";
+        subdomain = "a";
+      };
+      # Nginx port.
+      networking.firewall.allowedTCPPorts = [ 80 ];
+    };
+
+    nodes.client = {};
+
+    # TODO: Test login
+    testScript = { nodes, ... }: ''
+    import json
+
+    def curl(target, format, endpoint):
+        return json.loads(target.succeed(
+            "curl --fail-with-body --silent --show-error --output /dev/null --location"
+            + " --connect-to a.example.com:443:server:443"
+            + " --connect-to a.example.com:80:server:80"
+            + f" --write-out '{format}'"
+            + " " + endpoint
+        ))
+
+    start_all()
+    server.wait_for_unit("audiobookshelf.service")
+    server.wait_for_unit("nginx.service")
+    server.wait_for_open_port(${builtins.toString nodes.server.shb.audiobookshelf.webPort})
+
+    response = curl(client, """{"code":%{response_code}}""", "http://a.example.com")
+
+    if response['code'] != 200:
+        raise Exception(f"Code is {response['code']}")
+    '';
+  };
+
+  cert = pkgs.nixosTest {
+    name = "audiobookshelf-cert";
+
+    nodes.server = { config, pkgs, ... }: {
+      imports = [
+        {
+          options = {
+            shb.backup = lib.mkOption { type = lib.types.anything; };
+            shb.authelia = lib.mkOption { type = lib.types.anything; };
+          };
+        }
+        ../../modules/blocks/nginx.nix
+        ../../modules/blocks/ssl.nix
+        ../../modules/services/audiobookshelf.nix
+      ];
+
+      shb.certs = {
+        cas.selfsigned.myca = {
+          name = "My CA";
+        };
+        certs.selfsigned = {
+          n = {
+            ca = config.shb.certs.cas.selfsigned.myca;
+            domain = "*.example.com";
+            group = "nginx";
+          };
+        };
+      };
+
+      systemd.services.nginx.after = [ config.shb.certs.certs.selfsigned.n.systemdService ];
+      systemd.services.nginx.requires = [ config.shb.certs.certs.selfsigned.n.systemdService ];
+
+      shb.audiobookshelf = {
+        enable = true;
+        domain = "example.com";
+        subdomain = "a";
+        ssl = config.shb.certs.certs.selfsigned.n;
+      };
+      # Nginx port.
+      networking.firewall.allowedTCPPorts = [ 80 443 ];
+
+      shb.nginx.accessLog = true;
+    };
+
+    nodes.client = {};
+
+    # TODO: Test login
+    testScript = { nodes, ... }: ''
+    import json
+    import os
+    import pathlib
+
+    def curl(target, format, endpoint):
+        return json.loads(target.succeed(
+            "curl --fail-with-body --silent --show-error --output /dev/null --location"
+            + " --connect-to a.example.com:443:server:443"
+            + " --connect-to a.example.com:80:server:80"
+            + f" --write-out '{format}'"
+            + " " + endpoint
+        ))
+
+    start_all()
+    server.wait_for_unit("audiobookshelf.service")
+    server.wait_for_unit("nginx.service")
+    server.wait_for_open_port(${builtins.toString nodes.server.shb.audiobookshelf.webPort})
+
+    server.copy_from_vm("/etc/ssl/certs/ca-certificates.crt")
+    client.succeed("rm -r /etc/ssl/certs")
+    client.copy_from_host(str(pathlib.Path(os.environ.get("out", os.getcwd())) / "ca-certificates.crt"), "/etc/ssl/certs/ca-certificates.crt")
+
+    response = curl(client, """{"code":%{response_code}}""", "https://a.example.com")
+
+    if response['code'] != 200:
+        raise Exception(f"Code is {response['code']}")
+    '';
+  };
+
+  sso = pkgs.nixosTest {
+    name = "audiobookshelf-sso";
+
+    nodes.server = { config, pkgs, ... }: {
+      imports = [
+        {
+          options = {
+            shb.backup = lib.mkOption { type = lib.types.anything; };
+          };
+        }
+        ../../modules/blocks/authelia.nix
+        ../../modules/blocks/ldap.nix
+        ../../modules/blocks/postgresql.nix
+        ../../modules/blocks/ssl.nix
+        ../../modules/services/audiobookshelf.nix
+      ];
+
+      shb.ldap = {
+        enable = true;
+        domain = "example.com";
+        subdomain = "ldap";
+        ldapPort = 3890;
+        webUIListenPort = 17170;
+        dcdomain = "dc=example,dc=com";
+        ldapUserPasswordFile = pkgs.writeText "ldapUserPassword" "ldapUserPassword";
+        jwtSecretFile = pkgs.writeText "jwtSecret" "jwtSecret";
+      };
+
+      shb.certs = {
+        cas.selfsigned.myca = {
+          name = "My CA";
+        };
+        certs.selfsigned = {
+          n = {
+            ca = config.shb.certs.cas.selfsigned.myca;
+            domain = "*.example.com";
+            group = "nginx";
+          };
+        };
+      };
+
+      systemd.services.nginx.after = [ config.shb.certs.certs.selfsigned.n.systemdService ];
+      systemd.services.nginx.requires = [ config.shb.certs.certs.selfsigned.n.systemdService ];
+
+      shb.authelia = {
+        enable = true;
+        domain = "example.com";
+        subdomain = "auth";
+        ssl = config.shb.certs.certs.selfsigned.n;
+
+        ldapEndpoint = "ldap://127.0.0.1:${builtins.toString config.shb.ldap.ldapPort}";
+        dcdomain = config.shb.ldap.dcdomain;
+
+        secrets = {
+          jwtSecretFile = pkgs.writeText "jwtSecret" "jwtSecret";
+          ldapAdminPasswordFile = pkgs.writeText "ldapUserPassword" "ldapUserPassword";
+          sessionSecretFile = pkgs.writeText "sessionSecret" "sessionSecret";
+          storageEncryptionKeyFile = pkgs.writeText "storageEncryptionKey" "storageEncryptionKey";
+          identityProvidersOIDCHMACSecretFile = pkgs.writeText "identityProvidersOIDCHMACSecret" "identityProvidersOIDCHMACSecret";
+          identityProvidersOIDCIssuerPrivateKeyFile = (pkgs.runCommand "gen-private-key" {} ''
+          mkdir $out
+          ${pkgs.openssl}/bin/openssl genrsa -out $out/private.pem 4096
+          '') + "/private.pem";
+        };
+      };
+
+      shb.audiobookshelf = {
+        enable = true;
+        domain = "example.com";
+        subdomain = "a";
+        ssl = config.shb.certs.certs.selfsigned.n;
+
+        authEndpoint = "https://${config.shb.authelia.subdomain}.${config.shb.authelia.domain}";
+        ssoSecretFile = pkgs.writeText "ssoSecretFile" "ssoSecretFile";
+      };
+      # Nginx port.
+      networking.firewall.allowedTCPPorts = [ 80 443 ];
+    };
+
+    nodes.client = {};
+
+    # TODO: Test login with ldap user
+    testScript = { nodes, ... }: ''
+    import json
+    import os
+    import pathlib
+
+    def curl(target, format, endpoint):
+        return json.loads(target.succeed(
+            "curl --fail-with-body --silent --show-error --output /dev/null --location"
+            + " --connect-to a.example.com:443:server:443"
+            + " --connect-to a.example.com:80:server:80"
+            + f" --write-out '{format}'"
+            + " " + endpoint
+        ))
+
+    start_all()
+    server.wait_for_unit("audiobookshelf.service")
+    server.wait_for_unit("nginx.service")
+    server.wait_for_unit("lldap.service")
+    server.wait_for_unit("authelia-auth.example.com.service")
+    server.wait_for_open_port(${builtins.toString nodes.server.shb.audiobookshelf.webPort})
+
+    server.copy_from_vm("/etc/ssl/certs/ca-certificates.crt")
+    client.succeed("rm -r /etc/ssl/certs")
+    client.copy_from_host(str(pathlib.Path(os.environ.get("out", os.getcwd())) / "ca-certificates.crt"), "/etc/ssl/certs/ca-certificates.crt")
+
+    response = curl(client, """{"code":%{response_code}}""", "https://a.example.com")
+
+    if response['code'] != 200:
+        raise Exception(f"Code is {response['code']}")
+    '';
+  };
+}