diff --git a/.gitmodules b/.gitmodules
index 3a49c4874..f3051cca0 100644
--- a/.gitmodules
+++ b/.gitmodules
@@ -46,3 +46,9 @@
 [submodule "sirit"]
     path = externals/sirit
     url = https://github.com/ReinUsesLisp/sirit
+[submodule "libzip"]
+	path = externals/libzip
+	url = https://github.com/DarkLordZach/libzip
+[submodule "zlib"]
+	path = externals/zlib
+	url = https://github.com/DarkLordZach/zlib
diff --git a/CMakeLists.txt b/CMakeLists.txt
index bfa104034..9b3b0d6d5 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -21,6 +21,8 @@ option(YUZU_USE_BUNDLED_UNICORN "Build/Download bundled Unicorn" ON)
 
 option(YUZU_USE_QT_WEB_ENGINE "Use QtWebEngine for web applet implementation" OFF)
 
+option(YUZU_ENABLE_BOXCAT "Enable the Boxcat service, a yuzu high-level implementation of BCAT" ON)
+
 option(ENABLE_CUBEB "Enables the cubeb audio backend" ON)
 
 option(ENABLE_VULKAN "Enables Vulkan backend" ON)
diff --git a/externals/CMakeLists.txt b/externals/CMakeLists.txt
index e6fa11a03..d797d9fc9 100644
--- a/externals/CMakeLists.txt
+++ b/externals/CMakeLists.txt
@@ -77,6 +77,12 @@ if (ENABLE_VULKAN)
     add_subdirectory(sirit)
 endif()
 
+# libzip
+add_subdirectory(libzip)
+
+# zlib
+add_subdirectory(zlib)
+
 if (ENABLE_WEB_SERVICE)
     # LibreSSL
     set(LIBRESSL_SKIP_INSTALL ON CACHE BOOL "")
diff --git a/externals/libzip b/externals/libzip
new file mode 160000
index 000000000..bd7a8103e
--- /dev/null
+++ b/externals/libzip
@@ -0,0 +1 @@
+Subproject commit bd7a8103e96bc6d50164447f6b7b57bb786d8e2a
diff --git a/externals/zlib b/externals/zlib
new file mode 160000
index 000000000..094ed57db
--- /dev/null
+++ b/externals/zlib
@@ -0,0 +1 @@
+Subproject commit 094ed57db392170130bc710293568de7b576306d
diff --git a/src/core/CMakeLists.txt b/src/core/CMakeLists.txt
index a6b56c9c6..3416854db 100644
--- a/src/core/CMakeLists.txt
+++ b/src/core/CMakeLists.txt
@@ -1,3 +1,9 @@
+if (YUZU_ENABLE_BOXCAT)
+    set(BCAT_BOXCAT_ADDITIONAL_SOURCES hle/service/bcat/backend/boxcat.cpp hle/service/bcat/backend/boxcat.h)
+else()
+    set(BCAT_BOXCAT_ADDITIONAL_SOURCES)
+endif()
+
 add_library(core STATIC
     arm/arm_interface.h
     arm/arm_interface.cpp
@@ -82,6 +88,8 @@ add_library(core STATIC
     file_sys/vfs_concat.h
     file_sys/vfs_layered.cpp
     file_sys/vfs_layered.h
+    file_sys/vfs_libzip.cpp
+    file_sys/vfs_libzip.h
     file_sys/vfs_offset.cpp
     file_sys/vfs_offset.h
     file_sys/vfs_real.cpp
@@ -241,6 +249,9 @@ add_library(core STATIC
     hle/service/audio/errors.h
     hle/service/audio/hwopus.cpp
     hle/service/audio/hwopus.h
+    hle/service/bcat/backend/backend.cpp
+    hle/service/bcat/backend/backend.h
+    ${BCAT_BOXCAT_ADDITIONAL_SOURCES}
     hle/service/bcat/bcat.cpp
     hle/service/bcat/bcat.h
     hle/service/bcat/module.cpp
@@ -499,6 +510,15 @@ create_target_directory_groups(core)
 
 target_link_libraries(core PUBLIC common PRIVATE audio_core video_core)
 target_link_libraries(core PUBLIC Boost::boost PRIVATE fmt json-headers mbedtls opus unicorn open_source_archives)
+
+if (YUZU_ENABLE_BOXCAT)
+    get_directory_property(OPENSSL_LIBS
+        DIRECTORY ${PROJECT_SOURCE_DIR}/externals/libressl
+        DEFINITION OPENSSL_LIBS)
+    target_compile_definitions(core PRIVATE -DCPPHTTPLIB_OPENSSL_SUPPORT -DYUZU_ENABLE_BOXCAT)
+    target_link_libraries(core PRIVATE httplib json-headers ${OPENSSL_LIBS} zip)
+endif()
+
 if (ENABLE_WEB_SERVICE)
     target_compile_definitions(core PRIVATE -DENABLE_WEB_SERVICE)
     target_link_libraries(core PRIVATE web_service)
diff --git a/src/core/core.cpp b/src/core/core.cpp
index 92ba42fb9..75a7ffb97 100644
--- a/src/core/core.cpp
+++ b/src/core/core.cpp
@@ -339,6 +339,7 @@ struct System::Impl {
 
     std::unique_ptr<Memory::CheatEngine> cheat_engine;
     std::unique_ptr<Tools::Freezer> memory_freezer;
+    std::array<u8, 0x20> build_id{};
 
     /// Frontend applets
     Service::AM::Applets::AppletManager applet_manager;
@@ -640,6 +641,14 @@ bool System::GetExitLock() const {
     return impl->exit_lock;
 }
 
+void System::SetCurrentProcessBuildID(std::array<u8, 32> id) {
+    impl->build_id = id;
+}
+
+const std::array<u8, 32>& System::GetCurrentProcessBuildID() const {
+    return impl->build_id;
+}
+
 System::ResultStatus System::Init(Frontend::EmuWindow& emu_window) {
     return impl->Init(*this, emu_window);
 }
diff --git a/src/core/core.h b/src/core/core.h
index ff10ebe12..f49b7fbf9 100644
--- a/src/core/core.h
+++ b/src/core/core.h
@@ -330,6 +330,10 @@ public:
 
     bool GetExitLock() const;
 
+    void SetCurrentProcessBuildID(std::array<u8, 0x20> id);
+
+    const std::array<u8, 0x20>& GetCurrentProcessBuildID() const;
+
 private:
     System();
 
diff --git a/src/core/file_sys/bis_factory.cpp b/src/core/file_sys/bis_factory.cpp
index 8f758d6d9..0af44f340 100644
--- a/src/core/file_sys/bis_factory.cpp
+++ b/src/core/file_sys/bis_factory.cpp
@@ -136,4 +136,9 @@ u64 BISFactory::GetFullNANDTotalSpace() const {
     return static_cast<u64>(Settings::values.nand_total_size);
 }
 
+VirtualDir BISFactory::GetBCATDirectory(u64 title_id) const {
+    return GetOrCreateDirectoryRelative(nand_root,
+                                        fmt::format("/system/save/bcat/{:016X}", title_id));
+}
+
 } // namespace FileSys
diff --git a/src/core/file_sys/bis_factory.h b/src/core/file_sys/bis_factory.h
index bdfe728c9..8f0451c98 100644
--- a/src/core/file_sys/bis_factory.h
+++ b/src/core/file_sys/bis_factory.h
@@ -61,6 +61,8 @@ public:
     u64 GetUserNANDTotalSpace() const;
     u64 GetFullNANDTotalSpace() const;
 
+    VirtualDir GetBCATDirectory(u64 title_id) const;
+
 private:
     VirtualDir nand_root;
     VirtualDir load_root;
diff --git a/src/core/file_sys/vfs_libzip.cpp b/src/core/file_sys/vfs_libzip.cpp
new file mode 100644
index 000000000..8bdaa7e4a
--- /dev/null
+++ b/src/core/file_sys/vfs_libzip.cpp
@@ -0,0 +1,79 @@
+// Copyright 2019 yuzu emulator team
+// Licensed under GPLv2 or any later version
+// Refer to the license.txt file included.
+
+#include <string>
+#include <zip.h>
+#include "common/logging/backend.h"
+#include "core/file_sys/vfs.h"
+#include "core/file_sys/vfs_libzip.h"
+#include "core/file_sys/vfs_vector.h"
+
+namespace FileSys {
+
+VirtualDir ExtractZIP(VirtualFile file) {
+    zip_error_t error{};
+
+    const auto data = file->ReadAllBytes();
+    std::unique_ptr<zip_source_t, decltype(&zip_source_close)> src{
+        zip_source_buffer_create(data.data(), data.size(), 0, &error), zip_source_close};
+    if (src == nullptr)
+        return nullptr;
+
+    std::unique_ptr<zip_t, decltype(&zip_close)> zip{zip_open_from_source(src.get(), 0, &error),
+                                                     zip_close};
+    if (zip == nullptr)
+        return nullptr;
+
+    std::shared_ptr<VectorVfsDirectory> out = std::make_shared<VectorVfsDirectory>();
+
+    const auto num_entries = zip_get_num_entries(zip.get(), 0);
+
+    zip_stat_t stat{};
+    zip_stat_init(&stat);
+
+    for (std::size_t i = 0; i < num_entries; ++i) {
+        const auto stat_res = zip_stat_index(zip.get(), i, 0, &stat);
+        if (stat_res == -1)
+            return nullptr;
+
+        const std::string name(stat.name);
+        if (name.empty())
+            continue;
+
+        if (name.back() != '/') {
+            std::unique_ptr<zip_file_t, decltype(&zip_fclose)> file{
+                zip_fopen_index(zip.get(), i, 0), zip_fclose};
+
+            std::vector<u8> buf(stat.size);
+            if (zip_fread(file.get(), buf.data(), buf.size()) != buf.size())
+                return nullptr;
+
+            const auto parts = FileUtil::SplitPathComponents(stat.name);
+            const auto new_file = std::make_shared<VectorVfsFile>(buf, parts.back());
+
+            std::shared_ptr<VectorVfsDirectory> dtrv = out;
+            for (std::size_t j = 0; j < parts.size() - 1; ++j) {
+                if (dtrv == nullptr)
+                    return nullptr;
+                const auto subdir = dtrv->GetSubdirectory(parts[j]);
+                if (subdir == nullptr) {
+                    const auto temp = std::make_shared<VectorVfsDirectory>(
+                        std::vector<VirtualFile>{}, std::vector<VirtualDir>{}, parts[j]);
+                    dtrv->AddDirectory(temp);
+                    dtrv = temp;
+                } else {
+                    dtrv = std::dynamic_pointer_cast<VectorVfsDirectory>(subdir);
+                }
+            }
+
+            if (dtrv == nullptr)
+                return nullptr;
+            dtrv->AddFile(new_file);
+        }
+    }
+
+    return out;
+}
+
+} // namespace FileSys
diff --git a/src/core/file_sys/vfs_libzip.h b/src/core/file_sys/vfs_libzip.h
new file mode 100644
index 000000000..f68af576a
--- /dev/null
+++ b/src/core/file_sys/vfs_libzip.h
@@ -0,0 +1,13 @@
+// Copyright 2019 yuzu emulator team
+// Licensed under GPLv2 or any later version
+// Refer to the license.txt file included.
+
+#pragma once
+
+#include "core/file_sys/vfs_types.h"
+
+namespace FileSys {
+
+VirtualDir ExtractZIP(VirtualFile zip);
+
+} // namespace FileSys
diff --git a/src/core/hle/service/am/am.cpp b/src/core/hle/service/am/am.cpp
index 797c9a06f..34409e0c3 100644
--- a/src/core/hle/service/am/am.cpp
+++ b/src/core/hle/service/am/am.cpp
@@ -31,6 +31,7 @@
 #include "core/hle/service/am/tcap.h"
 #include "core/hle/service/apm/controller.h"
 #include "core/hle/service/apm/interface.h"
+#include "core/hle/service/bcat/backend/backend.h"
 #include "core/hle/service/filesystem/filesystem.h"
 #include "core/hle/service/ns/ns.h"
 #include "core/hle/service/nvflinger/nvflinger.h"
@@ -46,15 +47,20 @@ constexpr ResultCode ERR_NO_DATA_IN_CHANNEL{ErrorModule::AM, 0x2};
 constexpr ResultCode ERR_NO_MESSAGES{ErrorModule::AM, 0x3};
 constexpr ResultCode ERR_SIZE_OUT_OF_BOUNDS{ErrorModule::AM, 0x1F7};
 
-constexpr u32 POP_LAUNCH_PARAMETER_MAGIC = 0xC79497CA;
+enum class LaunchParameterKind : u32 {
+    ApplicationSpecific = 1,
+    AccountPreselectedUser = 2,
+};
 
-struct LaunchParameters {
+constexpr u32 LAUNCH_PARAMETER_ACCOUNT_PRESELECTED_USER_MAGIC = 0xC79497CA;
+
+struct LaunchParameterAccountPreselectedUser {
     u32_le magic;
     u32_le is_account_selected;
     u128 current_user;
     INSERT_PADDING_BYTES(0x70);
 };
-static_assert(sizeof(LaunchParameters) == 0x88);
+static_assert(sizeof(LaunchParameterAccountPreselectedUser) == 0x88);
 
 IWindowController::IWindowController(Core::System& system_)
     : ServiceFramework("IWindowController"), system{system_} {
@@ -1128,26 +1134,55 @@ void IApplicationFunctions::EndBlockingHomeButton(Kernel::HLERequestContext& ctx
 }
 
 void IApplicationFunctions::PopLaunchParameter(Kernel::HLERequestContext& ctx) {
-    LOG_DEBUG(Service_AM, "called");
+    IPC::RequestParser rp{ctx};
+    const auto kind = rp.PopEnum<LaunchParameterKind>();
 
-    LaunchParameters params{};
+    LOG_DEBUG(Service_AM, "called, kind={:08X}", static_cast<u8>(kind));
 
-    params.magic = POP_LAUNCH_PARAMETER_MAGIC;
-    params.is_account_selected = 1;
+    if (kind == LaunchParameterKind::ApplicationSpecific && !launch_popped_application_specific) {
+        const auto backend = BCAT::CreateBackendFromSettings(
+            [this](u64 tid) { return system.GetFileSystemController().GetBCATDirectory(tid); });
+        const auto build_id_full = Core::System::GetInstance().GetCurrentProcessBuildID();
+        u64 build_id{};
+        std::memcpy(&build_id, build_id_full.data(), sizeof(u64));
 
-    Account::ProfileManager profile_manager{};
-    const auto uuid = profile_manager.GetUser(Settings::values.current_user);
-    ASSERT(uuid);
-    params.current_user = uuid->uuid;
+        const auto data =
+            backend->GetLaunchParameter({Core::CurrentProcess()->GetTitleID(), build_id});
 
-    IPC::ResponseBuilder rb{ctx, 2, 0, 1};
+        if (data.has_value()) {
+            IPC::ResponseBuilder rb{ctx, 2, 0, 1};
+            rb.Push(RESULT_SUCCESS);
+            rb.PushIpcInterface<AM::IStorage>(*data);
+            launch_popped_application_specific = true;
+            return;
+        }
+    } else if (kind == LaunchParameterKind::AccountPreselectedUser &&
+               !launch_popped_account_preselect) {
+        LaunchParameterAccountPreselectedUser params{};
 
-    rb.Push(RESULT_SUCCESS);
+        params.magic = LAUNCH_PARAMETER_ACCOUNT_PRESELECTED_USER_MAGIC;
+        params.is_account_selected = 1;
 
-    std::vector<u8> buffer(sizeof(LaunchParameters));
-    std::memcpy(buffer.data(), &params, buffer.size());
+        Account::ProfileManager profile_manager{};
+        const auto uuid = profile_manager.GetUser(Settings::values.current_user);
+        ASSERT(uuid);
+        params.current_user = uuid->uuid;
 
-    rb.PushIpcInterface<AM::IStorage>(buffer);
+        IPC::ResponseBuilder rb{ctx, 2, 0, 1};
+
+        rb.Push(RESULT_SUCCESS);
+
+        std::vector<u8> buffer(sizeof(LaunchParameterAccountPreselectedUser));
+        std::memcpy(buffer.data(), &params, buffer.size());
+
+        rb.PushIpcInterface<AM::IStorage>(buffer);
+        launch_popped_account_preselect = true;
+        return;
+    }
+
+    LOG_ERROR(Service_AM, "Attempted to load launch parameter but none was found!");
+    IPC::ResponseBuilder rb{ctx, 2};
+    rb.Push(ERR_NO_DATA_IN_CHANNEL);
 }
 
 void IApplicationFunctions::CreateApplicationAndRequestToStartForQuest(
diff --git a/src/core/hle/service/am/am.h b/src/core/hle/service/am/am.h
index a3baeb673..9169eb2bd 100644
--- a/src/core/hle/service/am/am.h
+++ b/src/core/hle/service/am/am.h
@@ -255,6 +255,8 @@ private:
     void EnableApplicationCrashReport(Kernel::HLERequestContext& ctx);
     void GetGpuErrorDetectedSystemEvent(Kernel::HLERequestContext& ctx);
 
+    bool launch_popped_application_specific = false;
+    bool launch_popped_account_preselect = false;
     Kernel::EventPair gpu_error_detected_event;
     Core::System& system;
 };
diff --git a/src/core/hle/service/am/applets/applets.cpp b/src/core/hle/service/am/applets/applets.cpp
index d2e35362f..720fe766f 100644
--- a/src/core/hle/service/am/applets/applets.cpp
+++ b/src/core/hle/service/am/applets/applets.cpp
@@ -157,6 +157,10 @@ AppletManager::AppletManager(Core::System& system_) : system{system_} {}
 
 AppletManager::~AppletManager() = default;
 
+const AppletFrontendSet& AppletManager::GetAppletFrontendSet() const {
+    return frontend;
+}
+
 void AppletManager::SetAppletFrontendSet(AppletFrontendSet set) {
     if (set.parental_controls != nullptr)
         frontend.parental_controls = std::move(set.parental_controls);
diff --git a/src/core/hle/service/am/applets/applets.h b/src/core/hle/service/am/applets/applets.h
index 764c3418c..226be88b1 100644
--- a/src/core/hle/service/am/applets/applets.h
+++ b/src/core/hle/service/am/applets/applets.h
@@ -190,6 +190,8 @@ public:
     explicit AppletManager(Core::System& system_);
     ~AppletManager();
 
+    const AppletFrontendSet& GetAppletFrontendSet() const;
+
     void SetAppletFrontendSet(AppletFrontendSet set);
     void SetDefaultAppletFrontendSet();
     void SetDefaultAppletsIfMissing();
diff --git a/src/core/hle/service/bcat/backend/backend.cpp b/src/core/hle/service/bcat/backend/backend.cpp
new file mode 100644
index 000000000..9b677debe
--- /dev/null
+++ b/src/core/hle/service/bcat/backend/backend.cpp
@@ -0,0 +1,136 @@
+// Copyright 2019 yuzu emulator team
+// Licensed under GPLv2 or any later version
+// Refer to the license.txt file included.
+
+#include "common/hex_util.h"
+#include "common/logging/log.h"
+#include "core/core.h"
+#include "core/hle/lock.h"
+#include "core/hle/service/bcat/backend/backend.h"
+
+namespace Service::BCAT {
+
+ProgressServiceBackend::ProgressServiceBackend(std::string event_name) : impl{} {
+    auto& kernel{Core::System::GetInstance().Kernel()};
+    event = Kernel::WritableEvent::CreateEventPair(
+        kernel, Kernel::ResetType::Automatic, "ProgressServiceBackend:UpdateEvent:" + event_name);
+}
+
+Kernel::SharedPtr<Kernel::ReadableEvent> ProgressServiceBackend::GetEvent() {
+    return event.readable;
+}
+
+DeliveryCacheProgressImpl& ProgressServiceBackend::GetImpl() {
+    return impl;
+}
+
+void ProgressServiceBackend::SetNeedHLELock(bool need) {
+    need_hle_lock = need;
+}
+
+void ProgressServiceBackend::SetTotalSize(u64 size) {
+    impl.total_bytes = size;
+    SignalUpdate();
+}
+
+void ProgressServiceBackend::StartConnecting() {
+    impl.status = DeliveryCacheProgressImpl::Status::Connecting;
+    SignalUpdate();
+}
+
+void ProgressServiceBackend::StartProcessingDataList() {
+    impl.status = DeliveryCacheProgressImpl::Status::ProcessingDataList;
+    SignalUpdate();
+}
+
+void ProgressServiceBackend::StartDownloadingFile(std::string_view dir_name,
+                                                  std::string_view file_name, u64 file_size) {
+    impl.status = DeliveryCacheProgressImpl::Status::Downloading;
+    impl.current_downloaded_bytes = 0;
+    impl.current_total_bytes = file_size;
+    std::memcpy(impl.current_directory.data(), dir_name.data(),
+                std::min<u64>(dir_name.size(), 0x31ull));
+    std::memcpy(impl.current_file.data(), file_name.data(),
+                std::min<u64>(file_name.size(), 0x31ull));
+    SignalUpdate();
+}
+
+void ProgressServiceBackend::UpdateFileProgress(u64 downloaded) {
+    impl.current_downloaded_bytes = downloaded;
+    SignalUpdate();
+}
+
+void ProgressServiceBackend::FinishDownloadingFile() {
+    impl.total_downloaded_bytes += impl.current_total_bytes;
+    SignalUpdate();
+}
+
+void ProgressServiceBackend::CommitDirectory(std::string_view dir_name) {
+    impl.status = DeliveryCacheProgressImpl::Status::Committing;
+    impl.current_file.fill(0);
+    impl.current_downloaded_bytes = 0;
+    impl.current_total_bytes = 0;
+    std::memcpy(impl.current_directory.data(), dir_name.data(),
+                std::min<u64>(dir_name.size(), 0x31ull));
+    SignalUpdate();
+}
+
+void ProgressServiceBackend::FinishDownload(ResultCode result) {
+    impl.total_downloaded_bytes = impl.total_bytes;
+    impl.status = DeliveryCacheProgressImpl::Status::Done;
+    impl.result = result;
+    SignalUpdate();
+}
+
+void ProgressServiceBackend::SignalUpdate() const {
+    if (need_hle_lock) {
+        std::lock_guard<std::recursive_mutex> lock(HLE::g_hle_lock);
+        event.writable->Signal();
+    } else {
+        event.writable->Signal();
+    }
+}
+
+Backend::Backend(DirectoryGetter getter) : dir_getter(std::move(getter)) {}
+
+Backend::~Backend() = default;
+
+NullBackend::NullBackend(const DirectoryGetter& getter) : Backend(std::move(getter)) {}
+
+NullBackend::~NullBackend() = default;
+
+bool NullBackend::Synchronize(TitleIDVersion title, ProgressServiceBackend& progress) {
+    LOG_DEBUG(Service_BCAT, "called, title_id={:016X}, build_id={:016X}", title.title_id,
+              title.build_id);
+
+    progress.FinishDownload(RESULT_SUCCESS);
+    return true;
+}
+
+bool NullBackend::SynchronizeDirectory(TitleIDVersion title, std::string name,
+                                       ProgressServiceBackend& progress) {
+    LOG_DEBUG(Service_BCAT, "called, title_id={:016X}, build_id={:016X}, name={}", title.title_id,
+              title.build_id, name);
+
+    progress.FinishDownload(RESULT_SUCCESS);
+    return true;
+}
+
+bool NullBackend::Clear(u64 title_id) {
+    LOG_DEBUG(Service_BCAT, "called, title_id={:016X}");
+
+    return true;
+}
+
+void NullBackend::SetPassphrase(u64 title_id, const Passphrase& passphrase) {
+    LOG_DEBUG(Service_BCAT, "called, title_id={:016X}, passphrase = {}", title_id,
+              Common::HexToString(passphrase));
+}
+
+std::optional<std::vector<u8>> NullBackend::GetLaunchParameter(TitleIDVersion title) {
+    LOG_DEBUG(Service_BCAT, "called, title_id={:016X}, build_id={:016X}", title.title_id,
+              title.build_id);
+    return std::nullopt;
+}
+
+} // namespace Service::BCAT
diff --git a/src/core/hle/service/bcat/backend/backend.h b/src/core/hle/service/bcat/backend/backend.h
new file mode 100644
index 000000000..3f5d8b5dd
--- /dev/null
+++ b/src/core/hle/service/bcat/backend/backend.h
@@ -0,0 +1,147 @@
+// Copyright 2019 yuzu emulator team
+// Licensed under GPLv2 or any later version
+// Refer to the license.txt file included.
+
+#pragma once
+
+#include <functional>
+#include <optional>
+#include "common/common_types.h"
+#include "core/file_sys/vfs_types.h"
+#include "core/hle/kernel/readable_event.h"
+#include "core/hle/kernel/writable_event.h"
+#include "core/hle/result.h"
+
+namespace Service::BCAT {
+
+struct DeliveryCacheProgressImpl;
+
+using DirectoryGetter = std::function<FileSys::VirtualDir(u64)>;
+using Passphrase = std::array<u8, 0x20>;
+
+struct TitleIDVersion {
+    u64 title_id;
+    u64 build_id;
+};
+
+using DirectoryName = std::array<char, 0x20>;
+using FileName = std::array<char, 0x20>;
+
+struct DeliveryCacheProgressImpl {
+    enum class Status : s32 {
+        None = 0x0,
+        Queued = 0x1,
+        Connecting = 0x2,
+        ProcessingDataList = 0x3,
+        Downloading = 0x4,
+        Committing = 0x5,
+        Done = 0x9,
+    };
+
+    Status status;
+    ResultCode result = RESULT_SUCCESS;
+    DirectoryName current_directory;
+    FileName current_file;
+    s64 current_downloaded_bytes; ///< Bytes downloaded on current file.
+    s64 current_total_bytes;      ///< Bytes total on current file.
+    s64 total_downloaded_bytes;   ///< Bytes downloaded on overall download.
+    s64 total_bytes;              ///< Bytes total on overall download.
+    INSERT_PADDING_BYTES(
+        0x198); ///< Appears to be unused in official code, possibly reserved for future use.
+};
+static_assert(sizeof(DeliveryCacheProgressImpl) == 0x200,
+              "DeliveryCacheProgressImpl has incorrect size.");
+
+// A class to manage the signalling to the game about BCAT download progress.
+// Some of this class is implemented in module.cpp to avoid exposing the implementation structure.
+class ProgressServiceBackend {
+    friend class IBcatService;
+
+public:
+    // Clients should call this with true if any of the functions are going to be called from a
+    // non-HLE thread and this class need to lock the hle mutex. (default is false)
+    void SetNeedHLELock(bool need);
+
+    // Sets the number of bytes total in the entire download.
+    void SetTotalSize(u64 size);
+
+    // Notifies the application that the backend has started connecting to the server.
+    void StartConnecting();
+    // Notifies the application that the backend has begun accumulating and processing metadata.
+    void StartProcessingDataList();
+
+    // Notifies the application that a file is starting to be downloaded.
+    void StartDownloadingFile(std::string_view dir_name, std::string_view file_name, u64 file_size);
+    // Updates the progress of the current file to the size passed.
+    void UpdateFileProgress(u64 downloaded);
+    // Notifies the application that the current file has completed download.
+    void FinishDownloadingFile();
+
+    // Notifies the application that all files in this directory have completed and are being
+    // finalized.
+    void CommitDirectory(std::string_view dir_name);
+
+    // Notifies the application that the operation completed with result code result.
+    void FinishDownload(ResultCode result);
+
+private:
+    explicit ProgressServiceBackend(std::string event_name);
+
+    Kernel::SharedPtr<Kernel::ReadableEvent> GetEvent();
+    DeliveryCacheProgressImpl& GetImpl();
+
+    void SignalUpdate() const;
+
+    DeliveryCacheProgressImpl impl;
+    Kernel::EventPair event;
+    bool need_hle_lock = false;
+};
+
+// A class representing an abstract backend for BCAT functionality.
+class Backend {
+public:
+    explicit Backend(DirectoryGetter getter);
+    virtual ~Backend();
+
+    // Called when the backend is needed to synchronize the data for the game with title ID and
+    // version in title. A ProgressServiceBackend object is provided to alert the application of
+    // status.
+    virtual bool Synchronize(TitleIDVersion title, ProgressServiceBackend& progress) = 0;
+    // Very similar to Synchronize, but only for the directory provided. Backends should not alter
+    // the data for any other directories.
+    virtual bool SynchronizeDirectory(TitleIDVersion title, std::string name,
+                                      ProgressServiceBackend& progress) = 0;
+
+    // Removes all cached data associated with title id provided.
+    virtual bool Clear(u64 title_id) = 0;
+
+    // Sets the BCAT Passphrase to be used with the associated title ID.
+    virtual void SetPassphrase(u64 title_id, const Passphrase& passphrase) = 0;
+
+    // Gets the launch parameter used by AM associated with the title ID and version provided.
+    virtual std::optional<std::vector<u8>> GetLaunchParameter(TitleIDVersion title) = 0;
+
+protected:
+    DirectoryGetter dir_getter;
+};
+
+// A backend of BCAT that provides no operation.
+class NullBackend : public Backend {
+public:
+    explicit NullBackend(const DirectoryGetter& getter);
+    ~NullBackend() override;
+
+    bool Synchronize(TitleIDVersion title, ProgressServiceBackend& progress) override;
+    bool SynchronizeDirectory(TitleIDVersion title, std::string name,
+                              ProgressServiceBackend& progress) override;
+
+    bool Clear(u64 title_id) override;
+
+    void SetPassphrase(u64 title_id, const Passphrase& passphrase) override;
+
+    std::optional<std::vector<u8>> GetLaunchParameter(TitleIDVersion title) override;
+};
+
+std::unique_ptr<Backend> CreateBackendFromSettings(DirectoryGetter getter);
+
+} // namespace Service::BCAT
diff --git a/src/core/hle/service/bcat/backend/boxcat.cpp b/src/core/hle/service/bcat/backend/boxcat.cpp
new file mode 100644
index 000000000..e6ee0810b
--- /dev/null
+++ b/src/core/hle/service/bcat/backend/boxcat.cpp
@@ -0,0 +1,503 @@
+// Copyright 2019 yuzu emulator team
+// Licensed under GPLv2 or any later version
+// Refer to the license.txt file included.
+
+#include <fmt/ostream.h>
+#include <httplib.h>
+#include <json.hpp>
+#include <mbedtls/sha256.h>
+#include "common/hex_util.h"
+#include "common/logging/backend.h"
+#include "common/logging/log.h"
+#include "core/core.h"
+#include "core/file_sys/vfs.h"
+#include "core/file_sys/vfs_libzip.h"
+#include "core/file_sys/vfs_vector.h"
+#include "core/frontend/applets/error.h"
+#include "core/hle/service/am/applets/applets.h"
+#include "core/hle/service/bcat/backend/boxcat.h"
+#include "core/settings.h"
+
+namespace {
+
+// Prevents conflicts with windows macro called CreateFile
+FileSys::VirtualFile VfsCreateFileWrap(FileSys::VirtualDir dir, std::string_view name) {
+    return dir->CreateFile(name);
+}
+
+// Prevents conflicts with windows macro called DeleteFile
+bool VfsDeleteFileWrap(FileSys::VirtualDir dir, std::string_view name) {
+    return dir->DeleteFile(name);
+}
+
+} // Anonymous namespace
+
+namespace Service::BCAT {
+
+constexpr ResultCode ERROR_GENERAL_BCAT_FAILURE{ErrorModule::BCAT, 1};
+
+constexpr char BOXCAT_HOSTNAME[] = "api.yuzu-emu.org";
+
+// Formatted using fmt with arg[0] = hex title id
+constexpr char BOXCAT_PATHNAME_DATA[] = "/game-assets/{:016X}/boxcat";
+constexpr char BOXCAT_PATHNAME_LAUNCHPARAM[] = "/game-assets/{:016X}/launchparam";
+
+constexpr char BOXCAT_PATHNAME_EVENTS[] = "/game-assets/boxcat/events";
+
+constexpr char BOXCAT_API_VERSION[] = "1";
+constexpr char BOXCAT_CLIENT_TYPE[] = "yuzu";
+
+// HTTP status codes for Boxcat
+enum class ResponseStatus {
+    Ok = 200,               ///< Operation completed successfully.
+    BadClientVersion = 301, ///< The Boxcat-Client-Version doesn't match the server.
+    NoUpdate = 304,         ///< The digest provided would match the new data, no need to update.
+    NoMatchTitleId = 404,   ///< The title ID provided doesn't have a boxcat implementation.
+    NoMatchBuildId = 406,   ///< The build ID provided is blacklisted (potentially because of format
+                            ///< issues or whatnot) and has no data.
+};
+
+enum class DownloadResult {
+    Success = 0,
+    NoResponse,
+    GeneralWebError,
+    NoMatchTitleId,
+    NoMatchBuildId,
+    InvalidContentType,
+    GeneralFSError,
+    BadClientVersion,
+};
+
+constexpr std::array<const char*, 8> DOWNLOAD_RESULT_LOG_MESSAGES{
+    "Success",
+    "There was no response from the server.",
+    "There was a general web error code returned from the server.",
+    "The title ID of the current game doesn't have a boxcat implementation. If you believe an "
+    "implementation should be added, contact yuzu support.",
+    "The build ID of the current version of the game is marked as incompatible with the current "
+    "BCAT distribution. Try upgrading or downgrading your game version or contacting yuzu support.",
+    "The content type of the web response was invalid.",
+    "There was a general filesystem error while saving the zip file.",
+    "The server is either too new or too old to serve the request. Try using the latest version of "
+    "an official release of yuzu.",
+};
+
+std::ostream& operator<<(std::ostream& os, DownloadResult result) {
+    return os << DOWNLOAD_RESULT_LOG_MESSAGES.at(static_cast<std::size_t>(result));
+}
+
+constexpr u32 PORT = 443;
+constexpr u32 TIMEOUT_SECONDS = 30;
+constexpr u64 VFS_COPY_BLOCK_SIZE = 1ull << 24; // 4MB
+
+namespace {
+
+std::string GetBINFilePath(u64 title_id) {
+    return fmt::format("{}bcat/{:016X}/launchparam.bin",
+                       FileUtil::GetUserPath(FileUtil::UserPath::CacheDir), title_id);
+}
+
+std::string GetZIPFilePath(u64 title_id) {
+    return fmt::format("{}bcat/{:016X}/data.zip",
+                       FileUtil::GetUserPath(FileUtil::UserPath::CacheDir), title_id);
+}
+
+// If the error is something the user should know about (build ID mismatch, bad client version),
+// display an error.
+void HandleDownloadDisplayResult(DownloadResult res) {
+    if (res == DownloadResult::Success || res == DownloadResult::NoResponse ||
+        res == DownloadResult::GeneralWebError || res == DownloadResult::GeneralFSError ||
+        res == DownloadResult::NoMatchTitleId || res == DownloadResult::InvalidContentType) {
+        return;
+    }
+
+    const auto& frontend{Core::System::GetInstance().GetAppletManager().GetAppletFrontendSet()};
+    frontend.error->ShowCustomErrorText(
+        ResultCode(-1), "There was an error while attempting to use Boxcat.",
+        DOWNLOAD_RESULT_LOG_MESSAGES[static_cast<std::size_t>(res)], [] {});
+}
+
+bool VfsRawCopyProgress(FileSys::VirtualFile src, FileSys::VirtualFile dest,
+                        std::string_view dir_name, ProgressServiceBackend& progress,
+                        std::size_t block_size = 0x1000) {
+    if (src == nullptr || dest == nullptr || !src->IsReadable() || !dest->IsWritable())
+        return false;
+    if (!dest->Resize(src->GetSize()))
+        return false;
+
+    progress.StartDownloadingFile(dir_name, src->GetName(), src->GetSize());
+
+    std::vector<u8> temp(std::min(block_size, src->GetSize()));
+    for (std::size_t i = 0; i < src->GetSize(); i += block_size) {
+        const auto read = std::min(block_size, src->GetSize() - i);
+
+        if (src->Read(temp.data(), read, i) != read) {
+            return false;
+        }
+
+        if (dest->Write(temp.data(), read, i) != read) {
+            return false;
+        }
+
+        progress.UpdateFileProgress(i);
+    }
+
+    progress.FinishDownloadingFile();
+
+    return true;
+}
+
+bool VfsRawCopyDProgressSingle(FileSys::VirtualDir src, FileSys::VirtualDir dest,
+                               ProgressServiceBackend& progress, std::size_t block_size = 0x1000) {
+    if (src == nullptr || dest == nullptr || !src->IsReadable() || !dest->IsWritable())
+        return false;
+
+    for (const auto& file : src->GetFiles()) {
+        const auto out_file = VfsCreateFileWrap(dest, file->GetName());
+        if (!VfsRawCopyProgress(file, out_file, src->GetName(), progress, block_size)) {
+            return false;
+        }
+    }
+    progress.CommitDirectory(src->GetName());
+
+    return true;
+}
+
+bool VfsRawCopyDProgress(FileSys::VirtualDir src, FileSys::VirtualDir dest,
+                         ProgressServiceBackend& progress, std::size_t block_size = 0x1000) {
+    if (src == nullptr || dest == nullptr || !src->IsReadable() || !dest->IsWritable())
+        return false;
+
+    for (const auto& dir : src->GetSubdirectories()) {
+        const auto out = dest->CreateSubdirectory(dir->GetName());
+        if (!VfsRawCopyDProgressSingle(dir, out, progress, block_size)) {
+            return false;
+        }
+    }
+
+    return true;
+}
+
+} // Anonymous namespace
+
+class Boxcat::Client {
+public:
+    Client(std::string path, u64 title_id, u64 build_id)
+        : path(std::move(path)), title_id(title_id), build_id(build_id) {}
+
+    DownloadResult DownloadDataZip() {
+        return DownloadInternal(fmt::format(BOXCAT_PATHNAME_DATA, title_id), TIMEOUT_SECONDS,
+                                "application/zip");
+    }
+
+    DownloadResult DownloadLaunchParam() {
+        return DownloadInternal(fmt::format(BOXCAT_PATHNAME_LAUNCHPARAM, title_id),
+                                TIMEOUT_SECONDS / 3, "application/octet-stream");
+    }
+
+private:
+    DownloadResult DownloadInternal(const std::string& resolved_path, u32 timeout_seconds,
+                                    const std::string& content_type_name) {
+        if (client == nullptr) {
+            client = std::make_unique<httplib::SSLClient>(BOXCAT_HOSTNAME, PORT, timeout_seconds);
+        }
+
+        httplib::Headers headers{
+            {std::string("Game-Assets-API-Version"), std::string(BOXCAT_API_VERSION)},
+            {std::string("Boxcat-Client-Type"), std::string(BOXCAT_CLIENT_TYPE)},
+            {std::string("Game-Build-Id"), fmt::format("{:016X}", build_id)},
+        };
+
+        if (FileUtil::Exists(path)) {
+            FileUtil::IOFile file{path, "rb"};
+            if (file.IsOpen()) {
+                std::vector<u8> bytes(file.GetSize());
+                file.ReadBytes(bytes.data(), bytes.size());
+                const auto digest = DigestFile(bytes);
+                headers.insert({std::string("If-None-Match"), Common::HexToString(digest, false)});
+            }
+        }
+
+        const auto response = client->Get(resolved_path.c_str(), headers);
+        if (response == nullptr)
+            return DownloadResult::NoResponse;
+
+        if (response->status == static_cast<int>(ResponseStatus::NoUpdate))
+            return DownloadResult::Success;
+        if (response->status == static_cast<int>(ResponseStatus::BadClientVersion))
+            return DownloadResult::BadClientVersion;
+        if (response->status == static_cast<int>(ResponseStatus::NoMatchTitleId))
+            return DownloadResult::NoMatchTitleId;
+        if (response->status == static_cast<int>(ResponseStatus::NoMatchBuildId))
+            return DownloadResult::NoMatchBuildId;
+        if (response->status != static_cast<int>(ResponseStatus::Ok))
+            return DownloadResult::GeneralWebError;
+
+        const auto content_type = response->headers.find("content-type");
+        if (content_type == response->headers.end() ||
+            content_type->second.find(content_type_name) == std::string::npos) {
+            return DownloadResult::InvalidContentType;
+        }
+
+        FileUtil::CreateFullPath(path);
+        FileUtil::IOFile file{path, "wb"};
+        if (!file.IsOpen())
+            return DownloadResult::GeneralFSError;
+        if (!file.Resize(response->body.size()))
+            return DownloadResult::GeneralFSError;
+        if (file.WriteBytes(response->body.data(), response->body.size()) != response->body.size())
+            return DownloadResult::GeneralFSError;
+
+        return DownloadResult::Success;
+    }
+
+    using Digest = std::array<u8, 0x20>;
+    static Digest DigestFile(std::vector<u8> bytes) {
+        Digest out{};
+        mbedtls_sha256(bytes.data(), bytes.size(), out.data(), 0);
+        return out;
+    }
+
+    std::unique_ptr<httplib::Client> client;
+    std::string path;
+    u64 title_id;
+    u64 build_id;
+};
+
+Boxcat::Boxcat(DirectoryGetter getter) : Backend(std::move(getter)) {}
+
+Boxcat::~Boxcat() = default;
+
+void SynchronizeInternal(DirectoryGetter dir_getter, TitleIDVersion title,
+                         ProgressServiceBackend& progress,
+                         std::optional<std::string> dir_name = {}) {
+    progress.SetNeedHLELock(true);
+
+    if (Settings::values.bcat_boxcat_local) {
+        LOG_INFO(Service_BCAT, "Boxcat using local data by override, skipping download.");
+        const auto dir = dir_getter(title.title_id);
+        if (dir)
+            progress.SetTotalSize(dir->GetSize());
+        progress.FinishDownload(RESULT_SUCCESS);
+        return;
+    }
+
+    const auto zip_path{GetZIPFilePath(title.title_id)};
+    Boxcat::Client client{zip_path, title.title_id, title.build_id};
+
+    progress.StartConnecting();
+
+    const auto res = client.DownloadDataZip();
+    if (res != DownloadResult::Success) {
+        LOG_ERROR(Service_BCAT, "Boxcat synchronization failed with error '{}'!", res);
+
+        if (res == DownloadResult::NoMatchBuildId || res == DownloadResult::NoMatchTitleId) {
+            FileUtil::Delete(zip_path);
+        }
+
+        HandleDownloadDisplayResult(res);
+        progress.FinishDownload(ERROR_GENERAL_BCAT_FAILURE);
+        return;
+    }
+
+    progress.StartProcessingDataList();
+
+    FileUtil::IOFile zip{zip_path, "rb"};
+    const auto size = zip.GetSize();
+    std::vector<u8> bytes(size);
+    if (!zip.IsOpen() || size == 0 || zip.ReadBytes(bytes.data(), bytes.size()) != bytes.size()) {
+        LOG_ERROR(Service_BCAT, "Boxcat failed to read ZIP file at path '{}'!", zip_path);
+        progress.FinishDownload(ERROR_GENERAL_BCAT_FAILURE);
+        return;
+    }
+
+    const auto extracted = FileSys::ExtractZIP(std::make_shared<FileSys::VectorVfsFile>(bytes));
+    if (extracted == nullptr) {
+        LOG_ERROR(Service_BCAT, "Boxcat failed to extract ZIP file!");
+        progress.FinishDownload(ERROR_GENERAL_BCAT_FAILURE);
+        return;
+    }
+
+    if (dir_name == std::nullopt) {
+        progress.SetTotalSize(extracted->GetSize());
+
+        const auto target_dir = dir_getter(title.title_id);
+        if (target_dir == nullptr || !VfsRawCopyDProgress(extracted, target_dir, progress)) {
+            LOG_ERROR(Service_BCAT, "Boxcat failed to copy extracted ZIP to target directory!");
+            progress.FinishDownload(ERROR_GENERAL_BCAT_FAILURE);
+            return;
+        }
+    } else {
+        const auto target_dir = dir_getter(title.title_id);
+        if (target_dir == nullptr) {
+            LOG_ERROR(Service_BCAT, "Boxcat failed to get directory for title ID!");
+            progress.FinishDownload(ERROR_GENERAL_BCAT_FAILURE);
+            return;
+        }
+
+        const auto target_sub = target_dir->GetSubdirectory(*dir_name);
+        const auto source_sub = extracted->GetSubdirectory(*dir_name);
+
+        progress.SetTotalSize(source_sub->GetSize());
+
+        std::vector<std::string> filenames;
+        {
+            const auto files = target_sub->GetFiles();
+            std::transform(files.begin(), files.end(), std::back_inserter(filenames),
+                           [](const auto& vfile) { return vfile->GetName(); });
+        }
+
+        for (const auto& filename : filenames) {
+            VfsDeleteFileWrap(target_sub, filename);
+        }
+
+        if (target_sub == nullptr || source_sub == nullptr ||
+            !VfsRawCopyDProgressSingle(source_sub, target_sub, progress)) {
+            LOG_ERROR(Service_BCAT, "Boxcat failed to copy extracted ZIP to target directory!");
+            progress.FinishDownload(ERROR_GENERAL_BCAT_FAILURE);
+            return;
+        }
+    }
+
+    progress.FinishDownload(RESULT_SUCCESS);
+}
+
+bool Boxcat::Synchronize(TitleIDVersion title, ProgressServiceBackend& progress) {
+    is_syncing.exchange(true);
+    std::thread([this, title, &progress] { SynchronizeInternal(dir_getter, title, progress); })
+        .detach();
+    return true;
+}
+
+bool Boxcat::SynchronizeDirectory(TitleIDVersion title, std::string name,
+                                  ProgressServiceBackend& progress) {
+    is_syncing.exchange(true);
+    std::thread(
+        [this, title, name, &progress] { SynchronizeInternal(dir_getter, title, progress, name); })
+        .detach();
+    return true;
+}
+
+bool Boxcat::Clear(u64 title_id) {
+    if (Settings::values.bcat_boxcat_local) {
+        LOG_INFO(Service_BCAT, "Boxcat using local data by override, skipping clear.");
+        return true;
+    }
+
+    const auto dir = dir_getter(title_id);
+
+    std::vector<std::string> dirnames;
+
+    for (const auto& subdir : dir->GetSubdirectories())
+        dirnames.push_back(subdir->GetName());
+
+    for (const auto& subdir : dirnames) {
+        if (!dir->DeleteSubdirectoryRecursive(subdir))
+            return false;
+    }
+
+    return true;
+}
+
+void Boxcat::SetPassphrase(u64 title_id, const Passphrase& passphrase) {
+    LOG_DEBUG(Service_BCAT, "called, title_id={:016X}, passphrase={}", title_id,
+              Common::HexToString(passphrase));
+}
+
+std::optional<std::vector<u8>> Boxcat::GetLaunchParameter(TitleIDVersion title) {
+    const auto path{GetBINFilePath(title.title_id)};
+
+    if (Settings::values.bcat_boxcat_local) {
+        LOG_INFO(Service_BCAT, "Boxcat using local data by override, skipping download.");
+    } else {
+        Boxcat::Client client{path, title.title_id, title.build_id};
+
+        const auto res = client.DownloadLaunchParam();
+        if (res != DownloadResult::Success) {
+            LOG_ERROR(Service_BCAT, "Boxcat synchronization failed with error '{}'!", res);
+
+            if (res == DownloadResult::NoMatchBuildId || res == DownloadResult::NoMatchTitleId) {
+                FileUtil::Delete(path);
+            }
+
+            HandleDownloadDisplayResult(res);
+            return std::nullopt;
+        }
+    }
+
+    FileUtil::IOFile bin{path, "rb"};
+    const auto size = bin.GetSize();
+    std::vector<u8> bytes(size);
+    if (!bin.IsOpen() || size == 0 || bin.ReadBytes(bytes.data(), bytes.size()) != bytes.size()) {
+        LOG_ERROR(Service_BCAT, "Boxcat failed to read launch parameter binary at path '{}'!",
+                  path);
+        return std::nullopt;
+    }
+
+    return bytes;
+}
+
+Boxcat::StatusResult Boxcat::GetStatus(std::optional<std::string>& global,
+                                       std::map<std::string, EventStatus>& games) {
+    httplib::SSLClient client{BOXCAT_HOSTNAME, static_cast<int>(PORT),
+                              static_cast<int>(TIMEOUT_SECONDS)};
+
+    httplib::Headers headers{
+        {std::string("Game-Assets-API-Version"), std::string(BOXCAT_API_VERSION)},
+        {std::string("Boxcat-Client-Type"), std::string(BOXCAT_CLIENT_TYPE)},
+    };
+
+    const auto response = client.Get(BOXCAT_PATHNAME_EVENTS, headers);
+    if (response == nullptr)
+        return StatusResult::Offline;
+
+    if (response->status == static_cast<int>(ResponseStatus::BadClientVersion))
+        return StatusResult::BadClientVersion;
+
+    try {
+        nlohmann::json json = nlohmann::json::parse(response->body);
+
+        if (!json["online"].get<bool>())
+            return StatusResult::Offline;
+
+        if (json["global"].is_null())
+            global = std::nullopt;
+        else
+            global = json["global"].get<std::string>();
+
+        if (json["games"].is_array()) {
+            for (const auto object : json["games"]) {
+                if (object.is_object() && object.find("name") != object.end()) {
+                    EventStatus detail{};
+                    if (object["header"].is_string()) {
+                        detail.header = object["header"].get<std::string>();
+                    } else {
+                        detail.header = std::nullopt;
+                    }
+
+                    if (object["footer"].is_string()) {
+                        detail.footer = object["footer"].get<std::string>();
+                    } else {
+                        detail.footer = std::nullopt;
+                    }
+
+                    if (object["events"].is_array()) {
+                        for (const auto& event : object["events"]) {
+                            if (!event.is_string())
+                                continue;
+                            detail.events.push_back(event.get<std::string>());
+                        }
+                    }
+
+                    games.insert_or_assign(object["name"], std::move(detail));
+                }
+            }
+        }
+
+        return StatusResult::Success;
+    } catch (const nlohmann::json::parse_error& e) {
+        return StatusResult::ParseError;
+    }
+}
+
+} // namespace Service::BCAT
diff --git a/src/core/hle/service/bcat/backend/boxcat.h b/src/core/hle/service/bcat/backend/boxcat.h
new file mode 100644
index 000000000..601151189
--- /dev/null
+++ b/src/core/hle/service/bcat/backend/boxcat.h
@@ -0,0 +1,58 @@
+// Copyright 2019 yuzu emulator team
+// Licensed under GPLv2 or any later version
+// Refer to the license.txt file included.
+
+#pragma once
+
+#include <atomic>
+#include <map>
+#include <optional>
+#include "core/hle/service/bcat/backend/backend.h"
+
+namespace Service::BCAT {
+
+struct EventStatus {
+    std::optional<std::string> header;
+    std::optional<std::string> footer;
+    std::vector<std::string> events;
+};
+
+/// Boxcat is yuzu's custom backend implementation of Nintendo's BCAT service. It is free to use and
+/// doesn't require a switch or nintendo account. The content is controlled by the yuzu team.
+class Boxcat final : public Backend {
+    friend void SynchronizeInternal(DirectoryGetter dir_getter, TitleIDVersion title,
+                                    ProgressServiceBackend& progress,
+                                    std::optional<std::string> dir_name);
+
+public:
+    explicit Boxcat(DirectoryGetter getter);
+    ~Boxcat() override;
+
+    bool Synchronize(TitleIDVersion title, ProgressServiceBackend& progress) override;
+    bool SynchronizeDirectory(TitleIDVersion title, std::string name,
+                              ProgressServiceBackend& progress) override;
+
+    bool Clear(u64 title_id) override;
+
+    void SetPassphrase(u64 title_id, const Passphrase& passphrase) override;
+
+    std::optional<std::vector<u8>> GetLaunchParameter(TitleIDVersion title) override;
+
+    enum class StatusResult {
+        Success,
+        Offline,
+        ParseError,
+        BadClientVersion,
+    };
+
+    static StatusResult GetStatus(std::optional<std::string>& global,
+                                  std::map<std::string, EventStatus>& games);
+
+private:
+    std::atomic_bool is_syncing{false};
+
+    class Client;
+    std::unique_ptr<Client> client;
+};
+
+} // namespace Service::BCAT
diff --git a/src/core/hle/service/bcat/bcat.cpp b/src/core/hle/service/bcat/bcat.cpp
index 179aa4949..c2f946424 100644
--- a/src/core/hle/service/bcat/bcat.cpp
+++ b/src/core/hle/service/bcat/bcat.cpp
@@ -6,11 +6,15 @@
 
 namespace Service::BCAT {
 
-BCAT::BCAT(std::shared_ptr<Module> module, const char* name)
-    : Module::Interface(std::move(module), name) {
+BCAT::BCAT(std::shared_ptr<Module> module, FileSystem::FileSystemController& fsc, const char* name)
+    : Module::Interface(std::move(module), fsc, name) {
+    // clang-format off
     static const FunctionInfo functions[] = {
         {0, &BCAT::CreateBcatService, "CreateBcatService"},
+        {1, &BCAT::CreateDeliveryCacheStorageService, "CreateDeliveryCacheStorageService"},
+        {2, &BCAT::CreateDeliveryCacheStorageServiceWithApplicationId, "CreateDeliveryCacheStorageServiceWithApplicationId"},
     };
+    // clang-format on
     RegisterHandlers(functions);
 }
 
diff --git a/src/core/hle/service/bcat/bcat.h b/src/core/hle/service/bcat/bcat.h
index 802bd689a..813073658 100644
--- a/src/core/hle/service/bcat/bcat.h
+++ b/src/core/hle/service/bcat/bcat.h
@@ -10,7 +10,8 @@ namespace Service::BCAT {
 
 class BCAT final : public Module::Interface {
 public:
-    explicit BCAT(std::shared_ptr<Module> module, const char* name);
+    explicit BCAT(std::shared_ptr<Module> module, FileSystem::FileSystemController& fsc,
+                  const char* name);
     ~BCAT() override;
 };
 
diff --git a/src/core/hle/service/bcat/module.cpp b/src/core/hle/service/bcat/module.cpp
index b7bd738fc..b3fed56c7 100644
--- a/src/core/hle/service/bcat/module.cpp
+++ b/src/core/hle/service/bcat/module.cpp
@@ -2,34 +2,254 @@
 // Licensed under GPLv2 or any later version
 // Refer to the license.txt file included.
 
+#include <cctype>
+#include <mbedtls/md5.h>
+#include "backend/boxcat.h"
+#include "common/hex_util.h"
 #include "common/logging/log.h"
+#include "common/string_util.h"
+#include "core/file_sys/vfs.h"
 #include "core/hle/ipc_helpers.h"
+#include "core/hle/kernel/process.h"
+#include "core/hle/kernel/readable_event.h"
+#include "core/hle/kernel/writable_event.h"
+#include "core/hle/service/bcat/backend/backend.h"
 #include "core/hle/service/bcat/bcat.h"
 #include "core/hle/service/bcat/module.h"
+#include "core/hle/service/filesystem/filesystem.h"
+#include "core/settings.h"
 
 namespace Service::BCAT {
 
+constexpr ResultCode ERROR_INVALID_ARGUMENT{ErrorModule::BCAT, 1};
+constexpr ResultCode ERROR_FAILED_OPEN_ENTITY{ErrorModule::BCAT, 2};
+constexpr ResultCode ERROR_ENTITY_ALREADY_OPEN{ErrorModule::BCAT, 6};
+constexpr ResultCode ERROR_NO_OPEN_ENTITY{ErrorModule::BCAT, 7};
+
+// The command to clear the delivery cache just calls fs IFileSystem DeleteFile on all of the files
+// and if any of them have a non-zero result it just forwards that result. This is the FS error code
+// for permission denied, which is the closest approximation of this scenario.
+constexpr ResultCode ERROR_FAILED_CLEAR_CACHE{ErrorModule::FS, 6400};
+
+using BCATDigest = std::array<u8, 0x10>;
+
+namespace {
+
+u64 GetCurrentBuildID() {
+    const auto& id = Core::System::GetInstance().GetCurrentProcessBuildID();
+    u64 out{};
+    std::memcpy(&out, id.data(), sizeof(u64));
+    return out;
+}
+
+// The digest is only used to determine if a file is unique compared to others of the same name.
+// Since the algorithm isn't ever checked in game, MD5 is safe.
+BCATDigest DigestFile(const FileSys::VirtualFile& file) {
+    BCATDigest out{};
+    const auto bytes = file->ReadAllBytes();
+    mbedtls_md5(bytes.data(), bytes.size(), out.data());
+    return out;
+}
+
+// For a name to be valid it must be non-empty, must have a null terminating character as the final
+// char, can only contain numbers, letters, underscores and a hyphen if directory and a period if
+// file.
+bool VerifyNameValidInternal(Kernel::HLERequestContext& ctx, std::array<char, 0x20> name,
+                             char match_char) {
+    const auto null_chars = std::count(name.begin(), name.end(), 0);
+    const auto bad_chars = std::count_if(name.begin(), name.end(), [match_char](char c) {
+        return !std::isalnum(static_cast<u8>(c)) && c != '_' && c != match_char && c != '\0';
+    });
+    if (null_chars == 0x20 || null_chars == 0 || bad_chars != 0 || name[0x1F] != '\0') {
+        LOG_ERROR(Service_BCAT, "Name passed was invalid!");
+        IPC::ResponseBuilder rb{ctx, 2};
+        rb.Push(ERROR_INVALID_ARGUMENT);
+        return false;
+    }
+
+    return true;
+}
+
+bool VerifyNameValidDir(Kernel::HLERequestContext& ctx, DirectoryName name) {
+    return VerifyNameValidInternal(ctx, name, '-');
+}
+
+bool VerifyNameValidFile(Kernel::HLERequestContext& ctx, FileName name) {
+    return VerifyNameValidInternal(ctx, name, '.');
+}
+
+} // Anonymous namespace
+
+struct DeliveryCacheDirectoryEntry {
+    FileName name;
+    u64 size;
+    BCATDigest digest;
+};
+
+class IDeliveryCacheProgressService final : public ServiceFramework<IDeliveryCacheProgressService> {
+public:
+    IDeliveryCacheProgressService(Kernel::SharedPtr<Kernel::ReadableEvent> event,
+                                  const DeliveryCacheProgressImpl& impl)
+        : ServiceFramework{"IDeliveryCacheProgressService"}, event(std::move(event)), impl(impl) {
+        // clang-format off
+        static const FunctionInfo functions[] = {
+            {0, &IDeliveryCacheProgressService::GetEvent, "GetEvent"},
+            {1, &IDeliveryCacheProgressService::GetImpl, "GetImpl"},
+        };
+        // clang-format on
+
+        RegisterHandlers(functions);
+    }
+
+private:
+    void GetEvent(Kernel::HLERequestContext& ctx) {
+        LOG_DEBUG(Service_BCAT, "called");
+
+        IPC::ResponseBuilder rb{ctx, 2, 1};
+        rb.Push(RESULT_SUCCESS);
+        rb.PushCopyObjects(event);
+    }
+
+    void GetImpl(Kernel::HLERequestContext& ctx) {
+        LOG_DEBUG(Service_BCAT, "called");
+
+        ctx.WriteBuffer(&impl, sizeof(DeliveryCacheProgressImpl));
+
+        IPC::ResponseBuilder rb{ctx, 2};
+        rb.Push(RESULT_SUCCESS);
+    }
+
+    Kernel::SharedPtr<Kernel::ReadableEvent> event;
+    const DeliveryCacheProgressImpl& impl;
+};
+
 class IBcatService final : public ServiceFramework<IBcatService> {
 public:
-    IBcatService() : ServiceFramework("IBcatService") {
+    IBcatService(Backend& backend) : ServiceFramework("IBcatService"), backend(backend) {
+        // clang-format off
         static const FunctionInfo functions[] = {
-            {10100, nullptr, "RequestSyncDeliveryCache"},
-            {10101, nullptr, "RequestSyncDeliveryCacheWithDirectoryName"},
+            {10100, &IBcatService::RequestSyncDeliveryCache, "RequestSyncDeliveryCache"},
+            {10101, &IBcatService::RequestSyncDeliveryCacheWithDirectoryName, "RequestSyncDeliveryCacheWithDirectoryName"},
             {10200, nullptr, "CancelSyncDeliveryCacheRequest"},
             {20100, nullptr, "RequestSyncDeliveryCacheWithApplicationId"},
             {20101, nullptr, "RequestSyncDeliveryCacheWithApplicationIdAndDirectoryName"},
-            {30100, nullptr, "SetPassphrase"},
+            {30100, &IBcatService::SetPassphrase, "SetPassphrase"},
             {30200, nullptr, "RegisterBackgroundDeliveryTask"},
             {30201, nullptr, "UnregisterBackgroundDeliveryTask"},
             {30202, nullptr, "BlockDeliveryTask"},
             {30203, nullptr, "UnblockDeliveryTask"},
             {90100, nullptr, "EnumerateBackgroundDeliveryTask"},
             {90200, nullptr, "GetDeliveryList"},
-            {90201, nullptr, "ClearDeliveryCacheStorage"},
+            {90201, &IBcatService::ClearDeliveryCacheStorage, "ClearDeliveryCacheStorage"},
             {90300, nullptr, "GetPushNotificationLog"},
         };
+        // clang-format on
         RegisterHandlers(functions);
     }
+
+private:
+    enum class SyncType {
+        Normal,
+        Directory,
+        Count,
+    };
+
+    std::shared_ptr<IDeliveryCacheProgressService> CreateProgressService(SyncType type) {
+        auto& backend{progress.at(static_cast<std::size_t>(type))};
+        return std::make_shared<IDeliveryCacheProgressService>(backend.GetEvent(),
+                                                               backend.GetImpl());
+    }
+
+    void RequestSyncDeliveryCache(Kernel::HLERequestContext& ctx) {
+        LOG_DEBUG(Service_BCAT, "called");
+
+        backend.Synchronize({Core::CurrentProcess()->GetTitleID(), GetCurrentBuildID()},
+                            progress.at(static_cast<std::size_t>(SyncType::Normal)));
+
+        IPC::ResponseBuilder rb{ctx, 2, 0, 1};
+        rb.Push(RESULT_SUCCESS);
+        rb.PushIpcInterface(CreateProgressService(SyncType::Normal));
+    }
+
+    void RequestSyncDeliveryCacheWithDirectoryName(Kernel::HLERequestContext& ctx) {
+        IPC::RequestParser rp{ctx};
+        const auto name_raw = rp.PopRaw<DirectoryName>();
+        const auto name =
+            Common::StringFromFixedZeroTerminatedBuffer(name_raw.data(), name_raw.size());
+
+        LOG_DEBUG(Service_BCAT, "called, name={}", name);
+
+        backend.SynchronizeDirectory({Core::CurrentProcess()->GetTitleID(), GetCurrentBuildID()},
+                                     name,
+                                     progress.at(static_cast<std::size_t>(SyncType::Directory)));
+
+        IPC::ResponseBuilder rb{ctx, 2, 0, 1};
+        rb.Push(RESULT_SUCCESS);
+        rb.PushIpcInterface(CreateProgressService(SyncType::Directory));
+    }
+
+    void SetPassphrase(Kernel::HLERequestContext& ctx) {
+        IPC::RequestParser rp{ctx};
+        const auto title_id = rp.PopRaw<u64>();
+
+        const auto passphrase_raw = ctx.ReadBuffer();
+
+        LOG_DEBUG(Service_BCAT, "called, title_id={:016X}, passphrase={}", title_id,
+                  Common::HexToString(passphrase_raw));
+
+        if (title_id == 0) {
+            LOG_ERROR(Service_BCAT, "Invalid title ID!");
+            IPC::ResponseBuilder rb{ctx, 2};
+            rb.Push(ERROR_INVALID_ARGUMENT);
+        }
+
+        if (passphrase_raw.size() > 0x40) {
+            LOG_ERROR(Service_BCAT, "Passphrase too large!");
+            IPC::ResponseBuilder rb{ctx, 2};
+            rb.Push(ERROR_INVALID_ARGUMENT);
+            return;
+        }
+
+        Passphrase passphrase{};
+        std::memcpy(passphrase.data(), passphrase_raw.data(),
+                    std::min(passphrase.size(), passphrase_raw.size()));
+
+        backend.SetPassphrase(title_id, passphrase);
+
+        IPC::ResponseBuilder rb{ctx, 2};
+        rb.Push(RESULT_SUCCESS);
+    }
+
+    void ClearDeliveryCacheStorage(Kernel::HLERequestContext& ctx) {
+        IPC::RequestParser rp{ctx};
+        const auto title_id = rp.PopRaw<u64>();
+
+        LOG_DEBUG(Service_BCAT, "called, title_id={:016X}", title_id);
+
+        if (title_id == 0) {
+            LOG_ERROR(Service_BCAT, "Invalid title ID!");
+            IPC::ResponseBuilder rb{ctx, 2};
+            rb.Push(ERROR_INVALID_ARGUMENT);
+            return;
+        }
+
+        if (!backend.Clear(title_id)) {
+            LOG_ERROR(Service_BCAT, "Could not clear the directory successfully!");
+            IPC::ResponseBuilder rb{ctx, 2};
+            rb.Push(ERROR_FAILED_CLEAR_CACHE);
+            return;
+        }
+
+        IPC::ResponseBuilder rb{ctx, 2};
+        rb.Push(RESULT_SUCCESS);
+    }
+
+    Backend& backend;
+
+    std::array<ProgressServiceBackend, static_cast<std::size_t>(SyncType::Count)> progress{
+        ProgressServiceBackend{"Normal"},
+        ProgressServiceBackend{"Directory"},
+    };
 };
 
 void Module::Interface::CreateBcatService(Kernel::HLERequestContext& ctx) {
@@ -37,20 +257,331 @@ void Module::Interface::CreateBcatService(Kernel::HLERequestContext& ctx) {
 
     IPC::ResponseBuilder rb{ctx, 2, 0, 1};
     rb.Push(RESULT_SUCCESS);
-    rb.PushIpcInterface<IBcatService>();
+    rb.PushIpcInterface<IBcatService>(*backend);
 }
 
-Module::Interface::Interface(std::shared_ptr<Module> module, const char* name)
-    : ServiceFramework(name), module(std::move(module)) {}
+class IDeliveryCacheFileService final : public ServiceFramework<IDeliveryCacheFileService> {
+public:
+    IDeliveryCacheFileService(FileSys::VirtualDir root_)
+        : ServiceFramework{"IDeliveryCacheFileService"}, root(std::move(root_)) {
+        // clang-format off
+        static const FunctionInfo functions[] = {
+            {0, &IDeliveryCacheFileService::Open, "Open"},
+            {1, &IDeliveryCacheFileService::Read, "Read"},
+            {2, &IDeliveryCacheFileService::GetSize, "GetSize"},
+            {3, &IDeliveryCacheFileService::GetDigest, "GetDigest"},
+        };
+        // clang-format on
+
+        RegisterHandlers(functions);
+    }
+
+private:
+    void Open(Kernel::HLERequestContext& ctx) {
+        IPC::RequestParser rp{ctx};
+        const auto dir_name_raw = rp.PopRaw<DirectoryName>();
+        const auto file_name_raw = rp.PopRaw<FileName>();
+
+        const auto dir_name =
+            Common::StringFromFixedZeroTerminatedBuffer(dir_name_raw.data(), dir_name_raw.size());
+        const auto file_name =
+            Common::StringFromFixedZeroTerminatedBuffer(file_name_raw.data(), file_name_raw.size());
+
+        LOG_DEBUG(Service_BCAT, "called, dir_name={}, file_name={}", dir_name, file_name);
+
+        if (!VerifyNameValidDir(ctx, dir_name_raw) || !VerifyNameValidFile(ctx, file_name_raw))
+            return;
+
+        if (current_file != nullptr) {
+            LOG_ERROR(Service_BCAT, "A file has already been opened on this interface!");
+            IPC::ResponseBuilder rb{ctx, 2};
+            rb.Push(ERROR_ENTITY_ALREADY_OPEN);
+            return;
+        }
+
+        const auto dir = root->GetSubdirectory(dir_name);
+
+        if (dir == nullptr) {
+            LOG_ERROR(Service_BCAT, "The directory of name={} couldn't be opened!", dir_name);
+            IPC::ResponseBuilder rb{ctx, 2};
+            rb.Push(ERROR_FAILED_OPEN_ENTITY);
+            return;
+        }
+
+        current_file = dir->GetFile(file_name);
+
+        if (current_file == nullptr) {
+            LOG_ERROR(Service_BCAT, "The file of name={} couldn't be opened!", file_name);
+            IPC::ResponseBuilder rb{ctx, 2};
+            rb.Push(ERROR_FAILED_OPEN_ENTITY);
+            return;
+        }
+
+        IPC::ResponseBuilder rb{ctx, 2};
+        rb.Push(RESULT_SUCCESS);
+    }
+
+    void Read(Kernel::HLERequestContext& ctx) {
+        IPC::RequestParser rp{ctx};
+        const auto offset{rp.PopRaw<u64>()};
+
+        auto size = ctx.GetWriteBufferSize();
+
+        LOG_DEBUG(Service_BCAT, "called, offset={:016X}, size={:016X}", offset, size);
+
+        if (current_file == nullptr) {
+            LOG_ERROR(Service_BCAT, "There is no file currently open!");
+            IPC::ResponseBuilder rb{ctx, 2};
+            rb.Push(ERROR_NO_OPEN_ENTITY);
+        }
+
+        size = std::min<u64>(current_file->GetSize() - offset, size);
+        const auto buffer = current_file->ReadBytes(size, offset);
+        ctx.WriteBuffer(buffer);
+
+        IPC::ResponseBuilder rb{ctx, 4};
+        rb.Push(RESULT_SUCCESS);
+        rb.Push<u64>(buffer.size());
+    }
+
+    void GetSize(Kernel::HLERequestContext& ctx) {
+        LOG_DEBUG(Service_BCAT, "called");
+
+        if (current_file == nullptr) {
+            LOG_ERROR(Service_BCAT, "There is no file currently open!");
+            IPC::ResponseBuilder rb{ctx, 2};
+            rb.Push(ERROR_NO_OPEN_ENTITY);
+        }
+
+        IPC::ResponseBuilder rb{ctx, 4};
+        rb.Push(RESULT_SUCCESS);
+        rb.Push<u64>(current_file->GetSize());
+    }
+
+    void GetDigest(Kernel::HLERequestContext& ctx) {
+        LOG_DEBUG(Service_BCAT, "called");
+
+        if (current_file == nullptr) {
+            LOG_ERROR(Service_BCAT, "There is no file currently open!");
+            IPC::ResponseBuilder rb{ctx, 2};
+            rb.Push(ERROR_NO_OPEN_ENTITY);
+        }
+
+        IPC::ResponseBuilder rb{ctx, 6};
+        rb.Push(RESULT_SUCCESS);
+        rb.PushRaw(DigestFile(current_file));
+    }
+
+    FileSys::VirtualDir root;
+    FileSys::VirtualFile current_file;
+};
+
+class IDeliveryCacheDirectoryService final
+    : public ServiceFramework<IDeliveryCacheDirectoryService> {
+public:
+    IDeliveryCacheDirectoryService(FileSys::VirtualDir root_)
+        : ServiceFramework{"IDeliveryCacheDirectoryService"}, root(std::move(root_)) {
+        // clang-format off
+        static const FunctionInfo functions[] = {
+            {0, &IDeliveryCacheDirectoryService::Open, "Open"},
+            {1, &IDeliveryCacheDirectoryService::Read, "Read"},
+            {2, &IDeliveryCacheDirectoryService::GetCount, "GetCount"},
+        };
+        // clang-format on
+
+        RegisterHandlers(functions);
+    }
+
+private:
+    void Open(Kernel::HLERequestContext& ctx) {
+        IPC::RequestParser rp{ctx};
+        const auto name_raw = rp.PopRaw<DirectoryName>();
+        const auto name =
+            Common::StringFromFixedZeroTerminatedBuffer(name_raw.data(), name_raw.size());
+
+        LOG_DEBUG(Service_BCAT, "called, name={}", name);
+
+        if (!VerifyNameValidDir(ctx, name_raw))
+            return;
+
+        if (current_dir != nullptr) {
+            LOG_ERROR(Service_BCAT, "A file has already been opened on this interface!");
+            IPC::ResponseBuilder rb{ctx, 2};
+            rb.Push(ERROR_ENTITY_ALREADY_OPEN);
+            return;
+        }
+
+        current_dir = root->GetSubdirectory(name);
+
+        if (current_dir == nullptr) {
+            LOG_ERROR(Service_BCAT, "Failed to open the directory name={}!", name);
+            IPC::ResponseBuilder rb{ctx, 2};
+            rb.Push(ERROR_FAILED_OPEN_ENTITY);
+            return;
+        }
+
+        IPC::ResponseBuilder rb{ctx, 2};
+        rb.Push(RESULT_SUCCESS);
+    }
+
+    void Read(Kernel::HLERequestContext& ctx) {
+        auto write_size = ctx.GetWriteBufferSize() / sizeof(DeliveryCacheDirectoryEntry);
+
+        LOG_DEBUG(Service_BCAT, "called, write_size={:016X}", write_size);
+
+        if (current_dir == nullptr) {
+            LOG_ERROR(Service_BCAT, "There is no open directory!");
+            IPC::ResponseBuilder rb{ctx, 2};
+            rb.Push(ERROR_NO_OPEN_ENTITY);
+            return;
+        }
+
+        const auto files = current_dir->GetFiles();
+        write_size = std::min<u64>(write_size, files.size());
+        std::vector<DeliveryCacheDirectoryEntry> entries(write_size);
+        std::transform(
+            files.begin(), files.begin() + write_size, entries.begin(), [](const auto& file) {
+                FileName name{};
+                std::memcpy(name.data(), file->GetName().data(),
+                            std::min(file->GetName().size(), name.size()));
+                return DeliveryCacheDirectoryEntry{name, file->GetSize(), DigestFile(file)};
+            });
+
+        ctx.WriteBuffer(entries);
+
+        IPC::ResponseBuilder rb{ctx, 3};
+        rb.Push(RESULT_SUCCESS);
+        rb.Push<u32>(write_size * sizeof(DeliveryCacheDirectoryEntry));
+    }
+
+    void GetCount(Kernel::HLERequestContext& ctx) {
+        LOG_DEBUG(Service_BCAT, "called");
+
+        if (current_dir == nullptr) {
+            LOG_ERROR(Service_BCAT, "There is no open directory!");
+            IPC::ResponseBuilder rb{ctx, 2};
+            rb.Push(ERROR_NO_OPEN_ENTITY);
+            return;
+        }
+
+        const auto files = current_dir->GetFiles();
+
+        IPC::ResponseBuilder rb{ctx, 3};
+        rb.Push(RESULT_SUCCESS);
+        rb.Push<u32>(files.size());
+    }
+
+    FileSys::VirtualDir root;
+    FileSys::VirtualDir current_dir;
+};
+
+class IDeliveryCacheStorageService final : public ServiceFramework<IDeliveryCacheStorageService> {
+public:
+    IDeliveryCacheStorageService(FileSys::VirtualDir root_)
+        : ServiceFramework{"IDeliveryCacheStorageService"}, root(std::move(root_)) {
+        // clang-format off
+        static const FunctionInfo functions[] = {
+            {0, &IDeliveryCacheStorageService::CreateFileService, "CreateFileService"},
+            {1, &IDeliveryCacheStorageService::CreateDirectoryService, "CreateDirectoryService"},
+            {10, &IDeliveryCacheStorageService::EnumerateDeliveryCacheDirectory, "EnumerateDeliveryCacheDirectory"},
+        };
+        // clang-format on
+
+        RegisterHandlers(functions);
+
+        for (const auto& subdir : root->GetSubdirectories()) {
+            DirectoryName name{};
+            std::memcpy(name.data(), subdir->GetName().data(),
+                        std::min(sizeof(DirectoryName) - 1, subdir->GetName().size()));
+            entries.push_back(name);
+        }
+    }
+
+private:
+    void CreateFileService(Kernel::HLERequestContext& ctx) {
+        LOG_DEBUG(Service_BCAT, "called");
+
+        IPC::ResponseBuilder rb{ctx, 2, 0, 1};
+        rb.Push(RESULT_SUCCESS);
+        rb.PushIpcInterface<IDeliveryCacheFileService>(root);
+    }
+
+    void CreateDirectoryService(Kernel::HLERequestContext& ctx) {
+        LOG_DEBUG(Service_BCAT, "called");
+
+        IPC::ResponseBuilder rb{ctx, 2, 0, 1};
+        rb.Push(RESULT_SUCCESS);
+        rb.PushIpcInterface<IDeliveryCacheDirectoryService>(root);
+    }
+
+    void EnumerateDeliveryCacheDirectory(Kernel::HLERequestContext& ctx) {
+        auto size = ctx.GetWriteBufferSize() / sizeof(DirectoryName);
+
+        LOG_DEBUG(Service_BCAT, "called, size={:016X}", size);
+
+        size = std::min<u64>(size, entries.size() - next_read_index);
+        ctx.WriteBuffer(entries.data() + next_read_index, size * sizeof(DirectoryName));
+        next_read_index += size;
+
+        IPC::ResponseBuilder rb{ctx, 3};
+        rb.Push(RESULT_SUCCESS);
+        rb.Push<u32>(size);
+    }
+
+    FileSys::VirtualDir root;
+    std::vector<DirectoryName> entries;
+    u64 next_read_index = 0;
+};
+
+void Module::Interface::CreateDeliveryCacheStorageService(Kernel::HLERequestContext& ctx) {
+    LOG_DEBUG(Service_BCAT, "called");
+
+    IPC::ResponseBuilder rb{ctx, 2, 0, 1};
+    rb.Push(RESULT_SUCCESS);
+    rb.PushIpcInterface<IDeliveryCacheStorageService>(
+        fsc.GetBCATDirectory(Core::CurrentProcess()->GetTitleID()));
+}
+
+void Module::Interface::CreateDeliveryCacheStorageServiceWithApplicationId(
+    Kernel::HLERequestContext& ctx) {
+    IPC::RequestParser rp{ctx};
+    const auto title_id = rp.PopRaw<u64>();
+
+    LOG_DEBUG(Service_BCAT, "called, title_id={:016X}", title_id);
+
+    IPC::ResponseBuilder rb{ctx, 2, 0, 1};
+    rb.Push(RESULT_SUCCESS);
+    rb.PushIpcInterface<IDeliveryCacheStorageService>(fsc.GetBCATDirectory(title_id));
+}
+
+std::unique_ptr<Backend> CreateBackendFromSettings(DirectoryGetter getter) {
+    const auto backend = Settings::values.bcat_backend;
+
+#ifdef YUZU_ENABLE_BOXCAT
+    if (backend == "boxcat")
+        return std::make_unique<Boxcat>(std::move(getter));
+#endif
+
+    return std::make_unique<NullBackend>(std::move(getter));
+}
+
+Module::Interface::Interface(std::shared_ptr<Module> module, FileSystem::FileSystemController& fsc,
+                             const char* name)
+    : ServiceFramework(name), module(std::move(module)), fsc(fsc),
+      backend(CreateBackendFromSettings([&fsc](u64 tid) { return fsc.GetBCATDirectory(tid); })) {}
 
 Module::Interface::~Interface() = default;
 
-void InstallInterfaces(SM::ServiceManager& service_manager) {
+void InstallInterfaces(Core::System& system) {
     auto module = std::make_shared<Module>();
-    std::make_shared<BCAT>(module, "bcat:a")->InstallAsService(service_manager);
-    std::make_shared<BCAT>(module, "bcat:m")->InstallAsService(service_manager);
-    std::make_shared<BCAT>(module, "bcat:u")->InstallAsService(service_manager);
-    std::make_shared<BCAT>(module, "bcat:s")->InstallAsService(service_manager);
+    std::make_shared<BCAT>(module, system.GetFileSystemController(), "bcat:a")
+        ->InstallAsService(system.ServiceManager());
+    std::make_shared<BCAT>(module, system.GetFileSystemController(), "bcat:m")
+        ->InstallAsService(system.ServiceManager());
+    std::make_shared<BCAT>(module, system.GetFileSystemController(), "bcat:u")
+        ->InstallAsService(system.ServiceManager());
+    std::make_shared<BCAT>(module, system.GetFileSystemController(), "bcat:s")
+        ->InstallAsService(system.ServiceManager());
 }
 
 } // namespace Service::BCAT
diff --git a/src/core/hle/service/bcat/module.h b/src/core/hle/service/bcat/module.h
index f0d63cab0..27469926a 100644
--- a/src/core/hle/service/bcat/module.h
+++ b/src/core/hle/service/bcat/module.h
@@ -6,23 +6,39 @@
 
 #include "core/hle/service/service.h"
 
-namespace Service::BCAT {
+namespace Service {
+
+namespace FileSystem {
+class FileSystemController;
+} // namespace FileSystem
+
+namespace BCAT {
+
+class Backend;
 
 class Module final {
 public:
     class Interface : public ServiceFramework<Interface> {
     public:
-        explicit Interface(std::shared_ptr<Module> module, const char* name);
+        explicit Interface(std::shared_ptr<Module> module, FileSystem::FileSystemController& fsc,
+                           const char* name);
         ~Interface() override;
 
         void CreateBcatService(Kernel::HLERequestContext& ctx);
+        void CreateDeliveryCacheStorageService(Kernel::HLERequestContext& ctx);
+        void CreateDeliveryCacheStorageServiceWithApplicationId(Kernel::HLERequestContext& ctx);
 
     protected:
+        FileSystem::FileSystemController& fsc;
+
         std::shared_ptr<Module> module;
+        std::unique_ptr<Backend> backend;
     };
 };
 
 /// Registers all BCAT services with the specified service manager.
-void InstallInterfaces(SM::ServiceManager& service_manager);
+void InstallInterfaces(Core::System& system);
 
-} // namespace Service::BCAT
+} // namespace BCAT
+
+} // namespace Service
diff --git a/src/core/hle/service/filesystem/filesystem.cpp b/src/core/hle/service/filesystem/filesystem.cpp
index 14cd0e322..7fa4e820b 100644
--- a/src/core/hle/service/filesystem/filesystem.cpp
+++ b/src/core/hle/service/filesystem/filesystem.cpp
@@ -674,6 +674,15 @@ FileSys::VirtualDir FileSystemController::GetModificationDumpRoot(u64 title_id)
     return bis_factory->GetModificationDumpRoot(title_id);
 }
 
+FileSys::VirtualDir FileSystemController::GetBCATDirectory(u64 title_id) const {
+    LOG_TRACE(Service_FS, "Opening BCAT root for tid={:016X}", title_id);
+
+    if (bis_factory == nullptr)
+        return nullptr;
+
+    return bis_factory->GetBCATDirectory(title_id);
+}
+
 void FileSystemController::CreateFactories(FileSys::VfsFilesystem& vfs, bool overwrite) {
     if (overwrite) {
         bis_factory = nullptr;
diff --git a/src/core/hle/service/filesystem/filesystem.h b/src/core/hle/service/filesystem/filesystem.h
index 3e0c03ec0..e6b49d8a2 100644
--- a/src/core/hle/service/filesystem/filesystem.h
+++ b/src/core/hle/service/filesystem/filesystem.h
@@ -110,6 +110,8 @@ public:
     FileSys::VirtualDir GetModificationLoadRoot(u64 title_id) const;
     FileSys::VirtualDir GetModificationDumpRoot(u64 title_id) const;
 
+    FileSys::VirtualDir GetBCATDirectory(u64 title_id) const;
+
     // Creates the SaveData, SDMC, and BIS Factories. Should be called once and before any function
     // above is called.
     void CreateFactories(FileSys::VfsFilesystem& vfs, bool overwrite = true);
diff --git a/src/core/hle/service/nifm/nifm.cpp b/src/core/hle/service/nifm/nifm.cpp
index 24d1813a7..756a2af57 100644
--- a/src/core/hle/service/nifm/nifm.cpp
+++ b/src/core/hle/service/nifm/nifm.cpp
@@ -12,6 +12,13 @@
 
 namespace Service::NIFM {
 
+enum class RequestState : u32 {
+    NotSubmitted = 1,
+    Error = 1, ///< The duplicate 1 is intentional; it means both not submitted and error on HW.
+    Pending = 2,
+    Connected = 3,
+};
+
 class IScanRequest final : public ServiceFramework<IScanRequest> {
 public:
     explicit IScanRequest() : ServiceFramework("IScanRequest") {
@@ -81,7 +88,7 @@ private:
 
         IPC::ResponseBuilder rb{ctx, 3};
         rb.Push(RESULT_SUCCESS);
-        rb.Push<u32>(0);
+        rb.PushEnum(RequestState::Connected);
     }
 
     void GetResult(Kernel::HLERequestContext& ctx) {
@@ -189,14 +196,14 @@ private:
 
         IPC::ResponseBuilder rb{ctx, 3};
         rb.Push(RESULT_SUCCESS);
-        rb.Push<u8>(0);
+        rb.Push<u8>(1);
     }
     void IsAnyInternetRequestAccepted(Kernel::HLERequestContext& ctx) {
         LOG_WARNING(Service_NIFM, "(STUBBED) called");
 
         IPC::ResponseBuilder rb{ctx, 3};
         rb.Push(RESULT_SUCCESS);
-        rb.Push<u8>(0);
+        rb.Push<u8>(1);
     }
     Core::System& system;
 };
diff --git a/src/core/hle/service/service.cpp b/src/core/hle/service/service.cpp
index 831a427de..f2c6fe9dc 100644
--- a/src/core/hle/service/service.cpp
+++ b/src/core/hle/service/service.cpp
@@ -208,7 +208,7 @@ void Init(std::shared_ptr<SM::ServiceManager>& sm, Core::System& system) {
     AOC::InstallInterfaces(*sm, system);
     APM::InstallInterfaces(system);
     Audio::InstallInterfaces(*sm, system);
-    BCAT::InstallInterfaces(*sm);
+    BCAT::InstallInterfaces(system);
     BPC::InstallInterfaces(*sm);
     BtDrv::InstallInterfaces(*sm, system);
     BTM::InstallInterfaces(*sm, system);
diff --git a/src/core/loader/nso.cpp b/src/core/loader/nso.cpp
index e75c700ad..f629892ae 100644
--- a/src/core/loader/nso.cpp
+++ b/src/core/loader/nso.cpp
@@ -150,6 +150,7 @@ std::optional<VAddr> AppLoader_NSO::LoadModule(Kernel::Process& process,
     // Apply cheats if they exist and the program has a valid title ID
     if (pm) {
         auto& system = Core::System::GetInstance();
+        system.SetCurrentProcessBuildID(nso_header.build_id);
         const auto cheats = pm->CreateCheatList(system, nso_header.build_id);
         if (!cheats.empty()) {
             system.RegisterCheatList(cheats, nso_header.build_id, load_base, image_size);
diff --git a/src/core/settings.cpp b/src/core/settings.cpp
index 7de3fd1e5..d1fc94060 100644
--- a/src/core/settings.cpp
+++ b/src/core/settings.cpp
@@ -103,6 +103,8 @@ void LogSettings() {
     LogSetting("Debugging_UseGdbstub", Settings::values.use_gdbstub);
     LogSetting("Debugging_GdbstubPort", Settings::values.gdbstub_port);
     LogSetting("Debugging_ProgramArgs", Settings::values.program_args);
+    LogSetting("Services_BCATBackend", Settings::values.bcat_backend);
+    LogSetting("Services_BCATBoxcatLocal", Settings::values.bcat_boxcat_local);
 }
 
 } // namespace Settings
diff --git a/src/core/settings.h b/src/core/settings.h
index 47bddfb30..9c98a9287 100644
--- a/src/core/settings.h
+++ b/src/core/settings.h
@@ -448,6 +448,10 @@ struct Values {
     bool reporting_services;
     bool quest_flag;
 
+    // BCAT
+    std::string bcat_backend;
+    bool bcat_boxcat_local;
+
     // WebService
     bool enable_telemetry;
     std::string web_api_url;
diff --git a/src/yuzu/CMakeLists.txt b/src/yuzu/CMakeLists.txt
index dc6fa07fc..ff1c1d985 100644
--- a/src/yuzu/CMakeLists.txt
+++ b/src/yuzu/CMakeLists.txt
@@ -66,6 +66,9 @@ add_executable(yuzu
     configuration/configure_profile_manager.cpp
     configuration/configure_profile_manager.h
     configuration/configure_profile_manager.ui
+    configuration/configure_service.cpp
+    configuration/configure_service.h
+    configuration/configure_service.ui
     configuration/configure_system.cpp
     configuration/configure_system.h
     configuration/configure_system.ui
@@ -186,6 +189,10 @@ if (YUZU_USE_QT_WEB_ENGINE)
     target_compile_definitions(yuzu PRIVATE -DYUZU_USE_QT_WEB_ENGINE)
 endif ()
 
+if (YUZU_ENABLE_BOXCAT)
+    target_compile_definitions(yuzu PRIVATE -DYUZU_ENABLE_BOXCAT)
+endif ()
+
 if(UNIX AND NOT APPLE)
     install(TARGETS yuzu RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}/bin")
 endif()
diff --git a/src/yuzu/configuration/config.cpp b/src/yuzu/configuration/config.cpp
index 92d9fb161..4cb27ddb2 100644
--- a/src/yuzu/configuration/config.cpp
+++ b/src/yuzu/configuration/config.cpp
@@ -525,6 +525,17 @@ void Config::ReadDebuggingValues() {
     qt_config->endGroup();
 }
 
+void Config::ReadServiceValues() {
+    qt_config->beginGroup(QStringLiteral("Services"));
+    Settings::values.bcat_backend =
+        ReadSetting(QStringLiteral("bcat_backend"), QStringLiteral("boxcat"))
+            .toString()
+            .toStdString();
+    Settings::values.bcat_boxcat_local =
+        ReadSetting(QStringLiteral("bcat_boxcat_local"), false).toBool();
+    qt_config->endGroup();
+}
+
 void Config::ReadDisabledAddOnValues() {
     const auto size = qt_config->beginReadArray(QStringLiteral("DisabledAddOns"));
 
@@ -769,6 +780,7 @@ void Config::ReadValues() {
     ReadMiscellaneousValues();
     ReadDebuggingValues();
     ReadWebServiceValues();
+    ReadServiceValues();
     ReadDisabledAddOnValues();
     ReadUIValues();
 }
@@ -866,6 +878,7 @@ void Config::SaveValues() {
     SaveMiscellaneousValues();
     SaveDebuggingValues();
     SaveWebServiceValues();
+    SaveServiceValues();
     SaveDisabledAddOnValues();
     SaveUIValues();
 }
@@ -963,6 +976,14 @@ void Config::SaveDebuggingValues() {
     qt_config->endGroup();
 }
 
+void Config::SaveServiceValues() {
+    qt_config->beginGroup(QStringLiteral("Services"));
+    WriteSetting(QStringLiteral("bcat_backend"),
+                 QString::fromStdString(Settings::values.bcat_backend), QStringLiteral("null"));
+    WriteSetting(QStringLiteral("bcat_boxcat_local"), Settings::values.bcat_boxcat_local, false);
+    qt_config->endGroup();
+}
+
 void Config::SaveDisabledAddOnValues() {
     qt_config->beginWriteArray(QStringLiteral("DisabledAddOns"));
 
diff --git a/src/yuzu/configuration/config.h b/src/yuzu/configuration/config.h
index 6b523ecdd..ba6888004 100644
--- a/src/yuzu/configuration/config.h
+++ b/src/yuzu/configuration/config.h
@@ -42,6 +42,7 @@ private:
     void ReadCoreValues();
     void ReadDataStorageValues();
     void ReadDebuggingValues();
+    void ReadServiceValues();
     void ReadDisabledAddOnValues();
     void ReadMiscellaneousValues();
     void ReadPathValues();
@@ -65,6 +66,7 @@ private:
     void SaveCoreValues();
     void SaveDataStorageValues();
     void SaveDebuggingValues();
+    void SaveServiceValues();
     void SaveDisabledAddOnValues();
     void SaveMiscellaneousValues();
     void SavePathValues();
diff --git a/src/yuzu/configuration/configure.ui b/src/yuzu/configuration/configure.ui
index 49fadd0ef..372427ae2 100644
--- a/src/yuzu/configuration/configure.ui
+++ b/src/yuzu/configuration/configure.ui
@@ -98,6 +98,11 @@
          <string>Web</string>
         </attribute>
        </widget>
+       <widget class="ConfigureService" name="serviceTab">
+        <attribute name="title">
+         <string>Services</string>
+        </attribute>
+       </widget>
       </widget>
      </item>
     </layout>
@@ -178,6 +183,12 @@
    <header>configuration/configure_hotkeys.h</header>
    <container>1</container>
   </customwidget>
+  <customwidget>
+   <class>ConfigureService</class>
+   <extends>QWidget</extends>
+   <header>configuration/configure_service.h</header>
+   <container>1</container>
+  </customwidget>
  </customwidgets>
  <resources/>
  <connections>
diff --git a/src/yuzu/configuration/configure_dialog.cpp b/src/yuzu/configuration/configure_dialog.cpp
index 7c875ae87..25b2e1b05 100644
--- a/src/yuzu/configuration/configure_dialog.cpp
+++ b/src/yuzu/configuration/configure_dialog.cpp
@@ -44,6 +44,7 @@ void ConfigureDialog::ApplyConfiguration() {
     ui->audioTab->ApplyConfiguration();
     ui->debugTab->ApplyConfiguration();
     ui->webTab->ApplyConfiguration();
+    ui->serviceTab->ApplyConfiguration();
     Settings::Apply();
     Settings::LogSettings();
 }
@@ -74,7 +75,8 @@ Q_DECLARE_METATYPE(QList<QWidget*>);
 void ConfigureDialog::PopulateSelectionList() {
     const std::array<std::pair<QString, QList<QWidget*>>, 4> items{
         {{tr("General"), {ui->generalTab, ui->webTab, ui->debugTab, ui->gameListTab}},
-         {tr("System"), {ui->systemTab, ui->profileManagerTab, ui->filesystemTab, ui->audioTab}},
+         {tr("System"),
+          {ui->systemTab, ui->profileManagerTab, ui->serviceTab, ui->filesystemTab, ui->audioTab}},
          {tr("Graphics"), {ui->graphicsTab}},
          {tr("Controls"), {ui->inputTab, ui->hotkeysTab}}},
     };
@@ -108,6 +110,7 @@ void ConfigureDialog::UpdateVisibleTabs() {
         {ui->webTab, tr("Web")},
         {ui->gameListTab, tr("Game List")},
         {ui->filesystemTab, tr("Filesystem")},
+        {ui->serviceTab, tr("Services")},
     };
 
     [[maybe_unused]] const QSignalBlocker blocker(ui->tabWidget);
diff --git a/src/yuzu/configuration/configure_service.cpp b/src/yuzu/configuration/configure_service.cpp
new file mode 100644
index 000000000..81c9e933f
--- /dev/null
+++ b/src/yuzu/configuration/configure_service.cpp
@@ -0,0 +1,136 @@
+// Copyright 2019 yuzu Emulator Project
+// Licensed under GPLv2 or any later version
+// Refer to the license.txt file included.
+
+#include <QGraphicsItem>
+#include <QtConcurrent/QtConcurrent>
+#include "core/hle/service/bcat/backend/boxcat.h"
+#include "core/settings.h"
+#include "ui_configure_service.h"
+#include "yuzu/configuration/configure_service.h"
+
+namespace {
+QString FormatEventStatusString(const Service::BCAT::EventStatus& status) {
+    QString out;
+
+    if (status.header.has_value()) {
+        out += QStringLiteral("<i>%1</i><br>").arg(QString::fromStdString(*status.header));
+    }
+
+    if (status.events.size() == 1) {
+        out += QStringLiteral("%1<br>").arg(QString::fromStdString(status.events.front()));
+    } else {
+        for (const auto& event : status.events) {
+            out += QStringLiteral("- %1<br>").arg(QString::fromStdString(event));
+        }
+    }
+
+    if (status.footer.has_value()) {
+        out += QStringLiteral("<i>%1</i><br>").arg(QString::fromStdString(*status.footer));
+    }
+
+    return out;
+}
+} // Anonymous namespace
+
+ConfigureService::ConfigureService(QWidget* parent)
+    : QWidget(parent), ui(std::make_unique<Ui::ConfigureService>()) {
+    ui->setupUi(this);
+
+    ui->bcat_source->addItem(QStringLiteral("None"));
+    ui->bcat_empty_label->setHidden(true);
+    ui->bcat_empty_header->setHidden(true);
+
+#ifdef YUZU_ENABLE_BOXCAT
+    ui->bcat_source->addItem(QStringLiteral("Boxcat"), QStringLiteral("boxcat"));
+#endif
+
+    connect(ui->bcat_source, QOverload<int>::of(&QComboBox::currentIndexChanged), this,
+            &ConfigureService::OnBCATImplChanged);
+
+    this->SetConfiguration();
+}
+
+ConfigureService::~ConfigureService() = default;
+
+void ConfigureService::ApplyConfiguration() {
+    Settings::values.bcat_backend = ui->bcat_source->currentText().toLower().toStdString();
+}
+
+void ConfigureService::RetranslateUi() {
+    ui->retranslateUi(this);
+}
+
+void ConfigureService::SetConfiguration() {
+    const int index =
+        ui->bcat_source->findData(QString::fromStdString(Settings::values.bcat_backend));
+    ui->bcat_source->setCurrentIndex(index == -1 ? 0 : index);
+}
+
+std::pair<QString, QString> ConfigureService::BCATDownloadEvents() {
+    std::optional<std::string> global;
+    std::map<std::string, Service::BCAT::EventStatus> map;
+    const auto res = Service::BCAT::Boxcat::GetStatus(global, map);
+
+    switch (res) {
+    case Service::BCAT::Boxcat::StatusResult::Offline:
+        return {QString{},
+                tr("The boxcat service is offline or you are not connected to the internet.")};
+    case Service::BCAT::Boxcat::StatusResult::ParseError:
+        return {QString{},
+                tr("There was an error while processing the boxcat event data. Contact the yuzu "
+                   "developers.")};
+    case Service::BCAT::Boxcat::StatusResult::BadClientVersion:
+        return {QString{},
+                tr("The version of yuzu you are using is either too new or too old for the server. "
+                   "Try updating to the latest official release of yuzu.")};
+    }
+
+    if (map.empty()) {
+        return {QStringLiteral("Current Boxcat Events"),
+                tr("There are currently no events on boxcat.")};
+    }
+
+    QString out;
+
+    if (global.has_value()) {
+        out += QStringLiteral("%1<br>").arg(QString::fromStdString(*global));
+    }
+
+    for (const auto& [key, value] : map) {
+        out += QStringLiteral("%1<b>%2</b><br>%3")
+                   .arg(out.isEmpty() ? QString{} : QStringLiteral("<br>"))
+                   .arg(QString::fromStdString(key))
+                   .arg(FormatEventStatusString(value));
+    }
+    return {QStringLiteral("Current Boxcat Events"), std::move(out)};
+}
+
+void ConfigureService::OnBCATImplChanged() {
+#ifdef YUZU_ENABLE_BOXCAT
+    const auto boxcat = ui->bcat_source->currentText() == QStringLiteral("Boxcat");
+    ui->bcat_empty_header->setHidden(!boxcat);
+    ui->bcat_empty_label->setHidden(!boxcat);
+    ui->bcat_empty_header->setText(QString{});
+    ui->bcat_empty_label->setText(tr("Yuzu is retrieving the latest boxcat status..."));
+
+    if (!boxcat)
+        return;
+
+    const auto future = QtConcurrent::run([this] { return BCATDownloadEvents(); });
+
+    watcher.setFuture(future);
+    connect(&watcher, &QFutureWatcher<std::pair<QString, QString>>::finished, this,
+            [this] { OnUpdateBCATEmptyLabel(watcher.result()); });
+#endif
+}
+
+void ConfigureService::OnUpdateBCATEmptyLabel(std::pair<QString, QString> string) {
+#ifdef YUZU_ENABLE_BOXCAT
+    const auto boxcat = ui->bcat_source->currentText() == QStringLiteral("Boxcat");
+    if (boxcat) {
+        ui->bcat_empty_header->setText(string.first);
+        ui->bcat_empty_label->setText(string.second);
+    }
+#endif
+}
diff --git a/src/yuzu/configuration/configure_service.h b/src/yuzu/configuration/configure_service.h
new file mode 100644
index 000000000..f5c1b703a
--- /dev/null
+++ b/src/yuzu/configuration/configure_service.h
@@ -0,0 +1,34 @@
+// Copyright 2019 yuzu Emulator Project
+// Licensed under GPLv2 or any later version
+// Refer to the license.txt file included.
+
+#pragma once
+
+#include <memory>
+#include <QFutureWatcher>
+#include <QWidget>
+
+namespace Ui {
+class ConfigureService;
+}
+
+class ConfigureService : public QWidget {
+    Q_OBJECT
+
+public:
+    explicit ConfigureService(QWidget* parent = nullptr);
+    ~ConfigureService() override;
+
+    void ApplyConfiguration();
+    void RetranslateUi();
+
+private:
+    void SetConfiguration();
+
+    std::pair<QString, QString> BCATDownloadEvents();
+    void OnBCATImplChanged();
+    void OnUpdateBCATEmptyLabel(std::pair<QString, QString> string);
+
+    std::unique_ptr<Ui::ConfigureService> ui;
+    QFutureWatcher<std::pair<QString, QString>> watcher{this};
+};
diff --git a/src/yuzu/configuration/configure_service.ui b/src/yuzu/configuration/configure_service.ui
new file mode 100644
index 000000000..9668dd557
--- /dev/null
+++ b/src/yuzu/configuration/configure_service.ui
@@ -0,0 +1,124 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>ConfigureService</class>
+ <widget class="QWidget" name="ConfigureService">
+  <property name="geometry">
+   <rect>
+    <x>0</x>
+    <y>0</y>
+    <width>433</width>
+    <height>561</height>
+   </rect>
+  </property>
+  <property name="windowTitle">
+   <string>Form</string>
+  </property>
+  <layout class="QVBoxLayout" name="verticalLayout">
+   <item>
+    <layout class="QVBoxLayout" name="verticalLayout_3">
+     <item>
+      <widget class="QGroupBox" name="groupBox">
+       <property name="title">
+        <string>BCAT</string>
+       </property>
+       <layout class="QGridLayout" name="gridLayout">
+        <item row="1" column="1" colspan="2">
+         <widget class="QLabel" name="label_2">
+          <property name="maximumSize">
+           <size>
+            <width>260</width>
+            <height>16777215</height>
+           </size>
+          </property>
+          <property name="text">
+           <string>BCAT is Nintendo's way of sending data to games to engage its community and unlock additional content.</string>
+          </property>
+          <property name="wordWrap">
+           <bool>true</bool>
+          </property>
+         </widget>
+        </item>
+        <item row="0" column="0">
+         <widget class="QLabel" name="label">
+          <property name="maximumSize">
+           <size>
+            <width>16777215</width>
+            <height>16777215</height>
+           </size>
+          </property>
+          <property name="text">
+           <string>BCAT Backend</string>
+          </property>
+         </widget>
+        </item>
+        <item row="3" column="1" colspan="2">
+         <widget class="QLabel" name="bcat_empty_label">
+          <property name="enabled">
+           <bool>true</bool>
+          </property>
+          <property name="maximumSize">
+           <size>
+            <width>260</width>
+            <height>16777215</height>
+           </size>
+          </property>
+          <property name="text">
+           <string/>
+          </property>
+          <property name="alignment">
+           <set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop</set>
+          </property>
+          <property name="wordWrap">
+           <bool>true</bool>
+          </property>
+         </widget>
+        </item>
+        <item row="2" column="1" colspan="2">
+         <widget class="QLabel" name="label_3">
+          <property name="text">
+           <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;a href=&quot;https://yuzu-emu.org/help/feature/boxcat&quot;&gt;&lt;span style=&quot; text-decoration: underline; color:#0000ff;&quot;&gt;Learn more about BCAT, Boxcat, and Current Events&lt;/span&gt;&lt;/a&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
+          </property>
+          <property name="openExternalLinks">
+           <bool>true</bool>
+          </property>
+         </widget>
+        </item>
+        <item row="0" column="1" colspan="2">
+         <widget class="QComboBox" name="bcat_source"/>
+        </item>
+        <item row="3" column="0">
+         <widget class="QLabel" name="bcat_empty_header">
+          <property name="text">
+           <string/>
+          </property>
+          <property name="alignment">
+           <set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop</set>
+          </property>
+          <property name="wordWrap">
+           <bool>true</bool>
+          </property>
+         </widget>
+        </item>
+       </layout>
+      </widget>
+     </item>
+    </layout>
+   </item>
+   <item>
+    <spacer name="verticalSpacer">
+     <property name="orientation">
+      <enum>Qt::Vertical</enum>
+     </property>
+     <property name="sizeHint" stdset="0">
+      <size>
+       <width>20</width>
+       <height>40</height>
+      </size>
+     </property>
+    </spacer>
+   </item>
+  </layout>
+ </widget>
+ <resources/>
+ <connections/>
+</ui>
diff --git a/src/yuzu_cmd/config.cpp b/src/yuzu_cmd/config.cpp
index d82438502..1a812cb87 100644
--- a/src/yuzu_cmd/config.cpp
+++ b/src/yuzu_cmd/config.cpp
@@ -433,6 +433,11 @@ void Config::ReadValues() {
         sdl2_config->Get("WebService", "web_api_url", "https://api.yuzu-emu.org");
     Settings::values.yuzu_username = sdl2_config->Get("WebService", "yuzu_username", "");
     Settings::values.yuzu_token = sdl2_config->Get("WebService", "yuzu_token", "");
+
+    // Services
+    Settings::values.bcat_backend = sdl2_config->Get("Services", "bcat_backend", "boxcat");
+    Settings::values.bcat_boxcat_local =
+        sdl2_config->GetBoolean("Services", "bcat_boxcat_local", false);
 }
 
 void Config::Reload() {
diff --git a/src/yuzu_cmd/default_ini.h b/src/yuzu_cmd/default_ini.h
index a6171c3ed..8d18a4a5a 100644
--- a/src/yuzu_cmd/default_ini.h
+++ b/src/yuzu_cmd/default_ini.h
@@ -251,6 +251,11 @@ web_api_url = https://api.yuzu-emu.org
 yuzu_username =
 yuzu_token =
 
+[Services]
+# The name of the backend to use for BCAT
+# If this is set to 'boxcat' boxcat will be used, otherwise a null implementation will be used
+bcat_backend =
+
 [AddOns]
 # Used to disable add-ons
 # List of title IDs of games that will have add-ons disabled (separated by '|'):