diff --git a/README.md b/README.md
index 3686351dbfb27bc7585a6395f703ef2a4fe8c4dd..6a8c78bd93606ae4a6d5ef69fa140692d063dc86 100644
--- a/README.md
+++ b/README.md
@@ -5,7 +5,7 @@
 [![Maven Local](https://img.shields.io/maven-metadata/v?metadataUrl=https%3A%2F%2Fgit.ucsc.edu%2Fapi%2Fv4%2Fprojects%2F12162%2Fpackages%2Fmaven%2Fedu%2Fucsc%2Fits%2Ftemerity%2Fmaven-metadata.xml)](https://git.ucsc.edu/wnwalker/temerity/-/packages)
 [![Pipeline](https://git.ucsc.edu/wnwalker/temerity/badges/main/pipeline.svg)](https://git.ucsc.edu/wnwalker/temerity/-/pipelines)
 [![Issues](https://img.shields.io/gitlab/issues/open/wnwalker%2Ftemerity?gitlab_url=https%3A%2F%2Fgit.ucsc.edu)](https://git.ucsc.edu/wnwalker/temerity/-/issues)
-[![Kotlin](https://img.shields.io/badge/kotlin-2.0.20-blue?logo=kotlin)](http://kotlinlang.org)
+[![Kotlin](https://img.shields.io/badge/kotlin-2.0.21-blue?logo=kotlin)](http://kotlinlang.org)
 [![Gradle](https://img.shields.io/badge/Gradle-8-green?style=flat)](https://gradle.org)
 [![License](https://img.shields.io/badge/license-GNU%20LGPL%202.1-blue.svg?style=flat)](https://www.gnu.org/licenses/old-licenses/lgpl-2.1.en.html)
 
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index c9cac7a48116769bcc6169e1fa1f22ee35281cd0..4cc239f93dca70175de051edbb1e6c5364011575 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -2,25 +2,28 @@
 compose-multiplatform = "1.7.1"
 agp = "8.2.2"
 java = "17"
-kotlin = "2.0.21"
-ksp = "2.0.21-1.0.28"
+kotlin = "2.1.0"
+ksp = "2.1.0-1.0.29"
 
-ktor = "3.0.1"
-kotlinx-serialization = "1.7.3"
+kotlinx-serialization = "1.8.0-RC"
+ktor = "3.0.3"
 cache4k = "0.13.0"
+statelyConcurrentCollections = "2.1.0"
 slf4j = "2.0.16"
-
+kotlinx-io = "0.6.0"
 
 ktorfit = "2.2.0"
-coroutines = "1.9.0"
+coroutines = "1.10.1"
 datetime = "0.6.1"
 sandwich = "2.0.10"
 arrow = "1.2.4"
 # Main Koin version - for core, android deps
-koin = "4.0.0"
-koinTest = "4.0.0"
+koin = "4.0.1"
+koinTest = "4.0.1"
+gradleBuildConfigPlugin = "5.5.1"
+akkurate = "0.11.0"
 # Koin version for Compose multiplatform
-koinComposeMultiplatform = "4.0.0"
+koinComposeMultiplatform = "4.0.1"
 mosaic = "0.13.0"
 molecule = "2.0.0"
 adaptive = "1.0.1"
@@ -42,8 +45,11 @@ jline = "3.27.1"
 appdirs = "1.2.0"
 kstore = "0.9.1"
 kmpIo = "0.1.5"
+kotlinSemver = "2.0.0"
+jansi = "2.4.1"
+temerity = "[0.1.0-dev0z+41180a5,0.1.0]"
 
-kotlinx-dataframe = "0.13.1"
+kotlinx-dataframe = "0.14.2"
 klaxon = "5.6"
 kotest = "6.0.0.M1"
 kotest-datatest = "5.9.1"
@@ -54,7 +60,7 @@ alertKmp = "1.0.7"
 filekit = "0.8.7"
 composetray = "0.4.0"
 
-dokka = "2.0.0-Beta"
+dokka = "2.0.0"
 spotless = "7.0.0.BETA4"
 gitSemVer = "3.1.7"
 conventionalCommits = "1.0.12"
@@ -74,7 +80,10 @@ ktor-client-java = { group = "io.ktor", name = "ktor-client-java", version.ref =
 ktor-client-android = { group = "io.ktor", name = "ktor-client-android", version.ref = "ktor" }
 ktor-test = { group = "io.ktor", name = "ktor-client-mock", version.ref = "ktor" }
 cache4k = { group = "io.github.reactivecircus.cache4k", name = "cache4k", version.ref = "cache4k" }
+statelyConcurrentCollections = { group = "co.touchlab", name = "stately-concurrent-collections", version.ref = "statelyConcurrentCollections" }
 slf4j = { group = "org.slf4j", name = "slf4j-simple", version.ref = "slf4j" }
+slf4j-api = { group = "org.slf4j", name = "slf4j-api", version.ref = "slf4j" }
+
 
 kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinx-serialization" }
 kotlinx-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "coroutines" }
@@ -82,6 +91,7 @@ kotlinx-coroutines-debug = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-
 kotlinx-coroutines-swing = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-swing", version.ref = "coroutines" }
 kotlinx-coroutines-slf4j = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-slf4j", version.ref = "coroutines" }
 kotlinx-datetime = { module = "org.jetbrains.kotlinx:kotlinx-datetime", version.ref = "datetime" }
+kotlinx-io = { module = "org.jetbrains.kotlinx:kotlinx-io-core", version.ref = "kotlinx-io" }
 
 ktorfit-lib = { module = "de.jensklingenberg.ktorfit:ktorfit-lib-light", version.ref = "ktorfit" }
 
@@ -93,6 +103,9 @@ appdirs = { module = "ca.gosyer:kotlin-multiplatform-appdirs", version.ref = "ap
 kstore = { module = "io.github.xxfast:kstore", version.ref = "kstore" }
 kstore-file = { module = "io.github.xxfast:kstore-file", version.ref = "kstore" }
 kmpIo = { module = "io.github.skolson:kmp-io", version.ref = "kmpIo" }
+kotlinSemver = { module = "io.github.z4kn4fein:semver", version.ref = "kotlinSemver" }
+jansi = { module = "org.fusesource.jansi:jansi", version.ref = "jansi" }
+temerity = { module = "edu.ucsc.its:temerity", version.ref = "temerity" }
 
 sandwich = { module = "com.github.skydoves:sandwich", version.ref = "sandwich" }
 sandwich-ktor = { module = "com.github.skydoves:sandwich-ktor", version.ref = "sandwich" }
@@ -104,6 +117,7 @@ arrow-core = { module = "io.arrow-kt:arrow-core", version.ref = "arrow" }
 arrow-fxCoroutines = { module = "io.arrow-kt:arrow-fx-coroutines", version.ref = "arrow" }
 arrow-optics = { module = "io.arrow-kt:arrow-optics", version.ref = "arrow" }
 arrow-opticsKspPlugin = { module = "io.arrow-kt:arrow-optics-ksp-plugin", version.ref = "arrow" }
+akkurate = { module = "dev.nesk.akkurate:akkurate-core", version.ref = "akkurate" }
 
 mosaic = { module = "com.jakewharton.mosaic:mosaic-runtime", version.ref = "mosaic" }
 koin-compose = { module = "io.insert-koin:koin-compose", version.ref = "koinComposeMultiplatform" }
@@ -183,6 +197,7 @@ ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" }
 ktorfit = { id = "de.jensklingenberg.ktorfit", version.ref = "ktorfit" }
 kotlinxSerialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" }
 kotestMultiplatform = { id = "io.kotest.multiplatform", version.ref = "kotest" }
+buildConfig = { id = "com.github.gmazzo.buildconfig", version.ref = "gradleBuildConfigPlugin" }
 dokka = { id = "org.jetbrains.dokka", version.ref = "dokka" }
 spotless = { id = "com.diffplug.spotless", version.ref = "spotless" }
 gitSemVer = { id = "org.danilopianini.git-sensitive-semantic-versioning", version.ref = "gitSemVer" }
diff --git a/settings.gradle.kts b/settings.gradle.kts
index 51e1835fae0d4507b3624fc2cc6601091c23176b..d9a15e5af808390c423889b46d8a9cd7a95ecf1d 100644
--- a/settings.gradle.kts
+++ b/settings.gradle.kts
@@ -25,6 +25,7 @@ dependencyResolutionManagement {
         maven("https://maven.pkg.jetbrains.space/public/p/ktor/eap")
         maven("https://maven.pkg.jetbrains.space/kotlin/p/kotlin/dev")
         maven("https://plugins.gradle.org/m2")
+        maven("https://git.ucsc.edu/api/v4/projects/12162/packages/maven")
     }
 }
 
diff --git a/shared/shared/build.gradle.kts b/shared/shared/build.gradle.kts
index c74b195f04e69d495e193739b1d539175ac8af11..75bd6ae91b60ee86f6d024c73e43c0d6e59c05e8 100644
--- a/shared/shared/build.gradle.kts
+++ b/shared/shared/build.gradle.kts
@@ -54,7 +54,7 @@ kotlin {
     sourceSets {
         val commonMain by getting {
             dependencies {
-                api(project(":temerity"))
+                api(libs.temerity)
 
                 api(libs.koin.core)
                 implementation(libs.koin.composeVm)
diff --git a/temerity/NOTICE b/temerity/NOTICE
index 63f8d0fa9d96698bafee25b7a2a884655b75c898..9ad9767805d081fcc63003a80956b53cf79bbef6 100644
--- a/temerity/NOTICE
+++ b/temerity/NOTICE
@@ -1,7 +1,34 @@
-This product includes software developed by Álvaro Salcedo García
+This library includes portions of software developed by John O'Reilly et. al.: https://github.com/joreilly/FantasyPremierLeague/
+
+Function(s): buildHttpClient()
+File(s): commonMain/Temerity.kt
+Description: A method which builds and returns a Ktor HttpClient given a request engine, engine config, and logger
+License: Apache License, Version 2.0
+
+--------------------------------------------
+
+This library includes portions of software developed by Chris Krueger et. al: https://github.com/ChrisKruegerDev/tmdb-kotlin
+
+Function(s): buildHttpClient()
+File(s): commonMain/Temerity.kt
+Description: A method which builds and returns a Ktor HttpClient given a request engine, engine config, and logger
+License: Apache License, Version 2.0
+
+--------------------------------------------
+
+This library includes software developed by Álvaro Salcedo García
 
 File(s): jvmTest/Util.kt
 Description: A utility function for configuring Koin DI context in Kotest tests.
 License: Apache License, Version 2.0
 
+--------------------------------------------
+
+This library includes software developed by Paul Hawke et. al: https://github.com/psh/KermitExt
+
+File(s): /extensions/log/*.kt
+Description: A set of extension functions for Kermit, a Kotlin multiplatform logging library.
+These allow for custom log file writers, builders, and framework binders.
+License: Apache License, Version 2.0
+
 --------------------------------------------
\ No newline at end of file
diff --git a/temerity/TemerityDemo.ipynb b/temerity/TemerityDemo.ipynb
index 55b1a462c121edb5bb154f47c948aea6d1df8f95..48c19dbce2243aad4fab3b1df87bb2ab2e34525e 100644
--- a/temerity/TemerityDemo.ipynb
+++ b/temerity/TemerityDemo.ipynb
@@ -2,27 +2,104 @@
  "cells": [
   {
    "cell_type": "code",
-   "execution_count": null,
    "metadata": {
-    "collapsed": true
+    "collapsed": true,
+    "ExecuteTime": {
+     "end_time": "2024-12-04T17:26:08.492177800Z",
+     "start_time": "2024-12-04T17:26:08.025486300Z"
+    }
    },
-   "outputs": [],
    "source": [
-    "import edu.ucsc.its.temerity.PlatformClient\n",
+    "// README: Execute this notebook with the temerity.shared.shared.jvmMain source set active ^^^\n",
+    "import edu.ucsc.its.temerity.TemClientConfig\n",
+    "import edu.ucsc.its.temerity.core.Temerity\n",
+    "import io.github.cdimascio.dotenv.Dotenv\n",
     "import kotlinx.coroutines.runBlocking\n",
-    "        \n",
-    "startTemerity()\n",
-    "val client = platformClient {\n",
-    "            serviceEndpoint(\"https://your_organization.yuja.com/services/\")\n",
-    "            serviceToken(\"your_API_token\") }\n",
-    "runBlocking { client.getUsers().forEach { println(it) } }\n",
+    "import org.dotenv.vault.dotenvVault\n",
     "\n",
-    "val returnedDevices = runBlocking { client.getDevices() }\n",
-    "println(\"Returned devices:\")\n",
-    "returnedDevices.forEach { println(it) }\n",
+    "fun TemClientConfig.configureDevNotebookEnvironment(dotenv: Dotenv) {\n",
+    "  serviceUrl = dotenv[\"YUJADEV_API_URL\"]\n",
+    "  serviceToken = dotenv[\"YUJADEV_TOKEN\"]\n",
+    "  optDebugEnabled = true\n",
+    "  supportKtxNotebook = true\n",
+    "}\n",
     "\n",
-    "stopTemerity()"
-   ]
+    "\n",
+    "\n",
+    "//val temerityClient = Temerity {\n",
+    "//  serviceUrl = \"https://your_organization.yuja.com/services/\"\n",
+    "//  serviceToken = \"your_API_token\"\n",
+    "//}\n",
+    "\n",
+    "val temerityClient = Temerity {\n",
+    "  configureDevNotebookEnvironment(dotenvVault())\n",
+    "}\n",
+    "runBlocking { temerityClient.refreshCachedUserRoles().joinToString(\", \").also { println() } }\n",
+    "\n",
+    "val returnedDevices = runBlocking { temerityClient.getDevices() }\n",
+    "println(\"Returned devices: ${ returnedDevices.joinToString(\", \") }\")\n",
+    "\n",
+    "\n",
+    "\n"
+   ],
+   "outputs": [
+    {
+     "ename": "org.jetbrains.kotlinx.jupyter.exceptions.ReplCompilerException",
+     "evalue": "at Cell In[1], line 4, column 18: Unresolved reference: cdimascio\nat Cell In[1], line 6, column 12: Unresolved reference: dotenv\nat Cell In[1], line 8, column 61: Unresolved reference: Dotenv\nat Cell In[1], line 23, column 35: Unresolved reference: dotenvVault",
+     "output_type": "error",
+     "traceback": [
+      "org.jetbrains.kotlinx.jupyter.exceptions.ReplCompilerException: at Cell In[1], line 4, column 18: Unresolved reference: cdimascio",
+      "at Cell In[1], line 6, column 12: Unresolved reference: dotenv",
+      "at Cell In[1], line 8, column 61: Unresolved reference: Dotenv",
+      "at Cell In[1], line 23, column 35: Unresolved reference: dotenvVault",
+      "\tat org.jetbrains.kotlinx.jupyter.repl.impl.JupyterCompilerImpl.compileSync(JupyterCompilerImpl.kt:208)",
+      "\tat org.jetbrains.kotlinx.jupyter.repl.impl.InternalEvaluatorImpl.eval(InternalEvaluatorImpl.kt:126)",
+      "\tat org.jetbrains.kotlinx.jupyter.repl.impl.CellExecutorImpl$execute$1$result$1.invoke(CellExecutorImpl.kt:80)",
+      "\tat org.jetbrains.kotlinx.jupyter.repl.impl.CellExecutorImpl$execute$1$result$1.invoke(CellExecutorImpl.kt:78)",
+      "\tat org.jetbrains.kotlinx.jupyter.repl.impl.ReplForJupyterImpl.withHost(ReplForJupyterImpl.kt:774)",
+      "\tat org.jetbrains.kotlinx.jupyter.repl.impl.CellExecutorImpl.execute-L4Nmkdk(CellExecutorImpl.kt:78)",
+      "\tat org.jetbrains.kotlinx.jupyter.repl.execution.CellExecutor$DefaultImpls.execute-L4Nmkdk$default(CellExecutor.kt:13)",
+      "\tat org.jetbrains.kotlinx.jupyter.repl.impl.ReplForJupyterImpl.evaluateUserCode-wNURfNM(ReplForJupyterImpl.kt:596)",
+      "\tat org.jetbrains.kotlinx.jupyter.repl.impl.ReplForJupyterImpl.evalExImpl(ReplForJupyterImpl.kt:454)",
+      "\tat org.jetbrains.kotlinx.jupyter.repl.impl.ReplForJupyterImpl.access$evalExImpl(ReplForJupyterImpl.kt:141)",
+      "\tat org.jetbrains.kotlinx.jupyter.repl.impl.ReplForJupyterImpl$evalEx$1.invoke(ReplForJupyterImpl.kt:447)",
+      "\tat org.jetbrains.kotlinx.jupyter.repl.impl.ReplForJupyterImpl$evalEx$1.invoke(ReplForJupyterImpl.kt:446)",
+      "\tat org.jetbrains.kotlinx.jupyter.repl.impl.ReplForJupyterImpl.withEvalContext(ReplForJupyterImpl.kt:427)",
+      "\tat org.jetbrains.kotlinx.jupyter.repl.impl.ReplForJupyterImpl.evalEx(ReplForJupyterImpl.kt:446)",
+      "\tat org.jetbrains.kotlinx.jupyter.messaging.IdeCompatibleMessageRequestProcessor$processExecuteRequest$1$response$1$1.invoke(IdeCompatibleMessageRequestProcessor.kt:171)",
+      "\tat org.jetbrains.kotlinx.jupyter.messaging.IdeCompatibleMessageRequestProcessor$processExecuteRequest$1$response$1$1.invoke(IdeCompatibleMessageRequestProcessor.kt:170)",
+      "\tat org.jetbrains.kotlinx.jupyter.streams.BlockingSubstitutionEngine.withDataSubstitution(SubstitutionEngine.kt:70)",
+      "\tat org.jetbrains.kotlinx.jupyter.streams.StreamSubstitutionManager.withSubstitutedStreams(StreamSubstitutionManager.kt:118)",
+      "\tat org.jetbrains.kotlinx.jupyter.messaging.IdeCompatibleMessageRequestProcessor.withForkedIn(IdeCompatibleMessageRequestProcessor.kt:347)",
+      "\tat org.jetbrains.kotlinx.jupyter.messaging.IdeCompatibleMessageRequestProcessor.access$withForkedIn(IdeCompatibleMessageRequestProcessor.kt:67)",
+      "\tat org.jetbrains.kotlinx.jupyter.messaging.IdeCompatibleMessageRequestProcessor$evalWithIO$1$1.invoke(IdeCompatibleMessageRequestProcessor.kt:361)",
+      "\tat org.jetbrains.kotlinx.jupyter.streams.BlockingSubstitutionEngine.withDataSubstitution(SubstitutionEngine.kt:70)",
+      "\tat org.jetbrains.kotlinx.jupyter.streams.StreamSubstitutionManager.withSubstitutedStreams(StreamSubstitutionManager.kt:118)",
+      "\tat org.jetbrains.kotlinx.jupyter.messaging.IdeCompatibleMessageRequestProcessor.withForkedErr(IdeCompatibleMessageRequestProcessor.kt:336)",
+      "\tat org.jetbrains.kotlinx.jupyter.messaging.IdeCompatibleMessageRequestProcessor.access$withForkedErr(IdeCompatibleMessageRequestProcessor.kt:67)",
+      "\tat org.jetbrains.kotlinx.jupyter.messaging.IdeCompatibleMessageRequestProcessor$evalWithIO$1.invoke(IdeCompatibleMessageRequestProcessor.kt:360)",
+      "\tat org.jetbrains.kotlinx.jupyter.streams.BlockingSubstitutionEngine.withDataSubstitution(SubstitutionEngine.kt:70)",
+      "\tat org.jetbrains.kotlinx.jupyter.streams.StreamSubstitutionManager.withSubstitutedStreams(StreamSubstitutionManager.kt:118)",
+      "\tat org.jetbrains.kotlinx.jupyter.messaging.IdeCompatibleMessageRequestProcessor.withForkedOut(IdeCompatibleMessageRequestProcessor.kt:328)",
+      "\tat org.jetbrains.kotlinx.jupyter.messaging.IdeCompatibleMessageRequestProcessor.evalWithIO(IdeCompatibleMessageRequestProcessor.kt:359)",
+      "\tat org.jetbrains.kotlinx.jupyter.messaging.IdeCompatibleMessageRequestProcessor$processExecuteRequest$1$response$1.invoke(IdeCompatibleMessageRequestProcessor.kt:170)",
+      "\tat org.jetbrains.kotlinx.jupyter.messaging.IdeCompatibleMessageRequestProcessor$processExecuteRequest$1$response$1.invoke(IdeCompatibleMessageRequestProcessor.kt:169)",
+      "\tat org.jetbrains.kotlinx.jupyter.execution.JupyterExecutorImpl$Task.execute(JupyterExecutorImpl.kt:41)",
+      "\tat org.jetbrains.kotlinx.jupyter.execution.JupyterExecutorImpl$executorThread$1.invoke(JupyterExecutorImpl.kt:81)",
+      "\tat org.jetbrains.kotlinx.jupyter.execution.JupyterExecutorImpl$executorThread$1.invoke(JupyterExecutorImpl.kt:79)",
+      "\tat kotlin.concurrent.ThreadsKt$thread$thread$1.run(Thread.kt:30)",
+      ""
+     ]
+    }
+   ],
+   "execution_count": 1
+  },
+  {
+   "metadata": {},
+   "cell_type": "code",
+   "source": "",
+   "outputs": [],
+   "execution_count": null
   }
  ],
  "metadata": {
@@ -41,26 +118,9 @@
    "nbconvert_exporter": ""
   },
   "ktnbPluginMetadata": {
+   "projectLibraries": false,
    "projectDependencies": [
-    "Temerity.library.androidTest",
-    "Temerity.library.appleMain",
-    "Temerity.library.appleTest",
-    "Temerity.library.commonMain",
-    "Temerity.library.commonTest",
-    "Temerity.library.iosArm64Main",
-    "Temerity.library.iosArm64Test",
-    "Temerity.library.iosMain",
-    "Temerity.library.iosSimulatorArm64Main",
-    "Temerity.library.iosSimulatorArm64Test",
-    "Temerity.library.iosTest",
-    "Temerity.library.iosX64Main",
-    "Temerity.library.iosX64Test",
-    "Temerity.library.jvmMain",
-    "Temerity.library.jvmTest",
-    "Temerity.library.main",
-    "Temerity.library.nativeMain",
-    "Temerity.library.nativeTest",
-    "Temerity.library.unitTest"
+    "temerity.shared.shared.jvmMain"
    ]
   }
  },
diff --git a/temerity/build.gradle.kts b/temerity/build.gradle.kts
index f98fbe7efe856748e5d37d83b0c0852cdaa019eb..62d36fb7861d2aed3a52e41b86aa2b1853b9b0ef 100644
--- a/temerity/build.gradle.kts
+++ b/temerity/build.gradle.kts
@@ -26,6 +26,7 @@ plugins {
     alias(libs.plugins.ktorfit)
     alias(libs.plugins.kotlinxSerialization)
     alias(libs.plugins.kotestMultiplatform)
+    alias(libs.plugins.buildConfig)
 
     id("convention.formattingLib")
     id("convention.version")
@@ -43,6 +44,11 @@ buildscript {
     }
 }
 
+buildConfig {
+    buildConfigField("LIB_VERSION", provider { "${project.version}" })
+    buildConfigField("PACKAGE_NAME", provider { "edu.ucsc.its.${project.name}" })
+}
+
 gitSemVer {
     commitNameBasedUpdateStrategy(ConventionalCommit::semanticVersionUpdate)
 }
@@ -79,9 +85,11 @@ kotlin {
                 implementation(libs.koin.core)
                 implementation(libs.ktorfit.lib)
                 implementation(libs.kotlinx.coroutines.core)
-                implementation(libs.kermit)
-                implementation(libs.cache4k)
+                api(libs.kermit)
+                // TODO: Re-add cache4k when updated for Kotlin 2.0.21
+                implementation(libs.statelyConcurrentCollections)
                 implementation(libs.kmpIo)
+                implementation(libs.kotlinx.io)
             }
         }
         val commonTest by getting {
@@ -96,12 +104,14 @@ kotlin {
         val jvmMain by getting {
             dependencies {
                 implementation(libs.ktor.client.java)
-                implementation(libs.slf4j)
+                implementation(libs.slf4j.api)
+                implementation(libs.jansi)
             }
         }
         val androidMain by getting {
             dependencies {
                 implementation(libs.ktor.client.android)
+                // TODO: Add slf4f-api
             }
         }
         val jvmTest by getting {
@@ -118,6 +128,8 @@ kotlin {
 
                 implementation(libs.koin.test)
                 implementation(libs.koin.test.junit5)
+
+                implementation(libs.kotlinSemver)
             }
         }
     }
diff --git a/temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/core/JsonFactory.kt b/temerity/src/androidMain/kotlin/edu/ucsc/its/temerity/core/LoggerFactory.android.kt
similarity index 72%
rename from temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/core/JsonFactory.kt
rename to temerity/src/androidMain/kotlin/edu/ucsc/its/temerity/core/LoggerFactory.android.kt
index 02f61f54d0c020a81d018f20a3fb4642f3659449..bb98a1e554585bbdc31135f62c2040b2caac7e78 100644
--- a/temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/core/JsonFactory.kt
+++ b/temerity/src/androidMain/kotlin/edu/ucsc/its/temerity/core/LoggerFactory.android.kt
@@ -17,15 +17,13 @@
  */
 package edu.ucsc.its.temerity.core
 
-import kotlinx.serialization.json.Json
+import co.touchlab.kermit.Logger
+import edu.ucsc.its.temerity.TemClientConfig
 
-/**
- * Factory for creating a Ktor [Json] object.
- * Adapted from the tmdb-api project: https://github.com/MoviebaseApp/tmdb-kotlin/raw/refs/heads/main/tmdb-api/src/commonMain/kotlin/app/moviebase/tmdb/core/JsonFactory.kt
- */
-internal object JsonFactory {
-
-  fun buildJson(): Json = Json {
-    coerceInputValues = true
-  }
+internal actual fun createCommonLogger(
+  tag: String?,
+  config: TemClientConfig?,
+  supportKtxNotebook: Boolean,
+): Logger {
+  TODO("Not yet implemented")
 }
diff --git a/temerity/src/androidMain/kotlin/edu/ucsc/its/temerity/di/PlatformModule.android.kt b/temerity/src/androidMain/kotlin/edu/ucsc/its/temerity/di/PlatformModule.android.kt
new file mode 100644
index 0000000000000000000000000000000000000000..1f3994819517bc487b1b88a47077c10fcb72ec73
--- /dev/null
+++ b/temerity/src/androidMain/kotlin/edu/ucsc/its/temerity/di/PlatformModule.android.kt
@@ -0,0 +1,31 @@
+/*
+ *     Designed and developed in 2022-2024 by William Walker (wnwalker@ucsc.edu)
+ *     Copyright 2022-2024 The Regents of the University of California. All rights reserved.
+ *
+ *     This library is free software; you can redistribute it and/or
+ *     modify it under the terms of the GNU Lesser General Public
+ *     License as published by the Free Software Foundation; version 2.1 of the License.
+ *
+ *     This library is distributed in the hope that it will be useful,
+ *     but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ *     Lesser General Public License for more details.
+ *
+ *     You should have received a copy of the GNU Lesser General Public
+ *     License along with this library; if not, write to the Free Software
+ *     Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
+ */
+package edu.ucsc.its.temerity.di
+
+import io.ktor.client.engine.android.Android
+import kotlinx.coroutines.CoroutineDispatcher
+import org.koin.core.qualifier.named
+import org.koin.dsl.module
+
+internal actual fun platformModule() = module {
+  single(named("httpClientEngine")) { (libraryCoroutineDispatcher: CoroutineDispatcher) ->
+    Android.create {
+      dispatcher = libraryCoroutineDispatcher
+    }
+  }
+}
diff --git a/temerity/src/androidMain/kotlin/edu/ucsc/its/temerity/Actual.kt b/temerity/src/androidMain/kotlin/edu/ucsc/its/temerity/extensions/log/Filesystem.android.kt
similarity index 83%
rename from temerity/src/androidMain/kotlin/edu/ucsc/its/temerity/Actual.kt
rename to temerity/src/androidMain/kotlin/edu/ucsc/its/temerity/extensions/log/Filesystem.android.kt
index 5aac3efe907f7463ff9a2a3c9842b217b04eff4e..094b3d7530deffde589ca3ccadd23ad63130c62f 100644
--- a/temerity/src/androidMain/kotlin/edu/ucsc/its/temerity/Actual.kt
+++ b/temerity/src/androidMain/kotlin/edu/ucsc/its/temerity/extensions/log/Filesystem.android.kt
@@ -15,11 +15,9 @@
  *     License along with this library; if not, write to the Free Software
  *     Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  */
-package edu.ucsc.its.temerity
+package edu.ucsc.its.temerity.extensions.log
 
-import io.ktor.client.engine.android.Android
-import org.koin.dsl.module
+import kotlinx.io.files.FileSystem
+import kotlinx.io.files.SystemFileSystem
 
-internal actual fun platformModule() = module {
-  single { Android.create() }
-}
+internal actual fun fileSystem(): FileSystem = SystemFileSystem
diff --git a/temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/TemClientConfig.kt b/temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/TemClientConfig.kt
index a7372e974a21539852a64daba54998d237b4f674..491f006cd23c7ba5e4e3b89a6dc90309ea65ef9b 100644
--- a/temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/TemClientConfig.kt
+++ b/temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/TemClientConfig.kt
@@ -33,44 +33,50 @@ import kotlin.time.Duration
  * @property serviceToken Specifies the authorization token to use for API calls
  * @property optDebugEnabled Specifies whether debug logging should be enabled
  *
- * @property expectSuccess Specifies whether to expect successful responses by default
- * @property useWebTimeout Specifies whether to use a timeout  other than default for web requests
- * @property webTimeout Specifies the timeout [Duration] to use when making web requests
- * @property cacheTimeout Specifies the timeout [Duration] to use when storing cache entries. Affects keep-alive time for cached instance data like user role type fields.
+ * @property optExpectSuccess Specifies whether to expect successful responses by default
+ * @property optUseWebTimeout Specifies whether to use a timeout  other than default for web requests
+ * @property optWebTimeoutDuration Specifies the timeout [Duration] to use when making web requests
+ * @property optCacheTimeoutDuration Specifies the timeout [Duration] to use when storing cache entries. Affects keep-alive time for cached instance data like user role type fields.
+ * @property optThreadCount Specifies the number of threads to use for each client instance. Defaults to 2 at a minimum
  */
 @TemDsl
 public class TemClientConfig {
 
   public var serviceUrl: String? = null
   public var serviceToken: String? = null
+  public var optLoggingEnabled: Boolean = true
   public var optDebugEnabled: Boolean = false
+  public var optSupportKtxNotebook: Boolean = false
 
-  public var expectSuccess: Boolean = true
-  public var useWebTimeout: Boolean = false
+  public var optExpectSuccess: Boolean = true
+  public var optUseWebTimeout: Boolean = false
 
   @Suppress("MemberVisibilityCanBePrivate")
-  public var webTimeout: Duration? = null
+  public var optWebTimeoutDuration: Duration? = null
 
+  @Suppress("MemberVisibilityCanBePrivate")
   // Default cache timeout is set to 15 minutes if this is left unspecified; @see [edu.ucsc.its.temerity.core.Temerity] init block
-  public var cacheTimeout: Duration? = null
+  public var optCacheTimeoutDuration: Duration? = null
+
+  public var optThreadCount: Int? = null
 
   /**
    * Configures the HttpClient with the provided block.
    * @param () -> HttpClient The block to configure the HttpClient.
    */
-  internal var httpClientConfigBlock: (HttpClientConfig<*>.() -> Unit)? = null
-  internal var httpClientBuilder: (() -> HttpClient)? = null
-  internal var httpClientLoggingBlock: (LoggingConfig.() -> Unit)? = null
+  internal var optHttpClientConfigBlock: (HttpClientConfig<*>.() -> Unit)? = null
+  internal var optHttpClientBuilder: (() -> HttpClient)? = null
+  internal var optHttpClientLoggingBlock: (LoggingConfig.() -> Unit)? = null
 
   public fun logging(block: LoggingConfig.() -> Unit) {
-    httpClientLoggingBlock = block
+    optHttpClientLoggingBlock = block
   }
 
   /**
    * Set custom HttpClient configuration for the default HttpClient.
    */
   public fun httpClient(block: HttpClientConfig<*>.() -> Unit) {
-    httpClientConfigBlock = block
+    optHttpClientConfigBlock = block
   }
 
   /**
@@ -81,13 +87,12 @@ public class TemClientConfig {
     engineFactory: HttpClientEngineFactory<T>,
     block: HttpClientConfig<T>.() -> Unit = {},
   ) {
-    httpClientBuilder = {
+    optHttpClientBuilder = {
       HttpClient(engineFactory, block)
     }
   }
 
   public companion object {
-
     internal fun withToken(serviceToken: String): TemClientConfig = TemClientConfig().apply {
       this.serviceToken = serviceToken
     }
diff --git a/temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/TemerityApi.kt b/temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/TemerityApi.kt
index c0ab3d5dec0fcc35641200802be56d3ff6e6409a..93adda8dca388bae8e967d6cc332868bf3aaf126 100644
--- a/temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/TemerityApi.kt
+++ b/temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/TemerityApi.kt
@@ -17,6 +17,7 @@
  */
 package edu.ucsc.its.temerity
 
+import edu.ucsc.its.temerity.extensions.datetime.DateTimeExt.currentDate
 import edu.ucsc.its.temerity.model.AuditLogEntry
 import edu.ucsc.its.temerity.model.Course
 import edu.ucsc.its.temerity.model.Device
@@ -47,6 +48,13 @@ public interface TemerityApi {
 
   // User API public library functions
 
+  /**
+   * Fetches the version of the Temerity client library.
+   * @return The version of the client library as a semantic versioning formatted string.
+   */
+  @Suppress("PropertyName")
+  public val version: String
+
   /**
    * Fetches the list of users from the platform.
    * @return A list of User objects
@@ -225,10 +233,14 @@ public interface TemerityApi {
 
   public suspend fun refreshCachedUserRoles(): List<String>
 
-  public suspend fun getCachedUserRoles(refresh: Boolean): List<String>
+  /**
+   * Fetches the user roles from the platform cached locally on the client.
+   * @param refresh Whether to fetch the roles if not present locally.
+   */
+  public suspend fun getCachedUserRoles(refresh: Boolean = true): List<String>
 }
 
-// Objects used by Temerity implementations, and possibly library consumers
+// Objects used by library consumers and re-implementations of the Temerity API spec
 
 public val AUDIT_LOG_REQUEST_DATE_FORMAT: DateTimeFormat<LocalDate> = LocalDate.Format {
   dayOfMonth()
diff --git a/temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/core/HttpClientFactory.kt b/temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/core/HttpClientFactory.kt
deleted file mode 100644
index 3adbd44ebfb7fd1108319b03781c16ee998e0722..0000000000000000000000000000000000000000
--- a/temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/core/HttpClientFactory.kt
+++ /dev/null
@@ -1,106 +0,0 @@
-/*
- *     Designed and developed in 2022-2024 by William Walker (wnwalker@ucsc.edu)
- *     Copyright 2022-2024 The Regents of the University of California. All rights reserved.
- *
- *     This library is free software; you can redistribute it and/or
- *     modify it under the terms of the GNU Lesser General Public
- *     License as published by the Free Software Foundation; version 2.1 of the License.
- *
- *     This library is distributed in the hope that it will be useful,
- *     but WITHOUT ANY WARRANTY; without even the implied warranty of
- *     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
- *     Lesser General Public License for more details.
- *
- *     You should have received a copy of the GNU Lesser General Public
- *     License along with this library; if not, write to the Free Software
- *     Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
- */
-package edu.ucsc.its.temerity.core
-
-import edu.ucsc.its.temerity.TemClientConfig
-import edu.ucsc.its.temerity.core.TemerityLibrary.koin
-import io.ktor.client.HttpClient
-import io.ktor.client.HttpClientConfig
-import io.ktor.client.engine.HttpClientEngine
-import io.ktor.client.plugins.HttpTimeout
-import io.ktor.client.plugins.defaultRequest
-import io.ktor.client.plugins.logging.LogLevel
-import io.ktor.client.plugins.logging.Logging
-import io.ktor.client.request.header
-import io.ktor.http.URLProtocol
-import kotlin.time.DurationUnit
-import co.touchlab.kermit.Logger as KermitLogger
-import io.ktor.client.plugins.logging.Logger as KtorLogger
-/**
- * Factory providing web request clients for the Temerity client.
- * Adapted from the tmdb-api project: https://github.com/MoviebaseApp/tmdb-kotlin/raw/refs/heads/main/tmdb-api/src/commonMain/kotlin/app/moviebase/tmdb/core/HttpClientFactory.kt
- */
-internal object HttpClientFactory {
-  /**
-   * Adapted from https://github.com/joreilly/FantasyPremierLeague/
-   * This function creates an HttpClient with the provided HttpClientEngine.
-   * The HttpClientEngine is injected by the Koin platformModule(), which as its name suggests provides an HttpClientEngine implementation that is platform-dependent.
-   * It configures the client to log all requests and responses if enableNetworkLogs is true. TODO: read this as a build setting
-   * It also sets a default request header with the provided authToken.
-   * @param httpClientEngine The HttpClientEngine to use for the HttpClient.
-   * @param timeoutDuration The duration in milliseconds to wait for a response before timing out.
-   * @param config A block specifying the following options:
-   *  - Whether to log all requests and responses.
-   *  - Service URL to make requests to.
-   *  - Token to set as a default request header.
-   * @return The created HttpClient.
-   */
-  internal fun buildHttpClient(
-    httpClientEngine: HttpClientEngine,
-    config: TemClientConfig,
-  ): HttpClient {
-    val defaultConfig: HttpClientConfig<*>.() -> Unit = {
-      // Can't use install(ContentNegotiation){ json() } here because YuJa's API returns a 406 error if the Accept header is set to application/json
-      // This requires JSON transformations to be done via manually-called library functions
-
-      // Pass the stored authToken as a header in every request
-      defaultRequest {
-        header("authToken", config.serviceToken)
-        url {
-          protocol = URLProtocol.HTTPS
-        }
-      }
-
-      if (config.optDebugEnabled) {
-        if (config.httpClientLoggingBlock == null) {
-          install(Logging) {
-            val kermit = koin.get<KermitLogger>()
-            logger = object : KtorLogger {
-              override fun log(message: String) {
-                kermit.d(message)
-              }
-            }
-            level = LogLevel.ALL
-            sanitizeHeader { headerKey ->
-              headerKey == "authToken"
-            }
-          }
-        } else {
-          config.httpClientLoggingBlock?.let {
-            Logging(it)
-          }
-        }
-      }
-
-      expectSuccess = config.expectSuccess
-
-      if (config.useWebTimeout) {
-        val defaultWebTimeout = TemerityLibrary.DEFAULT_WEB_TIMEOUT.toLong(DurationUnit.MILLISECONDS)
-        val configuredWebTimeout = config.webTimeout?.toLong(DurationUnit.MILLISECONDS)
-        install(HttpTimeout) {
-          connectTimeoutMillis = configuredWebTimeout ?: defaultWebTimeout
-          requestTimeoutMillis = configuredWebTimeout ?: defaultWebTimeout
-          socketTimeoutMillis = configuredWebTimeout ?: defaultWebTimeout
-        }
-      }
-
-      config.httpClientConfigBlock?.invoke(this)
-    }
-    return config.httpClientBuilder?.invoke()?.config(defaultConfig) ?: HttpClient(httpClientEngine, defaultConfig)
-  }
-}
diff --git a/temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/core/Library.kt b/temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/core/Library.kt
deleted file mode 100644
index b074f8808843fb6d7ec83e1d12d94dbee7cb5f0f..0000000000000000000000000000000000000000
--- a/temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/core/Library.kt
+++ /dev/null
@@ -1,56 +0,0 @@
-/*
- *     Designed and developed in 2022-2024 by William Walker (wnwalker@ucsc.edu)
- *     Copyright 2022-2024 The Regents of the University of California. All rights reserved.
- *
- *     This library is free software; you can redistribute it and/or
- *     modify it under the terms of the GNU Lesser General Public
- *     License as published by the Free Software Foundation; version 2.1 of the License.
- *
- *     This library is distributed in the hope that it will be useful,
- *     but WITHOUT ANY WARRANTY; without even the implied warranty of
- *     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
- *     Lesser General Public License for more details.
- *
- *     You should have received a copy of the GNU Lesser General Public
- *     License along with this library; if not, write to the Free Software
- *     Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
- */
-package edu.ucsc.its.temerity.core
-
-import edu.ucsc.its.temerity.TemClientConfig
-import edu.ucsc.its.temerity.di.libModule
-import edu.ucsc.its.temerity.di.loggerModule
-import edu.ucsc.its.temerity.platformModule
-import org.koin.core.Koin
-import org.koin.dsl.koinApplication
-import kotlin.time.Duration
-import kotlin.time.Duration.Companion.minutes
-
-/**
- * TemerityLibrary is the main entry point for the Temerity library.
- *
- * This object initializes the Koin dependency injection framework and returns a library or builder instance
- *
- */
-public object TemerityLibrary {
-  public val DEFAULT_WEB_TIMEOUT: Duration = 2.minutes
-
-  private val koinApp = koinApplication {
-    modules(
-      loggerModule(),
-      platformModule(),
-      libModule,
-    )
-  }
-  internal val koin: Koin = koinApp.koin
-}
-
-/**
- * Create a Temerity instance using Kotlin-DSL.
- * Adapted from the tmdb-api project: https://github.com/MoviebaseApp/tmdb-kotlin/raw/refs/heads/main/tmdb-api/src/commonMain/kotlin/app/moviebase/tmdb/Tmdb4.kt
- */
-@TemDsl
-public fun Temerity(block: TemClientConfig.() -> Unit): Temerity {
-  val config = TemClientConfig().apply(block)
-  return Temerity(config)
-}
diff --git a/temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/core/LoggerFactory.kt b/temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/core/LoggerFactory.kt
new file mode 100644
index 0000000000000000000000000000000000000000..eae0e55d1ac6f140e52c3baaf6a6a7d94766233a
--- /dev/null
+++ b/temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/core/LoggerFactory.kt
@@ -0,0 +1,27 @@
+/*
+ *     Designed and developed in 2022-2024 by William Walker (wnwalker@ucsc.edu)
+ *     Copyright 2022-2024 The Regents of the University of California. All rights reserved.
+ *
+ *     This library is free software; you can redistribute it and/or
+ *     modify it under the terms of the GNU Lesser General Public
+ *     License as published by the Free Software Foundation; version 2.1 of the License.
+ *
+ *     This library is distributed in the hope that it will be useful,
+ *     but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ *     Lesser General Public License for more details.
+ *
+ *     You should have received a copy of the GNU Lesser General Public
+ *     License along with this library; if not, write to the Free Software
+ *     Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
+ */
+package edu.ucsc.its.temerity.core
+
+import co.touchlab.kermit.Logger
+import edu.ucsc.its.temerity.TemClientConfig
+
+internal object LoggerFactory {
+  internal fun createLogger(tag: String?, config: TemClientConfig? = null, supportKtxNotebook: Boolean = false) = createCommonLogger(tag = tag, config = config, supportKtxNotebook = supportKtxNotebook)
+}
+
+internal expect fun createCommonLogger(tag: String?, config: TemClientConfig? = null, supportKtxNotebook: Boolean = false): Logger
diff --git a/temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/core/Temerity.kt b/temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/core/Temerity.kt
index 896c0ad21a9ee2768b1214459305d7c4a57de4d2..ab73b7c140a8d20f60ad72aac32e60db4ae0759e 100644
--- a/temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/core/Temerity.kt
+++ b/temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/core/Temerity.kt
@@ -17,27 +17,33 @@
  */
 package edu.ucsc.its.temerity.core
 
+import co.touchlab.stately.collections.ConcurrentMutableMap
 import com.skydoves.sandwich.ApiResponse
-import com.skydoves.sandwich.StatusCode
+import com.skydoves.sandwich.StatusCode.NoContent
 import com.skydoves.sandwich.getOrThrow
 import com.skydoves.sandwich.ktor.executeApiResponse
 import com.skydoves.sandwich.ktor.statusCode
 import com.skydoves.sandwich.onSuccess
 import edu.ucsc.its.temerity.AuditLogSortOrder
+import edu.ucsc.its.temerity.AuditLogSortOrder.NEW_FIRST
+import edu.ucsc.its.temerity.BuildConfig
 import edu.ucsc.its.temerity.TemClientConfig
 import edu.ucsc.its.temerity.TemerityApi
 import edu.ucsc.its.temerity.api.PlatformApi
-import edu.ucsc.its.temerity.core.JsonFactory.buildJson
-import edu.ucsc.its.temerity.core.TemerityLibrary.koin
-import edu.ucsc.its.temerity.createJobScope
-import edu.ucsc.its.temerity.extensions.applyAuditLogFormat
-import edu.ucsc.its.temerity.extensions.applyScheduledSessionDateFormat
+import edu.ucsc.its.temerity.core.Temerity.Companion.DEFAULT_WEB_TIMEOUT
+import edu.ucsc.its.temerity.di.LibModule.libModule
+import edu.ucsc.its.temerity.di.platformModule
+import edu.ucsc.its.temerity.extensions.coroutines.availableThreads
+import edu.ucsc.its.temerity.extensions.coroutines.createJobScope
+import edu.ucsc.its.temerity.extensions.datetime.DateTimeExt
+import edu.ucsc.its.temerity.extensions.time.applyAuditLogFormat
+import edu.ucsc.its.temerity.extensions.time.applyScheduledSessionDateFormat
 import edu.ucsc.its.temerity.model.AuditLogEntry
 import edu.ucsc.its.temerity.model.Course
 import edu.ucsc.its.temerity.model.Device
 import edu.ucsc.its.temerity.model.DeviceRecordingSession
 import edu.ucsc.its.temerity.model.EventType
-import edu.ucsc.its.temerity.model.FolderPermissions
+import edu.ucsc.its.temerity.model.FolderPermissions.Action
 import edu.ucsc.its.temerity.model.Group
 import edu.ucsc.its.temerity.model.NewUser
 import edu.ucsc.its.temerity.model.User
@@ -45,40 +51,147 @@ import edu.ucsc.its.temerity.model.UserGroup
 import edu.ucsc.its.temerity.model.UserRecordingSession
 import edu.ucsc.its.temerity.model.UserUpdate
 import edu.ucsc.its.temerity.sortByCreationDate
-import io.github.reactivecircus.cache4k.Cache
+import io.ktor.client.HttpClient
+import io.ktor.client.HttpClientConfig
+import io.ktor.client.engine.HttpClientEngine
+import io.ktor.client.plugins.HttpTimeout
+import io.ktor.client.plugins.defaultRequest
+import io.ktor.client.plugins.logging.LogLevel.ALL
+import io.ktor.client.plugins.logging.Logging
+import io.ktor.client.request.header
 import io.ktor.client.statement.HttpResponse
+import io.ktor.http.URLProtocol
 import io.ktor.util.cio.toByteArray
 import io.ktor.utils.io.ByteReadChannel
-import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.async
 import kotlinx.coroutines.awaitAll
 import kotlinx.coroutines.delay
+import kotlinx.coroutines.withContext
 import kotlinx.datetime.DateTimeUnit
 import kotlinx.datetime.LocalDate
+import kotlinx.datetime.LocalTime
 import kotlinx.datetime.minus
 import kotlinx.serialization.SerializationException
-import kotlinx.serialization.encodeToString
 import kotlinx.serialization.json.Json
-import org.koin.core.context.GlobalContext.get
+import org.koin.core.Koin
+import org.koin.core.KoinApplication
+import org.koin.core.component.KoinComponent
+import org.koin.core.component.get
+import org.koin.core.logger.Level
+import org.koin.core.logger.Level.DEBUG
+import org.koin.core.logger.Level.ERROR
+import org.koin.core.logger.Level.INFO
+import org.koin.core.logger.Level.NONE
+import org.koin.core.logger.Level.WARNING
+import org.koin.core.logger.Logger
+import org.koin.core.logger.MESSAGE
 import org.koin.core.parameter.parametersOf
+import org.koin.core.qualifier.named
+import org.koin.dsl.koinApplication
+import kotlin.time.Duration
 import kotlin.time.Duration.Companion.minutes
+import kotlin.time.DurationUnit.MILLISECONDS
 import co.touchlab.kermit.Logger as KermitLogger
+import io.ktor.client.plugins.logging.Logger as KtorLogger
 
 /*
  * Adapted from https://github.com/santimattius/kmp-networking/blob/main/README.md
  */
 public class Temerity internal constructor(
   private val config: TemClientConfig,
-) : TemerityApi {
-
+) : TemerityApi,
+  KoinComponent {
   /**
    * Constructor for Temerity
+   * @param temApiToken The [String] API token to use for the Temerity client
    */
   internal constructor(temApiToken: String) : this(TemClientConfig.withToken(temApiToken))
 
-  private val json: Json = buildJson()
-  private val platformApi: PlatformApi
-  private val cachedUserRoleList: Cache<Int, String>
+  public companion object {
+    @JvmStatic
+    public val DEFAULT_WEB_TIMEOUT: Duration = 2.minutes
+
+    // TODO: Use this as cache4k expiration time
+    @JvmStatic
+    public val DEFAULT_CACHE_EXPIRATION: Duration = 15.minutes
+
+    @JvmStatic
+    internal val DEFAULT_MINIMUM_THREAD_COUNT: Int = 2
+
+    @JvmStatic
+    internal fun createLogger(
+      tag: String? = null,
+      config: TemClientConfig? = null,
+      supportKtxNotebook: Boolean = false,
+    ): KermitLogger = LoggerFactory.createLogger(
+      tag = tag,
+      config = config,
+      supportKtxNotebook = supportKtxNotebook,
+    )
+
+    @JvmStatic
+    @Suppress("MemberVisibilityCanBePrivate")
+    public fun currentDate(): LocalDate = DateTimeExt.currentDate()
+
+    @JvmStatic
+    @Suppress("MemberVisibilityCanBePrivate")
+    public fun currentTime(): LocalTime = DateTimeExt.currentTime()
+  }
+
+  /**
+   * This function initializes the Koin dependency injection framework,
+   * which provides the library's [HttpClient] and [PlatformApi] instances
+   * via factories defined as part of the [libModule].
+   */
+  private fun createKoinApp(config: TemClientConfig) = object : TemerityKoinContext() {
+    override val logger = createCommonLogger(tag = BuildConfig.PACKAGE_NAME, config)
+    override val koinApp = koinApplication {
+      logger(object : Logger() {
+        override fun display(level: Level, msg: MESSAGE) {
+          when (level) {
+            DEBUG -> logger.d(msg)
+            INFO -> logger.i(msg)
+            ERROR -> logger.e(msg)
+            NONE -> logger.v(msg)
+            WARNING -> logger.w(msg)
+          }
+        }
+      })
+      modules(
+        libModule(),
+        platformModule(),
+      )
+    }
+    override val koin: Koin = koinApp.koin
+  }
+
+  internal abstract class TemerityKoinContext {
+    abstract val koinApp: KoinApplication
+    abstract val koin: Koin
+    abstract val logger: KermitLogger
+  }
+
+  override val version: String = BuildConfig.LIB_VERSION
+
+  /**
+   * Kotlinx-serialization [Json] encoder/decoder object used for serializing/deserializing JSON object responses
+   */
+  private val json: Json = Json {
+    coerceInputValues = true
+  }
+  internal val koinContext = createKoinApp(config)
+  override fun getKoin(): Koin = koinContext.koin
+
+  private var libraryLogger: KermitLogger
+  private var platformApi: PlatformApi
+  private var cachedUserRoleList: ConcurrentMutableMap<Int, String>
+  private var libraryCoroutineDispatcher: CoroutineDispatcher
+  private var libraryCoroutineScope: CoroutineScope
+  private var jsonProcessingDispatcher: CoroutineDispatcher
+  private var webRequestDispatcher: CoroutineDispatcher
+  private var fileProcessDispatcher: CoroutineDispatcher
 
   init {
     check(!config.serviceToken.isNullOrBlank()) {
@@ -89,13 +202,26 @@ public class Temerity internal constructor(
       "Service url must be provided. Set it using TemClientConfig.serviceUrl(url)"
     }
 
-    platformApi = koin.get<PlatformApi> {
-      parametersOf(config)
+    libraryCoroutineDispatcher = get<CoroutineDispatcher>(named("libraryCoroutineDispatcher")) {
+      parametersOf(availableThreads(config.optThreadCount), "Temerity Library Dispatcher")
+    }
+    libraryCoroutineScope = get<CoroutineScope>(named("libraryCoroutineScope")) {
+      parametersOf(libraryCoroutineDispatcher)
     }
 
-    cachedUserRoleList = Cache.Builder<Int, String>()
-      .expireAfterWrite(config.cacheTimeout ?: 15.minutes)
-      .build()
+    jsonProcessingDispatcher = get<CoroutineDispatcher>(named("childDispatcher")) { parametersOf(libraryCoroutineDispatcher, 2, "TemerityJsonProcessingCoroutineDispatcher") }
+    webRequestDispatcher = get<CoroutineDispatcher>(named("childDispatcher")) { parametersOf(libraryCoroutineDispatcher, 2, "TemerityWebRequestCoroutineDispatcher") }
+    fileProcessDispatcher = get<CoroutineDispatcher>(named("childDispatcher")) { parametersOf(libraryCoroutineDispatcher, 1, "TemerityFileProcessCoroutineDispatcher") }
+
+    libraryLogger = get<KermitLogger>(named("libraryLogger")) {
+      parametersOf(BuildConfig.PACKAGE_NAME, config)
+    }
+
+    platformApi = get<PlatformApi>(named("ktorfitApi")) {
+      parametersOf(config, webRequestDispatcher)
+    }
+
+    cachedUserRoleList = ConcurrentMutableMap()
   }
 
   /**
@@ -114,15 +240,16 @@ public class Temerity internal constructor(
         // If the API returns a 204 No Content status code, we should throw a BreakException to stop the page iteration loop
         // As of Cashew release, this is only used in getGroups() to stop the loop when no more groups are returned
         // May be used in the future for other paginated endpoints (such as getAuditLogEntries)
-        StatusCode.NoContent -> throw BreakException(page - 1)
+        NoContent -> throw BreakException(page - 1)
 
         else -> Unit
       }
     }
     val apiResponsePayload = response.getOrThrow()
+
     if (apiResponsePayload == "[]") throw BreakException(page - 1)
     // Handle deserialization exceptions. Queue up decode:
-    val deserializedJson = runCatching {
+    val deserializedJson = createJobScope(jsonProcessingDispatcher).runCatching {
       json.decodeFromString<T>(apiResponsePayload)
     }
     // Attempt, returning the deserialized object if successful
@@ -130,8 +257,12 @@ public class Temerity internal constructor(
       when (exception) {
         is SerializationException -> {
           // TODO: Implement platform API version checking
-          get().get<KermitLogger>().d { "Returned JSON object: ${exception.message}" }
-          error("Encountered error decoding response from platform API. You likely need to choose a Temerity release that supports the API version implemented by your instance. \nPlatform API response data : $apiResponsePayload")
+          get<KermitLogger>(named("libraryLogger")).d { "Returned JSON object: ${exception.message}" }
+          val error = "Encountered error decoding response from platform API. You likely need to choose a Temerity release that supports the API version implemented by your instance. \nPlatform API response data : $apiResponsePayload"
+          error.run {
+            get<KermitLogger>(named("libraryLogger")).e { this }
+            error(this)
+          }
         }
 
         else -> throw exception
@@ -140,61 +271,95 @@ public class Temerity internal constructor(
   }
 
   public override suspend fun getUsers(): List<User> =
-    decodeResponseCatching(platformApi.getUsers().executeApiResponse<String>())
+    withContext(libraryCoroutineDispatcher) {
+      val userRequest = platformApi.getUsers()
+      val response = userRequest.executeApiResponse<String>()
+      decodeResponseCatching(response)
+    }
 
   public override suspend fun getUser(userId: Long): User =
-    getUsers().first { it.userId == userId }
-
-  public override suspend fun createUser(primaryIdentifier: String, newUser: NewUser): HttpResponse {
-    val validationMessage = newUser.validate()
-    if (validationMessage != "Valid") {
-      error(validationMessage)
-    } else {
-      return platformApi.createUser(primaryIdentifier, json.encodeToString(newUser)).getOrThrow()
+    withContext(libraryCoroutineDispatcher) {
+      val returnedUsers = getUsers()
+      returnedUsers.first { it.userId == userId }
+    }
+
+  public override suspend fun createUser(primaryIdentifier: String, newUser: NewUser): HttpResponse =
+    withContext(libraryCoroutineDispatcher) {
+      val validationMessage = newUser.validate()
+      if (validationMessage != "Valid") {
+        error(validationMessage)
+      } else {
+        val serializedNewUser = createJobScope(jsonProcessingDispatcher).run {
+          json.encodeToString(newUser)
+        }
+        val request = platformApi.createUser(primaryIdentifier, serializedNewUser)
+        request.getOrThrow()
+      }
     }
-  }
 
   public override suspend fun updateUser(userId: Long, userUpdate: UserUpdate): HttpResponse =
-    platformApi.setUser(userId, json.encodeToString(userUpdate)).getOrThrow()
+    withContext(libraryCoroutineDispatcher) {
+      val serializedUpdatedUser = createJobScope(jsonProcessingDispatcher).run {
+        json.encodeToString(userUpdate)
+      }
+      val request = platformApi.setUser(userId, serializedUpdatedUser)
+      request.getOrThrow()
+    }
 
   public override suspend fun deleteUser(userId: Long): HttpResponse =
-    platformApi.deleteUser(userId).getOrThrow()
-
-  public override suspend fun refreshCachedUserRoles(): List<String> {
-    val returnedUsersResponse = platformApi.getUsers().executeApiResponse<String>().getOrThrow()
-    val returnedUserList = json.decodeFromString<List<User>>(returnedUsersResponse)
-    val roleTypes = returnedUserList.map { it.userType }.distinct()
-    roleTypes.forEach {
-      cachedUserRoleList.put(it.hashCode(), it)
+    withContext(libraryCoroutineDispatcher) {
+      val request = platformApi.deleteUser(userId)
+      request.getOrThrow()
     }
-    return roleTypes
-  }
 
-  public override suspend fun getCachedUserRoles(refresh: Boolean): List<String> {
-    if (refresh) {
-      return refreshCachedUserRoles()
+  public override suspend fun refreshCachedUserRoles(): List<String> =
+    withContext(libraryCoroutineDispatcher) {
+      val usersRequest = platformApi.getUsers()
+      val returnedUsersResponse = usersRequest.executeApiResponse<String>().getOrThrow()
+      val returnedUserList = createJobScope(jsonProcessingDispatcher).run {
+        json.decodeFromString<List<User>>(returnedUsersResponse)
+      }
+      val roleTypes = returnedUserList.map { it.userType }.distinct()
+      cachedUserRoleList.clear()
+      roleTypes.forEach {
+        cachedUserRoleList[it.hashCode()] = it
+      }
+      roleTypes
     }
-    val returnedUserRoles = cachedUserRoleList.asMap().values.toList()
-    return returnedUserRoles.ifEmpty {
-      refreshCachedUserRoles()
+
+  public override suspend fun getCachedUserRoles(refresh: Boolean): List<String> =
+    withContext(libraryCoroutineDispatcher) {
+      if (refresh) {
+        refreshCachedUserRoles()
+      }
+      val returnedUserRoles = cachedUserRoleList.values.toList()
+      returnedUserRoles.ifEmpty {
+        refreshCachedUserRoles()
+      }
     }
-  }
 
   public override suspend fun getUserGroups(userId: Long): List<UserGroup> =
-    decodeResponseCatching(platformApi.getUserGroups(userId).executeApiResponse<String>())
+    withContext(libraryCoroutineDispatcher) {
+      val request = platformApi.getUserGroups(userId)
+      val response = request.executeApiResponse<String>()
+      decodeResponseCatching(response)
+    }
 
-  public override suspend fun getUserGroupsOwned(userId: Long): List<UserGroup> =
-    decodeResponseCatching<List<UserGroup>>(platformApi.getUserGroupsOwned(userId).executeApiResponse())
+  public override suspend fun getUserGroupsOwned(userId: Long): List<UserGroup> = withContext(libraryCoroutineDispatcher) {
+    val request = platformApi.getUserGroupsOwned(userId)
+    val response = request.executeApiResponse<String>()
+    decodeResponseCatching<List<UserGroup>>(response)
+  }
 
   public override suspend fun createUserWithExternalId(primaryIdentifier: String, newUser: NewUser): HttpResponse {
     TODO("Not yet implemented")
   }
 
-  public override suspend fun addFolderPermission(perms: FolderPermissions.Action): HttpResponse {
+  public override suspend fun addFolderPermission(perms: Action): HttpResponse {
     TODO("Not yet implemented")
   }
 
-  public override suspend fun removeFolderPermission(perms: FolderPermissions.Action) {
+  public override suspend fun removeFolderPermission(perms: Action) {
     TODO("Not yet implemented")
   }
 
@@ -202,41 +367,44 @@ public class Temerity internal constructor(
     TODO("Not yet implemented")
   }
 
-  public override suspend fun getGroups(paginated: Boolean): List<Group> {
+  public override suspend fun getGroups(paginated: Boolean): List<Group> = withContext(libraryCoroutineDispatcher) {
     when (paginated) {
       true -> {
         val returnedGroups = ArrayList<Group>()
         try {
           for (x in (0..Int.MAX_VALUE)) {
-            val thisPage = platformApi.getGroupPage(x, true)
-            val thisPageGroups: List<Group> = decodeResponseCatching(thisPage.executeApiResponse<String>(), page = x)
+            val thisPageRequest = platformApi.getGroupPage(x, true)
+            val response = thisPageRequest.executeApiResponse<String>()
+            val thisPageGroups: List<Group> = decodeResponseCatching(response, page = x)
             returnedGroups.addAll(thisPageGroups)
-            delay(750)
           }
         } catch (e: Exception) {
           when (e) {
             is BreakException -> {
               if (config.optDebugEnabled) {
-                get().get<KermitLogger>().d("Caught BreakException() from decodeResponseCatching() notifying we're done reading groups from API: ${e.page} pages read")
+                get<KermitLogger>(named("libraryLogger")).d("Caught BreakException() from decodeResponseCatching() notifying we're done reading groups from API: ${e.page} pages read")
               }
-              return returnedGroups
+              return@withContext returnedGroups
             }
 
             else -> throw e
           }
         }
-        return returnedGroups
+        returnedGroups
       }
+
       else -> {
-        return emptyList()
+        emptyList()
       }
     }
   }
 
-  public override suspend fun getCourse(courseCode: String): Course = decodeResponseCatching(platformApi.getCourse(courseCode).executeApiResponse<String>())
+  public override suspend fun getCourse(courseCode: String): Course = withContext(libraryCoroutineDispatcher) {
+    decodeResponseCatching(platformApi.getCourse(courseCode).executeApiResponse<String>())
+  }
 
-  public override suspend fun getCourses(paginated: Boolean): List<Course> {
-    return when (paginated) {
+  public override suspend fun getCourses(paginated: Boolean): List<Course> = withContext(libraryCoroutineDispatcher) {
+    when (paginated) {
       true -> {
         val returnedCourses = ArrayList<Course>()
         try {
@@ -251,15 +419,15 @@ public class Temerity internal constructor(
           when (e) {
             is BreakException -> {
               if (config.optDebugEnabled) {
-                get().get<KermitLogger>().d("Caught BreakException() from decodeResponseCatching() notifying we're done reading groups from API: $e")
+                get<KermitLogger>(named("libraryLogger")).d("Caught BreakException() from decodeResponseCatching() notifying we're done reading groups from API: $e")
               }
-              return returnedCourses
+              return@withContext returnedCourses
             }
 
             else -> throw e
           }
         }
-        return returnedCourses
+        returnedCourses
       }
 
       false -> {
@@ -276,10 +444,10 @@ public class Temerity internal constructor(
     endTime: LocalDate,
     eventTypeList: List<EventType>?,
     sortOrder: AuditLogSortOrder?,
-  ): List<AuditLogEntry> {
+  ): List<AuditLogEntry> = withContext(libraryCoroutineDispatcher) {
     // TODO: Error out if user inputs time window > 1 month, since the platform API doesn't support
     val returnedEntries = ArrayList<AuditLogEntry>()
-    val jobScope = createJobScope(Dispatchers.IO)
+    val jobScope = createJobScope(libraryCoroutineScope.coroutineContext)
     val requestJobs = with(eventTypeList ?: EventType.entries) {
       map { eventType ->
         jobScope.async {
@@ -292,8 +460,8 @@ public class Temerity internal constructor(
     results.forEach { returnedEntries.addAll(it) }
 
     // Apply passed sortOrder, or default to NEW_FIRST TODO: Read from user settings
-    returnedEntries.applyOrDefault(sortOrder ?: AuditLogSortOrder.NEW_FIRST)
-    return returnedEntries
+    returnedEntries.applyOrDefault(sortOrder ?: NEW_FIRST)
+    returnedEntries
   }
 
   // Get audit log entries for a specific event type within window
@@ -304,17 +472,17 @@ public class Temerity internal constructor(
     eventType: EventType,
     sortOrder: AuditLogSortOrder?,
     paginated: Boolean,
-  ): List<AuditLogEntry> {
+  ): List<AuditLogEntry> = withContext(libraryCoroutineDispatcher) {
     require(startTime <= endTime) {
       "End date must be same or later than start date"
     }
     when (paginated) {
       true -> {
         val returnedEntries = ArrayList<AuditLogEntry>()
-        var windowStart = if (endTime.minus(1, DateTimeUnit.Companion.MONTH) < startTime) {
+        var windowStart = if (endTime.minus(1, DateTimeUnit.MONTH) < startTime) {
           startTime
         } else {
-          endTime.minus(1, DateTimeUnit.Companion.MONTH)
+          endTime.minus(1, DateTimeUnit.MONTH)
         }
         var windowEnd = endTime
         while (windowStart < windowEnd) {
@@ -326,14 +494,18 @@ public class Temerity internal constructor(
                 entryOffset = x * 75,
                 eventType = eventType.value,
               )
-              val logEventEntries: List<AuditLogEntry> = decodeResponseCatching(endpointRequest.executeApiResponse<String>(), page = x)
+              val response = createJobScope(webRequestDispatcher).run {
+                endpointRequest.executeApiResponse<String>()
+              }
+              val logEventEntries: List<AuditLogEntry> =
+                decodeResponseCatching(response, page = x)
               returnedEntries.addAll(logEventEntries)
             }
           } catch (e: Exception) {
             when (e) {
               is BreakException -> {
                 if (config.optDebugEnabled) {
-                  get().get<KermitLogger>().d("Caught BreakException() from decodeResponseCatching() notifying we're done reading audit log entries from API for specific window: ${e.page} pages read")
+                  get<KermitLogger>(named("libraryLogger")).d("Caught BreakException() from decodeResponseCatching() notifying we're done reading audit log entries from API for specific window: ${e.page} pages read")
                 }
                 break
               }
@@ -342,64 +514,167 @@ public class Temerity internal constructor(
             }
           }
           windowEnd = windowStart
-          val backStep = windowStart.minus(1, DateTimeUnit.Companion.MONTH)
+          val backStep = windowStart.minus(1, DateTimeUnit.MONTH)
           windowStart = if (backStep < startTime) startTime else backStep
         }
         // Apply passed sortOrder, or default to NEW_FIRST TODO: Read from user settings
-        returnedEntries.applyOrDefault(sortOrder ?: AuditLogSortOrder.NEW_FIRST)
-        return returnedEntries
+        returnedEntries.applyOrDefault(sortOrder ?: NEW_FIRST)
+        returnedEntries
       }
+
       false -> {
         TODO("Implement non-paginated audit log entry retrieval")
       }
     }
   }
 
-  private fun ArrayList<AuditLogEntry>.applyOrDefault(sortOrder: AuditLogSortOrder?): ArrayList<AuditLogEntry> = when (sortOrder) {
-    null -> {
-      // Currently reads from prefs, falling back to BuildConfig constant. Can be overridden by passing a sort order as a parameter.
-      // TODO: Implement a way to pass a clientDefaultSortOrder to Builder
-      // TODO: Persist user-defined default sort order for subsequent runs
-      apply { sortByCreationDate(AuditLogSortOrder.NEW_FIRST) }
-    }
+  private suspend fun ArrayList<AuditLogEntry>.applyOrDefault(sortOrder: AuditLogSortOrder?): ArrayList<AuditLogEntry> = withContext(libraryCoroutineDispatcher) {
+    with(this@applyOrDefault) {
+      when (sortOrder) {
+        null -> {
+          // Currently reads from prefs, falling back to BuildConfig constant. Can be overridden by passing a sort order as a parameter.
+          // TODO: Implement a way to pass a clientDefaultSortOrder to Builder
+          // TODO: Persist user-defined default sort order for subsequent runs
+          apply { sortByCreationDate(NEW_FIRST) }
+        }
 
-    else -> apply { sortByCreationDate(sortOrder) }
+        else -> apply { sortByCreationDate(sortOrder) }
+      }
+    }
   }
 
-  // TODO: Implement setUser() function
-
-  public override suspend fun getDevices(): List<Device> =
-    decodeResponseCatching(platformApi.getDevices().executeApiResponse<String>())
+  public override suspend fun getDevices(): List<Device> = withContext(libraryCoroutineDispatcher) {
+    val request = platformApi.getDevices()
+    val response = request.executeApiResponse<String>()
+    decodeResponseCatching(response)
+  }
 
-  public override suspend fun getDevice(deviceId: Long): Device =
-    decodeResponseCatching(platformApi.getDeviceById(deviceId).executeApiResponse<String>())
+  public override suspend fun getDevice(deviceId: Long): Device = withContext(libraryCoroutineDispatcher) {
+    val request = platformApi.getDeviceById(deviceId)
+    val response = request.executeApiResponse<String>()
+    decodeResponseCatching(response)
+  }
 
   public override suspend fun getUserSessions(
     userId: String,
     startTime: LocalDate,
     endTime: LocalDate,
-  ): List<UserRecordingSession> =
-    decodeResponseCatching(
-      platformApi.getUserSessions(
-        userId,
-        startTime.applyScheduledSessionDateFormat(),
-        endTime.applyScheduledSessionDateFormat(),
-      ).executeApiResponse<String>(),
+  ): List<UserRecordingSession> = withContext(libraryCoroutineDispatcher) {
+    val request = platformApi.getUserSessions(
+      userId,
+      startTime.applyScheduledSessionDateFormat(),
+      endTime.applyScheduledSessionDateFormat(),
     )
+    val response = request.executeApiResponse<String>()
+    decodeResponseCatching(response)
+  }
 
   public override suspend fun getDeviceSchedule(
     deviceId: Long,
     startTime: LocalDate,
     endTime: LocalDate,
-  ): List<DeviceRecordingSession> =
-    decodeResponseCatching(
-      platformApi.getDeviceSchedule(
-        deviceId,
-        startTime.applyScheduledSessionDateFormat(),
-        endTime.applyScheduledSessionDateFormat(),
-      ).executeApiResponse<String>(),
+  ): List<DeviceRecordingSession> = withContext(libraryCoroutineDispatcher) {
+    val request = platformApi.getDeviceSchedule(
+      deviceId,
+      startTime.applyScheduledSessionDateFormat(),
+      endTime.applyScheduledSessionDateFormat(),
     )
+    val response = request.executeApiResponse<String>()
+    decodeResponseCatching(response)
+  }
 
-  public override suspend fun getStorageAnalyticsReport(groupId: Long): ByteArray =
-    platformApi.getStorageAnalyticsReport(groupId).executeApiResponse<ByteReadChannel>().getOrThrow().toByteArray(limit = 2000000000) // Limit file downloads to 2 GB
+  public override suspend fun getStorageAnalyticsReport(groupId: Long): ByteArray = withContext(libraryCoroutineDispatcher) {
+    val apiRequest = platformApi.getStorageAnalyticsReport(groupId)
+    val responseBytes = apiRequest.executeApiResponse<ByteReadChannel>().getOrThrow()
+    withContext(fileProcessDispatcher) {
+      responseBytes.toByteArray(limit = 2000000000)
+    } // Limit file downloads to 2 GB
+  }
 }
+
+/**
+ * Adapted from https://github.com/joreilly/FantasyPremierLeague/
+ * Adapted from the tmdb-api project: https://github.com/MoviebaseApp/tmdb-kotlin/raw/refs/heads/main/tmdb-api/src/commonMain/kotlin/app/moviebase/tmdb/core/HttpClientFactory.kt
+ * This function creates an HttpClient with the provided HttpClientEngine.
+ * The HttpClientEngine is injected by the Koin platformModule(), which as its name suggests provides an HttpClientEngine implementation that is platform-dependent.
+ * It configures the client to log all requests and responses if enableNetworkLogs is true. TODO: read this as a build setting
+ * It also sets a default request header with the provided authToken.
+ * @param httpClientEngine The HttpClientEngine to use for the HttpClient.
+ * @param config A block specifying the following options:
+ *  - Whether to log all requests and responses.
+ *  - Service URL to make requests to.
+ *  - Token to set as a default request header.
+ * @return The created HttpClient.
+ */
+internal fun buildHttpClient(
+  httpClientEngine: HttpClientEngine,
+  config: TemClientConfig,
+  logger: KermitLogger,
+): HttpClient {
+  val defaultConfig: HttpClientConfig<*>.() -> Unit = {
+    // Can't use install(ContentNegotiation){ json() } here because YuJa's API returns a 406 error if the Accept header is set to application/json
+    // This requires JSON transformations to be done via manually-called library functions
+
+    // Pass the stored authToken as a header in every request
+    defaultRequest {
+      header("authToken", config.serviceToken)
+      url {
+        protocol = URLProtocol.HTTPS
+      }
+    }
+
+    if (config.optDebugEnabled) {
+      when (config.optHttpClientLoggingBlock) {
+        null -> {
+          install(Logging) {
+            this.logger = object : KtorLogger {
+              override fun log(message: String) {
+                logger.d(message)
+              }
+            }
+            level = ALL
+            sanitizeHeader { headerKey ->
+              headerKey == "authToken"
+            }
+          }
+        }
+        else -> {
+          config.optHttpClientLoggingBlock?.let {
+            Logging(it)
+          }
+        }
+      }
+    }
+
+    expectSuccess = config.optExpectSuccess
+
+    if (config.optUseWebTimeout) {
+      val defaultWebTimeout = DEFAULT_WEB_TIMEOUT.toLong(MILLISECONDS)
+      val configuredWebTimeout = config.optWebTimeoutDuration?.toLong(MILLISECONDS)
+      install(HttpTimeout) {
+        connectTimeoutMillis = configuredWebTimeout ?: defaultWebTimeout
+        requestTimeoutMillis = configuredWebTimeout ?: defaultWebTimeout
+        socketTimeoutMillis = configuredWebTimeout ?: defaultWebTimeout
+      }
+    }
+
+    config.optHttpClientConfigBlock?.invoke(this)
+  }
+  return config.optHttpClientBuilder?.invoke()?.config(defaultConfig) ?: HttpClient(httpClientEngine, defaultConfig)
+}
+
+/**
+ * Create a Temerity instance using Kotlin-DSL.
+ */
+@TemDsl
+public fun Temerity(block: TemClientConfig.() -> Unit): Temerity {
+  //  Adapted from the tmdb-api project: https://github.com/MoviebaseApp/tmdb-kotlin/raw/refs/heads/main/tmdb-api/src/commonMain/kotlin/app/moviebase/tmdb/Tmdb4.kt
+  val config = TemClientConfig().apply(block)
+  return Temerity(config)
+}
+
+/**
+ * Create a Temerity Client Configuration instance using Kotlin-DSL.
+ */
+@TemDsl
+public fun TemClientConfig(block: TemClientConfig.() -> Unit): TemClientConfig = TemClientConfig().apply(block)
diff --git a/temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/di/Koin.kt b/temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/di/Koin.kt
deleted file mode 100644
index 92400c898f7ac651fe51ceb7556436fece7e64e2..0000000000000000000000000000000000000000
--- a/temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/di/Koin.kt
+++ /dev/null
@@ -1,76 +0,0 @@
-/*
- *     Designed and developed in 2022-2024 by William Walker (wnwalker@ucsc.edu)
- *     Copyright 2022-2024 The Regents of the University of California. All rights reserved.
- *
- *     This library is free software; you can redistribute it and/or
- *     modify it under the terms of the GNU Lesser General Public
- *     License as published by the Free Software Foundation; version 2.1 of the License.
- *
- *     This library is distributed in the hope that it will be useful,
- *     but WITHOUT ANY WARRANTY; without even the implied warranty of
- *     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
- *     Lesser General Public License for more details.
- *
- *     You should have received a copy of the GNU Lesser General Public
- *     License along with this library; if not, write to the Free Software
- *     Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
- */
-package edu.ucsc.its.temerity.di
-
-import co.touchlab.kermit.NoTagFormatter
-import co.touchlab.kermit.Severity
-import co.touchlab.kermit.loggerConfigInit
-import co.touchlab.kermit.platformLogWriter
-import com.skydoves.sandwich.ktorfit.ApiResponseConverterFactory
-import de.jensklingenberg.ktorfit.ktorfit
-import edu.ucsc.its.temerity.TemClientConfig
-import edu.ucsc.its.temerity.api.PlatformApi
-import edu.ucsc.its.temerity.api.createPlatformApi
-import edu.ucsc.its.temerity.core.HttpClientFactory.buildHttpClient
-import edu.ucsc.its.temerity.core.TemerityLibrary.koin
-import io.ktor.client.HttpClient
-import org.koin.core.Koin
-import org.koin.core.component.KoinComponent
-import org.koin.core.parameter.parametersOf
-import org.koin.dsl.module
-import co.touchlab.kermit.Logger as KermitLogger
-
-internal fun loggerModule() = module {
-  factory<co.touchlab.kermit.Logger> { tag ->
-    createLogger(tag.getOrNull())
-  }
-}
-
-/**
- * This function provides the Koin module for the Temerity library.
- * It includes the platform module and provides factories for [HttpClient] and [PlatformApi].
- */
-internal val libModule = module {
-  factory { (config: TemClientConfig) ->
-    buildHttpClient(
-      httpClientEngine = get(),
-      config = config,
-    )
-  }
-  factory { (config: TemClientConfig) ->
-    val client: HttpClient = get { parametersOf(config) }
-    val ktorfit = ktorfit {
-      config.serviceUrl?.let { baseUrl(it) }
-      httpClient(client)
-      converterFactories(ApiResponseConverterFactory.create())
-    }
-    ktorfit.createPlatformApi()
-  }
-}
-
-private fun createLogger(tag: String?): KermitLogger = KermitLogger(
-  config = loggerConfigInit(
-    platformLogWriter(NoTagFormatter),
-    minSeverity = Severity.Debug,
-  ),
-  tag = tag ?: "TemerityLib",
-)
-
-internal abstract class TemerityKoinComponent : KoinComponent {
-  override fun getKoin(): Koin = koin
-}
diff --git a/temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/di/LibModule.kt b/temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/di/LibModule.kt
new file mode 100644
index 0000000000000000000000000000000000000000..d6fbe81f556afa013b7acc10362d538977c9b6a3
--- /dev/null
+++ b/temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/di/LibModule.kt
@@ -0,0 +1,75 @@
+/*
+ *     Designed and developed in 2022-2024 by William Walker (wnwalker@ucsc.edu)
+ *     Copyright 2022-2024 The Regents of the University of California. All rights reserved.
+ *
+ *     This library is free software; you can redistribute it and/or
+ *     modify it under the terms of the GNU Lesser General Public
+ *     License as published by the Free Software Foundation; version 2.1 of the License.
+ *
+ *     This library is distributed in the hope that it will be useful,
+ *     but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ *     Lesser General Public License for more details.
+ *
+ *     You should have received a copy of the GNU Lesser General Public
+ *     License along with this library; if not, write to the Free Software
+ *     Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
+ */
+package edu.ucsc.its.temerity.di
+
+import com.skydoves.sandwich.ktorfit.ApiResponseConverterFactory
+import de.jensklingenberg.ktorfit.ktorfit
+import edu.ucsc.its.temerity.TemClientConfig
+import edu.ucsc.its.temerity.api.PlatformApi
+import edu.ucsc.its.temerity.api.createPlatformApi
+import edu.ucsc.its.temerity.core.Temerity.Companion.createLogger
+import edu.ucsc.its.temerity.core.buildHttpClient
+import edu.ucsc.its.temerity.extensions.coroutines.createLibraryScope
+import io.ktor.client.HttpClient
+import io.ktor.client.engine.HttpClientEngine
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import org.koin.core.parameter.parametersOf
+import org.koin.core.qualifier.named
+import org.koin.dsl.module
+import co.touchlab.kermit.Logger as KermitLogger
+
+internal object LibModule {
+  /**
+   * This function provides the Koin module for the Temerity library.
+   * It includes the platform module and provides factories for [HttpClient] and [PlatformApi].
+   */
+  internal fun libModule() = module {
+    single<KermitLogger>(named("libraryLogger")) { (tag: String, config: TemClientConfig) ->
+      createLogger(tag, config)
+    }
+    single<CoroutineDispatcher>(named("libraryCoroutineDispatcher")) { (threadCount: Int, dispatcherName: String) ->
+      Dispatchers.IO.limitedParallelism(threadCount, dispatcherName)
+    }
+    factory<CoroutineDispatcher>(named("childDispatcher")) { (parentDispatcher: CoroutineDispatcher, threadCount: Int, dispatcherName: String) ->
+      parentDispatcher.limitedParallelism(threadCount, dispatcherName)
+    }
+    single<CoroutineScope>(named("libraryCoroutineScope")) { (dispatcher: CoroutineDispatcher) ->
+      createLibraryScope(dispatcher)
+    }
+    factory<HttpClient>(named("httpClient")) { (dispatcher: CoroutineDispatcher, config: TemClientConfig, kermit: co.touchlab.kermit.Logger) ->
+      buildHttpClient(
+        httpClientEngine = get<HttpClientEngine>(named("httpClientEngine")) { parametersOf(dispatcher) },
+        config = config,
+        logger = kermit,
+      )
+    }
+    factory<PlatformApi>(named("ktorfitApi")) { (config: TemClientConfig, webRequestDispatcher: CoroutineDispatcher) ->
+      val logger: KermitLogger = get<KermitLogger>(named("libraryLogger"))
+      val client: HttpClient =
+        get<HttpClient>(named("httpClient")) { parametersOf(webRequestDispatcher, config, logger) }
+      val ktorfit = ktorfit {
+        config.serviceUrl?.let { baseUrl(it) }
+        httpClient(client)
+        converterFactories(ApiResponseConverterFactory.create())
+      }
+      ktorfit.createPlatformApi()
+    }
+  }
+}
diff --git a/temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/Expect.kt b/temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/di/PlatformModule.kt
similarity index 86%
rename from temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/Expect.kt
rename to temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/di/PlatformModule.kt
index 41bdf96fdd09c08c129c55fc1e91f240a0089258..06086bdd73c13f50739d89aafbd5708313d113ee 100644
--- a/temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/Expect.kt
+++ b/temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/di/PlatformModule.kt
@@ -15,13 +15,13 @@
  *     License along with this library; if not, write to the Free Software
  *     Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  */
-package edu.ucsc.its.temerity
+package edu.ucsc.its.temerity.di
 
 import org.koin.core.module.Module
 
 /**
- * This function provides an expectation for the implementation of a platform-specific dependency module
+ * This function provides an implementation of a platform-specific dependency module
  * Implementations for each source set contain factories for injected platform-specific dependencies that the Temerity library relies on.
- * For now, this includes an HttpClientEngine for making requests, and a PreferencesSettings object for storing settings.
+ * For now, this includes an HttpClientEngine for making requests
  */
 internal expect fun platformModule(): Module
diff --git a/temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/extensions/coroutines/CoroutinesExt.kt b/temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/extensions/coroutines/CoroutinesExt.kt
new file mode 100644
index 0000000000000000000000000000000000000000..402405c011efd66aaa337b484d9bae9dd9f6594a
--- /dev/null
+++ b/temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/extensions/coroutines/CoroutinesExt.kt
@@ -0,0 +1,55 @@
+/*
+ *     Designed and developed in 2022-2024 by William Walker (wnwalker@ucsc.edu)
+ *     Copyright 2022-2024 The Regents of the University of California. All rights reserved.
+ *
+ *     This library is free software; you can redistribute it and/or
+ *     modify it under the terms of the GNU Lesser General Public
+ *     License as published by the Free Software Foundation; version 2.1 of the License.
+ *
+ *     This library is distributed in the hope that it will be useful,
+ *     but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ *     Lesser General Public License for more details.
+ *
+ *     You should have received a copy of the GNU Lesser General Public
+ *     License along with this library; if not, write to the Free Software
+ *     Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
+ */
+package edu.ucsc.its.temerity.extensions.coroutines
+
+import edu.ucsc.its.temerity.core.Temerity.Companion.DEFAULT_MINIMUM_THREAD_COUNT
+import io.ktor.util.SilentSupervisor
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.SupervisorJob
+import kotlin.coroutines.CoroutineContext
+
+internal fun createJobScope(coroutineContext: CoroutineContext, allowIndependentFailure: Boolean = false): CoroutineScope {
+  val parentJob = if (allowIndependentFailure) SupervisorJob() else Job()
+  return CoroutineScope(coroutineContext + parentJob)
+}
+
+internal fun createLibraryScope(dispatcher: CoroutineDispatcher): CoroutineScope = CoroutineScope(dispatcher + SilentSupervisor())
+
+internal fun availableThreads(maximumThreadCount: Int? = null): Int {
+  val availableThreadCount = Runtime.getRuntime().availableProcessors().plus(1) // +1 to maintain full utilization in case one coroutine blocks. Never less than 2 (DEFAULT_MINIMUM_THREAD_COUNT). See: https://github.com/Kotlin/kotlinx.coroutines/issues/261
+  with(maximumThreadCount) {
+    return when {
+      this == null -> availableThreadCount
+      this > availableThreadCount -> {
+        // TODO: Log warning that the configured thread count is higher than the number of available threads
+        availableThreadCount
+      }
+      this < DEFAULT_MINIMUM_THREAD_COUNT -> {
+        // TODO: Log warning that the configured thread count is lower than the default minimum thread pool size
+        DEFAULT_MINIMUM_THREAD_COUNT
+      }
+      this < availableThreadCount -> {
+        // TODO: Log warning that the configured thread count is lower than the optimal thread pool size
+        this
+      }
+      else -> availableThreadCount
+    }
+  }
+}
diff --git a/temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/Util.kt b/temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/extensions/datetime/DateTimeExt.kt
similarity index 65%
rename from temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/Util.kt
rename to temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/extensions/datetime/DateTimeExt.kt
index 157f53f1a2dd182e0b04c6f8418679fc667dd3c0..04c295361d81e270b0325b9d5ddfbbc9afd02286 100644
--- a/temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/Util.kt
+++ b/temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/extensions/datetime/DateTimeExt.kt
@@ -15,25 +15,21 @@
  *     License along with this library; if not, write to the Free Software
  *     Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  */
-package edu.ucsc.its.temerity
+package edu.ucsc.its.temerity.extensions.datetime
 
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.Job
 import kotlinx.datetime.Clock
 import kotlinx.datetime.LocalDate
 import kotlinx.datetime.LocalTime
 import kotlinx.datetime.TimeZone
 import kotlinx.datetime.toLocalDateTime
 import kotlinx.datetime.todayIn
-import kotlin.coroutines.CoroutineContext
 
-internal fun currentTz(): TimeZone = TimeZone.currentSystemDefault()
+internal object DateTimeExt {
+  internal fun currentTz(): TimeZone = TimeZone.currentSystemDefault()
 
-internal fun thisInstant() = Clock.System.now()
+  internal fun thisInstant() = Clock.System.now()
 
-internal fun currentDate(): LocalDate = Clock.System.todayIn(currentTz())
+  internal fun currentDate(): LocalDate = Clock.System.todayIn(currentTz())
 
-internal fun currentTime(): LocalTime = thisInstant().toLocalDateTime(currentTz()).time
-
-internal fun createJobScope(coroutineContext: CoroutineContext = Dispatchers.Main): CoroutineScope = CoroutineScope(coroutineContext + Job())
+  internal fun currentTime(): LocalTime = thisInstant().toLocalDateTime(currentTz()).time
+}
diff --git a/temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/extensions/log/FilesystemLogWriter.kt b/temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/extensions/log/FilesystemLogWriter.kt
new file mode 100644
index 0000000000000000000000000000000000000000..baf4c7d887f311954ef2b7cd7fe29e6472e57d47
--- /dev/null
+++ b/temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/extensions/log/FilesystemLogWriter.kt
@@ -0,0 +1,97 @@
+/*
+ *     Designed and developed in 2022-2024 by William Walker (wnwalker@ucsc.edu)
+ *     Copyright 2022-2024 The Regents of the University of California. All rights reserved.
+ *
+ *     This library is free software; you can redistribute it and/or
+ *     modify it under the terms of the GNU Lesser General Public
+ *     License as published by the Free Software Foundation; version 2.1 of the License.
+ *
+ *     This library is distributed in the hope that it will be useful,
+ *     but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ *     Lesser General Public License for more details.
+ *
+ *     You should have received a copy of the GNU Lesser General Public
+ *     License along with this library; if not, write to the Free Software
+ *     Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
+ */
+package edu.ucsc.its.temerity.extensions.log
+
+import co.touchlab.kermit.DefaultFormatter
+import co.touchlab.kermit.LogWriter
+import co.touchlab.kermit.Message
+import co.touchlab.kermit.MessageStringFormatter
+import co.touchlab.kermit.Severity
+import co.touchlab.kermit.Tag
+import kotlinx.io.buffered
+import kotlinx.io.files.FileSystem
+import kotlinx.io.files.Path
+import kotlinx.io.writeString
+
+internal expect fun fileSystem(): FileSystem
+
+internal class FilesystemLogWriter internal constructor(
+  private val logPath: String,
+  private val logRoller: LogRoller? = null,
+  private val formatter: MessageStringFormatter = DefaultFormatter,
+) : LogWriter() {
+
+  // Not called since we are "Context Aware"
+  override fun log(severity: Severity, message: String, tag: String, throwable: Throwable?) {
+    val fileSystem = fileSystem()
+    val kotlinxIoPath = Path(logPath)
+
+    logRoller?.rollLogs(kotlinxIoPath, fileSystem)
+
+    val sink = fileSystem.sink(kotlinxIoPath, append = true).buffered()
+
+    with(sink) {
+      writeString(formatter.formatMessage(severity, Tag(tag), Message(message)))
+      writeString("\n")
+      throwable?.let {
+        writeString(it.stackTraceToString())
+      }
+      flush()
+    }
+  }
+
+  companion object {
+    operator fun invoke(block: Builder.() -> Unit) =
+      with(FilesystemLogWriterBuilder()) {
+        block(this)
+        build()
+      }
+  }
+
+  interface Builder {
+    fun rollLogAtSize(size: Long): Builder
+    fun logPath(path: String): Builder
+    fun build(): FilesystemLogWriter
+  }
+
+  class FilesystemLogWriterBuilder : Builder {
+    private var maxFileSize: Long? = null
+    private var logPath: String? = null
+
+    override fun rollLogAtSize(size: Long): Builder {
+      maxFileSize = size
+      return this
+    }
+
+    override fun logPath(path: String): Builder {
+      logPath = path
+      return this
+    }
+
+    override fun build(): FilesystemLogWriter {
+      if (logPath == null) throw NullPointerException("Invalid / missing log path")
+
+      // Can you resist the urge to Rick-roll the logs?
+      val rick = maxFileSize?.let {
+        FileSizeLogRoller(logPath!!, maxFileSize!!)
+      }
+
+      return FilesystemLogWriter(logPath!!, rick)
+    }
+  }
+}
diff --git a/temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/extensions/log/Kermit.kt b/temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/extensions/log/Kermit.kt
new file mode 100644
index 0000000000000000000000000000000000000000..fe4e986c29965bf157c8cff8708c0cb43ebf8c43
--- /dev/null
+++ b/temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/extensions/log/Kermit.kt
@@ -0,0 +1,87 @@
+/*
+ *     Designed and developed in 2022-2024 by William Walker (wnwalker@ucsc.edu)
+ *     Copyright 2022-2024 The Regents of the University of California. All rights reserved.
+ *
+ *     This library is free software; you can redistribute it and/or
+ *     modify it under the terms of the GNU Lesser General Public
+ *     License as published by the Free Software Foundation; version 2.1 of the License.
+ *
+ *     This library is distributed in the hope that it will be useful,
+ *     but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ *     Lesser General Public License for more details.
+ *
+ *     You should have received a copy of the GNU Lesser General Public
+ *     License along with this library; if not, write to the Free Software
+ *     Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
+ */
+package edu.ucsc.its.temerity.extensions.log
+
+import co.touchlab.kermit.LogWriter
+import co.touchlab.kermit.Logger
+import co.touchlab.kermit.Severity
+import co.touchlab.kermit.StaticConfig
+
+/*
+ * An overload for the Kermit logger that allows for more concise and readable configuration code.
+ * Adapted from: https://github.com/psh/KermitExt/blob/060cd7fab8a73af4187498af8ed169fa4624bf5a/kermit-config/src/commonMain/kotlin/com/gatebuzz/kermit/ext/Kermit.kt
+ */
+internal class Kermit {
+
+  companion object {
+    @JvmStatic
+    fun builder(): Builder = LoggerBuilder()
+
+    operator fun invoke(block: Builder.() -> Unit): Logger = with(LoggerBuilder()) {
+      block(this)
+      build()
+    }
+  }
+
+  interface Builder {
+    fun tag(tag: String): Builder
+    fun minSeverity(severity: Severity): Builder
+    fun setLogWriters(vararg logWriter: LogWriter): Builder
+    fun addLogWriter(vararg logWriter: LogWriter): Builder
+    operator fun LogWriter.unaryPlus()
+    fun build(): Logger
+  }
+
+  internal class LoggerBuilder : Builder {
+    private var tag: String = ""
+    private var logWriters: MutableList<LogWriter> = mutableListOf()
+    private var minSeverity: Severity = Severity.Verbose
+
+    override fun tag(tag: String): Builder {
+      this.tag = tag
+      return this
+    }
+
+    override fun minSeverity(severity: Severity): Builder {
+      this.minSeverity = severity
+      return this
+    }
+
+    override fun setLogWriters(vararg logWriter: LogWriter): Builder {
+      logWriters = logWriter.toMutableList()
+      return this
+    }
+
+    override operator fun LogWriter.unaryPlus() {
+      logWriters.add(this)
+    }
+
+    override fun addLogWriter(vararg logWriter: LogWriter): Builder {
+      logWriters.addAll(logWriter)
+      return this
+    }
+
+    override fun build(): Logger {
+      if (logWriters.isEmpty()) throw Exception("At least one log writer is needed")
+
+      return Logger(
+        StaticConfig(minSeverity, logWriters.toList()),
+      )
+    }
+  }
+}
diff --git a/temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/extensions/log/LogRoller.kt b/temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/extensions/log/LogRoller.kt
new file mode 100644
index 0000000000000000000000000000000000000000..69171fedb96f80f28c3987d4ae125c66336884ab
--- /dev/null
+++ b/temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/extensions/log/LogRoller.kt
@@ -0,0 +1,46 @@
+/*
+ *     Designed and developed in 2022-2024 by William Walker (wnwalker@ucsc.edu)
+ *     Copyright 2022-2024 The Regents of the University of California. All rights reserved.
+ *
+ *     This library is free software; you can redistribute it and/or
+ *     modify it under the terms of the GNU Lesser General Public
+ *     License as published by the Free Software Foundation; version 2.1 of the License.
+ *
+ *     This library is distributed in the hope that it will be useful,
+ *     but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ *     Lesser General Public License for more details.
+ *
+ *     You should have received a copy of the GNU Lesser General Public
+ *     License along with this library; if not, write to the Free Software
+ *     Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
+ */
+package edu.ucsc.its.temerity.extensions.log
+
+import kotlinx.io.files.FileSystem
+import kotlinx.io.files.Path
+
+internal interface LogRoller {
+  fun rollLogs(kotlinxIoPath: Path, fileSystem: FileSystem)
+}
+
+internal class FileSizeLogRoller(
+  private val logPath: String,
+  private val maxFileSize: Long,
+) : LogRoller {
+  override fun rollLogs(kotlinxIoPath: Path, fileSystem: FileSystem) {
+    val metadata = fileSystem.metadataOrNull(kotlinxIoPath)
+    metadata?.let {
+      it.size.let { size ->
+        if (size >= maxFileSize) {
+          val filename = kotlinxIoPath.name
+          val count = fileSystem.list(kotlinxIoPath.parent!!).count { p: Path ->
+            p.name.contains(filename)
+          }
+          val to = Path("$logPath.$count")
+          fileSystem.atomicMove(kotlinxIoPath, to)
+        }
+      }
+    }
+  }
+}
diff --git a/temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/extensions/log/WrappingFormatter.kt b/temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/extensions/log/WrappingFormatter.kt
new file mode 100644
index 0000000000000000000000000000000000000000..26703f81aca62012f948c33e137c017af0a24728
--- /dev/null
+++ b/temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/extensions/log/WrappingFormatter.kt
@@ -0,0 +1,37 @@
+/*
+ *     Designed and developed in 2022-2024 by William Walker (wnwalker@ucsc.edu)
+ *     Copyright 2022-2024 The Regents of the University of California. All rights reserved.
+ *
+ *     This library is free software; you can redistribute it and/or
+ *     modify it under the terms of the GNU Lesser General Public
+ *     License as published by the Free Software Foundation; version 2.1 of the License.
+ *
+ *     This library is distributed in the hope that it will be useful,
+ *     but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ *     Lesser General Public License for more details.
+ *
+ *     You should have received a copy of the GNU Lesser General Public
+ *     License along with this library; if not, write to the Free Software
+ *     Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
+ */
+package edu.ucsc.its.temerity.extensions.log
+
+import co.touchlab.kermit.Message
+import co.touchlab.kermit.MessageStringFormatter
+import co.touchlab.kermit.Severity
+import co.touchlab.kermit.Tag
+
+internal abstract class WrappingFormatter(
+  private val messageStringFormatter: MessageStringFormatter,
+) : MessageStringFormatter {
+  override fun formatMessage(severity: Severity?, tag: Tag?, message: Message): String {
+    val prefix = prefix(severity, tag, message)
+    val content = messageStringFormatter.formatMessage(severity, tag, message)
+    val suffix = suffix(severity, tag, message)
+    return "${prefix}$content$suffix"
+  }
+
+  open fun prefix(severity: Severity?, tag: Tag?, message: Message): String = ""
+  open fun suffix(severity: Severity?, tag: Tag?, message: Message): String = ""
+}
diff --git a/temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/extensions/DateTimeExtensions.kt b/temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/extensions/time/DateTimeExtensions.kt
similarity index 97%
rename from temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/extensions/DateTimeExtensions.kt
rename to temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/extensions/time/DateTimeExtensions.kt
index bfb164a31af64f1e313fb4c280d0e6161b6ff0ba..7ed64322b5d44f0fd76206566d0b49c49e09f028 100644
--- a/temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/extensions/DateTimeExtensions.kt
+++ b/temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/extensions/time/DateTimeExtensions.kt
@@ -15,7 +15,7 @@
  *     License along with this library; if not, write to the Free Software
  *     Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  */
-package edu.ucsc.its.temerity.extensions
+package edu.ucsc.its.temerity.extensions.time
 
 import edu.ucsc.its.temerity.AUDIT_LOG_REQUEST_DATE_FORMAT
 import edu.ucsc.its.temerity.SCHEDULED_SESSION_DATE_FORMAT
diff --git a/temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/extensions/LocalDateTimeSerializer.kt b/temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/extensions/time/LocalDateTimeSerializer.kt
similarity index 97%
rename from temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/extensions/LocalDateTimeSerializer.kt
rename to temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/extensions/time/LocalDateTimeSerializer.kt
index b33286c63652fd4129abecd17b1e13b064aea7af..96695252d3ce72acaa684e88cb9f2a96b62533bb 100644
--- a/temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/extensions/LocalDateTimeSerializer.kt
+++ b/temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/extensions/time/LocalDateTimeSerializer.kt
@@ -15,7 +15,7 @@
  *     License along with this library; if not, write to the Free Software
  *     Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  */
-package edu.ucsc.its.temerity.extensions
+package edu.ucsc.its.temerity.extensions.time
 
 import edu.ucsc.its.temerity.AUDIT_LOG_TIMESTAMP_FORMAT
 import edu.ucsc.its.temerity.model.AuditLogEntry
diff --git a/temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/model/AuditLogs.kt b/temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/model/AuditLogs.kt
index 538b45508ef65fd405ae3a10fbc0583fec87b7a5..ed163d057075a7e1c37d9060733833a6952ace73 100644
--- a/temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/model/AuditLogs.kt
+++ b/temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/model/AuditLogs.kt
@@ -17,7 +17,7 @@
  */
 package edu.ucsc.its.temerity.model
 
-import edu.ucsc.its.temerity.extensions.LocalDateTimeSerializer
+import edu.ucsc.its.temerity.extensions.time.LocalDateTimeSerializer
 import kotlinx.datetime.LocalDateTime
 import kotlinx.serialization.Serializable
 
diff --git a/temerity/src/jvmMain/kotlin/edu/ucsc/its/temerity/core/LoggerFactory.jvm.kt b/temerity/src/jvmMain/kotlin/edu/ucsc/its/temerity/core/LoggerFactory.jvm.kt
new file mode 100644
index 0000000000000000000000000000000000000000..9cb64854d8f7a4c32115b92efa2ad55f5cc893cd
--- /dev/null
+++ b/temerity/src/jvmMain/kotlin/edu/ucsc/its/temerity/core/LoggerFactory.jvm.kt
@@ -0,0 +1,58 @@
+/*
+ *     Designed and developed in 2022-2024 by William Walker (wnwalker@ucsc.edu)
+ *     Copyright 2022-2024 The Regents of the University of California. All rights reserved.
+ *
+ *     This library is free software; you can redistribute it and/or
+ *     modify it under the terms of the GNU Lesser General Public
+ *     License as published by the Free Software Foundation; version 2.1 of the License.
+ *
+ *     This library is distributed in the hope that it will be useful,
+ *     but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ *     Lesser General Public License for more details.
+ *
+ *     You should have received a copy of the GNU Lesser General Public
+ *     License along with this library; if not, write to the Free Software
+ *     Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
+ */
+package edu.ucsc.its.temerity.core
+
+import co.touchlab.kermit.CommonWriter
+import co.touchlab.kermit.Logger
+import co.touchlab.kermit.NoTagFormatter
+import co.touchlab.kermit.Severity.Debug
+import co.touchlab.kermit.loggerConfigInit
+import co.touchlab.kermit.platformLogWriter
+import edu.ucsc.its.temerity.TemClientConfig
+
+internal actual fun createCommonLogger(tag: String?, config: TemClientConfig?, supportKtxNotebook: Boolean): Logger = when (config) {
+  null -> {
+    Logger(
+      config = loggerConfigInit(
+        if (supportKtxNotebook) CommonWriter(NoTagFormatter) else platformLogWriter(NoTagFormatter),
+        minSeverity = Debug,
+      ),
+      tag = tag ?: "TemerityLib",
+    )
+  }
+  else -> {
+    when (config.optDebugEnabled) {
+      // If debug logging is enabled, create a logger which color prints to the console
+      true -> {
+        Logger(
+          config = loggerConfigInit(
+            if (supportKtxNotebook) CommonWriter(NoTagFormatter) else platformLogWriter(NoTagFormatter),
+            minSeverity = Debug,
+          ),
+          tag = tag ?: "TemerityLib",
+        )
+      }
+      // If debug logging is disabled, create a logger which prints to a log file
+      false -> {
+        Logger(
+          config = loggerConfigInit(),
+        )
+      }
+    }
+  }
+}
diff --git a/temerity/src/jvmMain/kotlin/edu/ucsc/its/temerity/di/PlatformModule.jvm.kt b/temerity/src/jvmMain/kotlin/edu/ucsc/its/temerity/di/PlatformModule.jvm.kt
new file mode 100644
index 0000000000000000000000000000000000000000..a9845defa0bd8148131c81826c2f120f22642902
--- /dev/null
+++ b/temerity/src/jvmMain/kotlin/edu/ucsc/its/temerity/di/PlatformModule.jvm.kt
@@ -0,0 +1,31 @@
+/*
+ *     Designed and developed in 2022-2024 by William Walker (wnwalker@ucsc.edu)
+ *     Copyright 2022-2024 The Regents of the University of California. All rights reserved.
+ *
+ *     This library is free software; you can redistribute it and/or
+ *     modify it under the terms of the GNU Lesser General Public
+ *     License as published by the Free Software Foundation; version 2.1 of the License.
+ *
+ *     This library is distributed in the hope that it will be useful,
+ *     but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ *     Lesser General Public License for more details.
+ *
+ *     You should have received a copy of the GNU Lesser General Public
+ *     License along with this library; if not, write to the Free Software
+ *     Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
+ */
+package edu.ucsc.its.temerity.di
+
+import io.ktor.client.engine.java.Java
+import kotlinx.coroutines.CoroutineDispatcher
+import org.koin.core.qualifier.named
+import org.koin.dsl.module
+
+internal actual fun platformModule() = module {
+  factory(named("httpClientEngine")) { (libraryCoroutineDispatcher: CoroutineDispatcher) ->
+    Java.create {
+      dispatcher = libraryCoroutineDispatcher
+    }
+  }
+}
diff --git a/temerity/src/jvmMain/kotlin/edu/ucsc/its/temerity/extensions/log/ColorFormatter.kt b/temerity/src/jvmMain/kotlin/edu/ucsc/its/temerity/extensions/log/ColorFormatter.kt
new file mode 100644
index 0000000000000000000000000000000000000000..d11bb8f44f4a9c7949808ddded9343f09f56e290
--- /dev/null
+++ b/temerity/src/jvmMain/kotlin/edu/ucsc/its/temerity/extensions/log/ColorFormatter.kt
@@ -0,0 +1,65 @@
+/*
+ *     Designed and developed in 2022-2024 by William Walker (wnwalker@ucsc.edu)
+ *     Copyright 2022-2024 The Regents of the University of California. All rights reserved.
+ *
+ *     This library is free software; you can redistribute it and/or
+ *     modify it under the terms of the GNU Lesser General Public
+ *     License as published by the Free Software Foundation; version 2.1 of the License.
+ *
+ *     This library is distributed in the hope that it will be useful,
+ *     but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ *     Lesser General Public License for more details.
+ *
+ *     You should have received a copy of the GNU Lesser General Public
+ *     License along with this library; if not, write to the Free Software
+ *     Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
+ */
+@file:Suppress("unused")
+
+package edu.ucsc.its.temerity.extensions.log
+
+import co.touchlab.kermit.DefaultFormatter
+import co.touchlab.kermit.Message
+import co.touchlab.kermit.MessageStringFormatter
+import co.touchlab.kermit.Severity
+import co.touchlab.kermit.Tag
+import org.fusesource.jansi.Ansi
+
+private fun withColor(messageStringFormatter: MessageStringFormatter = DefaultFormatter): MessageStringFormatter = ColorFormatter(messageStringFormatter)
+
+private fun withBrightColor(messageStringFormatter: MessageStringFormatter = DefaultFormatter): MessageStringFormatter = BrightColorFormatter(messageStringFormatter)
+
+internal class ColorFormatter(
+  messageStringFormatter: MessageStringFormatter = DefaultFormatter,
+) : WrappingFormatter(messageStringFormatter) {
+  override fun prefix(severity: Severity?, tag: Tag?, message: Message) =
+    severity?.toAnsiColor() ?: ""
+
+  override fun suffix(severity: Severity?, tag: Tag?, message: Message) =
+    resetColor()
+}
+
+internal class BrightColorFormatter(
+  messageStringFormatter: MessageStringFormatter = DefaultFormatter,
+) : WrappingFormatter(messageStringFormatter) {
+  override fun prefix(severity: Severity?, tag: Tag?, message: Message) =
+    severity?.toBrightAnsiColor() ?: ""
+
+  override fun suffix(severity: Severity?, tag: Tag?, message: Message) = resetColor()
+}
+
+internal fun Severity.toBrightAnsiColor() = "${Ansi.ansi().fgBright(this.asColor())}"
+
+internal fun Severity.toAnsiColor() = "${Ansi.ansi().fg(this.asColor())}"
+
+internal fun resetColor() = "${Ansi.ansi().a(Ansi.Attribute.RESET)}"
+
+internal fun Severity.asColor(): Ansi.Color = when (this) {
+  Severity.Verbose -> Ansi.Color.WHITE
+  Severity.Debug -> Ansi.Color.CYAN
+  Severity.Info -> Ansi.Color.GREEN
+  Severity.Warn -> Ansi.Color.YELLOW
+  Severity.Error -> Ansi.Color.RED
+  Severity.Assert -> Ansi.Color.MAGENTA
+}
diff --git a/temerity/src/jvmMain/kotlin/edu/ucsc/its/temerity/Actual.kt b/temerity/src/jvmMain/kotlin/edu/ucsc/its/temerity/extensions/log/Filesystem.jvm.kt
similarity index 83%
rename from temerity/src/jvmMain/kotlin/edu/ucsc/its/temerity/Actual.kt
rename to temerity/src/jvmMain/kotlin/edu/ucsc/its/temerity/extensions/log/Filesystem.jvm.kt
index 8574a6a44702540df719f632c6979650a0538f42..094b3d7530deffde589ca3ccadd23ad63130c62f 100644
--- a/temerity/src/jvmMain/kotlin/edu/ucsc/its/temerity/Actual.kt
+++ b/temerity/src/jvmMain/kotlin/edu/ucsc/its/temerity/extensions/log/Filesystem.jvm.kt
@@ -15,11 +15,9 @@
  *     License along with this library; if not, write to the Free Software
  *     Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  */
-package edu.ucsc.its.temerity
+package edu.ucsc.its.temerity.extensions.log
 
-import io.ktor.client.engine.java.Java
-import org.koin.dsl.module
+import kotlinx.io.files.FileSystem
+import kotlinx.io.files.SystemFileSystem
 
-internal actual fun platformModule() = module {
-  factory { Java.create() }
-}
+internal actual fun fileSystem(): FileSystem = SystemFileSystem
diff --git a/temerity/src/jvmMain/kotlin/edu/ucsc/its/temerity/extensions/log/KermitServiceProvider.kt b/temerity/src/jvmMain/kotlin/edu/ucsc/its/temerity/extensions/log/KermitServiceProvider.kt
new file mode 100644
index 0000000000000000000000000000000000000000..2f4e0b8a4687d191296792ed2c4617062eefa755
--- /dev/null
+++ b/temerity/src/jvmMain/kotlin/edu/ucsc/its/temerity/extensions/log/KermitServiceProvider.kt
@@ -0,0 +1,68 @@
+/*
+ *     Designed and developed in 2022-2024 by William Walker (wnwalker@ucsc.edu)
+ *     Copyright 2022-2024 The Regents of the University of California. All rights reserved.
+ *
+ *     This library is free software; you can redistribute it and/or
+ *     modify it under the terms of the GNU Lesser General Public
+ *     License as published by the Free Software Foundation; version 2.1 of the License.
+ *
+ *     This library is distributed in the hope that it will be useful,
+ *     but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ *     Lesser General Public License for more details.
+ *
+ *     You should have received a copy of the GNU Lesser General Public
+ *     License along with this library; if not, write to the Free Software
+ *     Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
+ */
+@file:Suppress("unused")
+
+package edu.ucsc.its.temerity.extensions.log
+
+import co.touchlab.kermit.CommonWriter
+import co.touchlab.kermit.LogWriter
+import co.touchlab.kermit.Severity
+import co.touchlab.kermit.StaticConfig
+import org.slf4j.ILoggerFactory
+import org.slf4j.helpers.BasicMarkerFactory
+import org.slf4j.helpers.NOPMDCAdapter
+
+internal class KermitServiceProvider : org.slf4j.spi.SLF4JServiceProvider {
+  private val markerFactory = BasicMarkerFactory()
+  private val mdcAdapter = NOPMDCAdapter()
+  override fun getLoggerFactory() = ILoggerFactory {
+    Slf4jKermitLogger(it, config)
+  }
+
+  override fun getMarkerFactory() = markerFactory
+
+  override fun getMDCAdapter() = mdcAdapter
+
+  override fun getRequestedApiVersion() = "2.0.99"
+
+  override fun initialize() = Unit
+
+  companion object {
+    private val writers = mutableListOf<LogWriter>().apply {
+      add(CommonWriter())
+    }
+    var config: StaticConfig = StaticConfig(logWriterList = writers)
+
+    var minSeverity: Severity
+      get() = config.minSeverity
+      set(value) {
+        config = config.copy(minSeverity = value)
+      }
+
+    fun addWriter(writer: LogWriter) {
+      writers.add(writer)
+      config = config.copy(logWriterList = writers)
+    }
+
+    fun setWriters(vararg writer: LogWriter) {
+      writers.clear()
+      writers.addAll(writer)
+      config = config.copy(logWriterList = writers)
+    }
+  }
+}
diff --git a/temerity/src/jvmMain/kotlin/edu/ucsc/its/temerity/extensions/log/Slf4jKermitLogger.kt b/temerity/src/jvmMain/kotlin/edu/ucsc/its/temerity/extensions/log/Slf4jKermitLogger.kt
new file mode 100644
index 0000000000000000000000000000000000000000..d5ea3eb24c621c3610145a9519817e34535d526a
--- /dev/null
+++ b/temerity/src/jvmMain/kotlin/edu/ucsc/its/temerity/extensions/log/Slf4jKermitLogger.kt
@@ -0,0 +1,78 @@
+/*
+ *     Designed and developed in 2022-2024 by William Walker (wnwalker@ucsc.edu)
+ *     Copyright 2022-2024 The Regents of the University of California. All rights reserved.
+ *
+ *     This library is free software; you can redistribute it and/or
+ *     modify it under the terms of the GNU Lesser General Public
+ *     License as published by the Free Software Foundation; version 2.1 of the License.
+ *
+ *     This library is distributed in the hope that it will be useful,
+ *     but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ *     Lesser General Public License for more details.
+ *
+ *     You should have received a copy of the GNU Lesser General Public
+ *     License along with this library; if not, write to the Free Software
+ *     Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
+ */
+@file:Suppress("unused")
+
+package edu.ucsc.its.temerity.extensions.log
+
+import co.touchlab.kermit.BaseLogger
+import co.touchlab.kermit.LoggerConfig
+import co.touchlab.kermit.Severity
+import org.slf4j.Marker
+import org.slf4j.event.Level
+import org.slf4j.helpers.AbstractLogger
+
+internal class Slf4jKermitLogger(private val name: String, config: LoggerConfig) : AbstractLogger() {
+  private val logger = BaseLogger(config)
+  override fun getName(): String = "slf4j-over-kermit"
+
+  //region Is Logging enabled at various levels
+  override fun isTraceEnabled() = logger.config.minSeverity <= Severity.Verbose
+  override fun isTraceEnabled(marker: Marker?) = logger.config.minSeverity <= Severity.Verbose
+  override fun isDebugEnabled() = logger.config.minSeverity <= Severity.Debug
+  override fun isDebugEnabled(marker: Marker?) = logger.config.minSeverity <= Severity.Debug
+  override fun isInfoEnabled() = logger.config.minSeverity <= Severity.Info
+  override fun isInfoEnabled(marker: Marker?) = logger.config.minSeverity <= Severity.Info
+  override fun isWarnEnabled() = logger.config.minSeverity <= Severity.Warn
+  override fun isWarnEnabled(marker: Marker?) = logger.config.minSeverity <= Severity.Warn
+  override fun isErrorEnabled() = logger.config.minSeverity <= Severity.Error
+  override fun isErrorEnabled(marker: Marker?) = logger.config.minSeverity <= Severity.Error
+  //endregion
+
+  override fun getFullyQualifiedCallerName(): String? = null
+
+  override fun handleNormalizedLoggingCall(
+    level: Level?,
+    marker: Marker?,
+    messagePattern: String?,
+    arguments: Array<out Any>?,
+    throwable: Throwable?,
+  ) {
+    val severity = when (level) {
+      Level.ERROR -> Severity.Error
+      Level.WARN -> Severity.Warn
+      Level.INFO -> Severity.Info
+      Level.DEBUG -> Severity.Debug
+      else -> Severity.Verbose
+    }
+
+    val formatted = if (messagePattern != null && arguments != null) {
+      String.format(messagePattern, *(arguments.toList().toTypedArray()))
+    } else {
+      null
+    }
+
+    messagePattern.let {
+      logger.log(
+        severity,
+        marker?.toString() ?: name,
+        throwable,
+        formatted ?: (messagePattern ?: ""),
+      )
+    }
+  }
+}
diff --git a/temerity/src/jvmTest/kotlin/edu/ucsc/its/temerity/test/DevDeviceApiTests.kt b/temerity/src/jvmTest/kotlin/edu/ucsc/its/temerity/test/DevDeviceApiTests.kt
index a629715af9f553b9f07284e8bcc20299c40b58a0..058672e8279d74875a658994c3975424e65c6dbb 100644
--- a/temerity/src/jvmTest/kotlin/edu/ucsc/its/temerity/test/DevDeviceApiTests.kt
+++ b/temerity/src/jvmTest/kotlin/edu/ucsc/its/temerity/test/DevDeviceApiTests.kt
@@ -20,11 +20,10 @@ package edu.ucsc.its.temerity.test
 import edu.ucsc.its.temerity.core.Temerity
 import io.kotest.core.spec.style.FunSpec
 import kotlinx.coroutines.runBlocking
-import org.dotenv.vault.dotenvVault
 
 class DevDeviceApiTests :
   FunSpec({
-    val dotenv = dotenvVault()
+    val dotenv = dotenvVaultJvm()
 
     lateinit var temerityTest: Temerity
 
diff --git a/temerity/src/jvmTest/kotlin/edu/ucsc/its/temerity/test/DevGroupApiTests.kt b/temerity/src/jvmTest/kotlin/edu/ucsc/its/temerity/test/DevGroupApiTests.kt
index 7457d64fed42fe280fe04e6f2764a30733fee869..3658f2465eaef2f5dc443af31816e9b5143d2296 100644
--- a/temerity/src/jvmTest/kotlin/edu/ucsc/its/temerity/test/DevGroupApiTests.kt
+++ b/temerity/src/jvmTest/kotlin/edu/ucsc/its/temerity/test/DevGroupApiTests.kt
@@ -20,12 +20,11 @@ package edu.ucsc.its.temerity.test
 import edu.ucsc.its.temerity.core.Temerity
 import io.kotest.core.spec.style.FunSpec
 import kotlinx.coroutines.runBlocking
-import org.dotenv.vault.dotenvVault
 
 class DevGroupApiTests :
   FunSpec({
 
-    val dotenv = dotenvVault()
+    val dotenv = dotenvVaultJvm()
 
     lateinit var testTemerity: Temerity
 
diff --git a/temerity/src/jvmTest/kotlin/edu/ucsc/its/temerity/test/DevUserApiTests.kt b/temerity/src/jvmTest/kotlin/edu/ucsc/its/temerity/test/DevUserApiTests.kt
index b40a4a98607b600e61a43092f05d73aeb75588d6..57ac043bbcff13f64448a341ebe7edf68855b45a 100644
--- a/temerity/src/jvmTest/kotlin/edu/ucsc/its/temerity/test/DevUserApiTests.kt
+++ b/temerity/src/jvmTest/kotlin/edu/ucsc/its/temerity/test/DevUserApiTests.kt
@@ -21,18 +21,19 @@ import co.touchlab.kermit.Logger
 import com.skydoves.sandwich.StatusCode
 import com.skydoves.sandwich.ktor.getStatusCode
 import edu.ucsc.its.temerity.core.Temerity
+import edu.ucsc.its.temerity.core.Temerity.Companion.createLogger
 import edu.ucsc.its.temerity.model.NewUser
 import edu.ucsc.its.temerity.model.UserUpdate
 import io.kotest.core.spec.style.FunSpec
 import io.kotest.matchers.shouldBe
 import io.ktor.http.HttpStatusCode
 import kotlinx.coroutines.runBlocking
-import org.dotenv.vault.dotenvVault
 
 class DevUserApiTests :
   FunSpec({
 
-    val dotenv = dotenvVault()
+    val dotenv = dotenvVaultJvm()
+    val kermit = createLogger(tag = "DevUserApiTests")
 
     lateinit var testTemerity: Temerity
     beforeTest {
@@ -45,7 +46,7 @@ class DevUserApiTests :
       runBlocking {
         val returnedUsers = testTemerity.getUsers()
         println("Returned users:")
-        returnedUsers.forEach { Logger.d(it.toString()) }
+        returnedUsers.forEach { kermit.d(it.toString()) }
         assert(returnedUsers.isNotEmpty())
       }
     }
diff --git a/temerity/src/jvmTest/kotlin/edu/ucsc/its/temerity/test/DevUtilityTests.kt b/temerity/src/jvmTest/kotlin/edu/ucsc/its/temerity/test/DevUtilityTests.kt
index 565875c2a00f42a90a583552d0ab15828d8fb796..19a62a3a5572b022f7f77feddacdf38692e2b5a7 100644
--- a/temerity/src/jvmTest/kotlin/edu/ucsc/its/temerity/test/DevUtilityTests.kt
+++ b/temerity/src/jvmTest/kotlin/edu/ucsc/its/temerity/test/DevUtilityTests.kt
@@ -17,51 +17,47 @@
  */
 package edu.ucsc.its.temerity.test
 
-import co.touchlab.kermit.Logger
 import edu.ucsc.its.temerity.core.Temerity
-import edu.ucsc.its.temerity.di.loggerModule
-import io.kotest.core.extensions.Extension
+import edu.ucsc.its.temerity.core.Temerity.Companion.createLogger
 import io.kotest.core.spec.style.FunSpec
-import kotlinx.datetime.Clock
-import kotlinx.datetime.TimeZone
-import kotlinx.datetime.todayIn
-import org.dotenv.vault.dotenvVault
-import org.koin.core.parameter.parametersOf
+import io.kotest.engine.runBlocking
 import org.koin.test.KoinTest
-import org.koin.test.inject
+import kotlin.time.Duration.Companion.hours
 import kotlin.time.Duration.Companion.minutes
 
 class DevUtilityTests :
   FunSpec(),
   KoinTest {
 
-  override fun extensions(): List<Extension> = listOf(koinExtension(loggerModule()))
-  private val kermit: Logger by inject { parametersOf("DevUtilityTests") }
+  private val kermit = createLogger("DevUtilityTests")
 
   init {
     coroutineDebugProbes = true
 
-    val dotenv = dotenvVault()
+    val dotenv = dotenvVaultJvm()
 
     lateinit var testTemerity: Temerity
-    val today = Clock.System.todayIn(TimeZone.currentSystemDefault())
 
     beforeTest {
       testTemerity = Temerity {
         configureDevEnvironment(dotenv)
       }
     }
-
     timeout = 5.minutes.inWholeMilliseconds
 
-    test("Delete all Canvas Test Student Users") {
-      val testStudentList = testTemerity.getUsers().filter {
-        it.firstName.startsWith("test", ignoreCase = true) &&
-          it.lastName.startsWith("student", ignoreCase = true) &&
-          it.emailAddress.isEmpty()
-      }
-      testStudentList.forEach {
-        testTemerity.deleteUser(it.userId)
+    test("Delete all Canvas Test Student Users").config(blockingTest = true, timeout = 12.hours) {
+      runBlocking {
+        var deletedUsers = 0
+        val testStudentList = testTemerity.getUsers().filter {
+          it.firstName.startsWith("test", ignoreCase = true) &&
+            it.lastName.startsWith("student", ignoreCase = true) &&
+            it.emailAddress.isEmpty()
+        }
+        testStudentList.forEach {
+          testTemerity.deleteUser(it.userId)
+          deletedUsers++
+        }
+        kermit.d("Deleted $deletedUsers test student users.")
       }
     }
   }
diff --git a/temerity/src/jvmTest/kotlin/edu/ucsc/its/temerity/test/ProdReportTests.kt b/temerity/src/jvmTest/kotlin/edu/ucsc/its/temerity/test/ProdReportTests.kt
index 7dd2e2af56f74835c7629cb7da6faa6e0a9cf462..5ce6590495267cbee21e8aa098b035297fe23dbd 100644
--- a/temerity/src/jvmTest/kotlin/edu/ucsc/its/temerity/test/ProdReportTests.kt
+++ b/temerity/src/jvmTest/kotlin/edu/ucsc/its/temerity/test/ProdReportTests.kt
@@ -16,10 +16,11 @@
  *     Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  */
 package edu.ucsc.its.temerity.test
+
 import edu.ucsc.its.temerity.AuditLogSortOrder.NEW_FIRST
 import edu.ucsc.its.temerity.core.Temerity
-import edu.ucsc.its.temerity.currentDate
-import edu.ucsc.its.temerity.di.loggerModule
+import edu.ucsc.its.temerity.core.Temerity.Companion.createLogger
+import edu.ucsc.its.temerity.core.Temerity.Companion.currentDate
 import edu.ucsc.its.temerity.model.EventType.AUTOMATED_SESSION_FAILED_TO_START
 import edu.ucsc.its.temerity.model.EventType.AUTOMATED_SESSION_MONITOR
 import edu.ucsc.its.temerity.model.EventType.CAPTURE_ERROR
@@ -31,7 +32,6 @@ import edu.ucsc.its.temerity.model.EventType.HDCP_SIGNAL_DETECTED
 import edu.ucsc.its.temerity.model.EventType.HUB_CAPTURE_PROBLEM
 import edu.ucsc.its.temerity.model.EventType.NEW_LOG_IN
 import edu.ucsc.its.temerity.model.EventType.RECORDING_ERROR
-import io.kotest.core.extensions.Extension
 import io.kotest.core.spec.style.FunSpec
 import io.kotest.matchers.file.shouldNotBeEmpty
 import kotlinx.coroutines.runBlocking
@@ -40,12 +40,9 @@ import kotlinx.datetime.DateTimeUnit
 import kotlinx.datetime.TimeZone
 import kotlinx.datetime.minus
 import kotlinx.datetime.todayIn
-import org.dotenv.vault.dotenvVault
 import org.jetbrains.kotlinx.dataframe.api.toDataFrame
 import org.jetbrains.kotlinx.dataframe.io.writeCSV
-import org.koin.core.parameter.parametersOf
 import org.koin.test.KoinTest
-import org.koin.test.inject
 import java.io.File
 import kotlin.time.Duration.Companion.minutes
 import kotlin.uuid.ExperimentalUuidApi
@@ -57,16 +54,15 @@ class ProdReportTests :
   FunSpec(),
   KoinTest {
 
-  override fun extensions(): List<Extension> = listOf(koinExtension(loggerModule()))
-  private val kermit: KermitLogger by inject { parametersOf("ProdReportTests") }
+  private val kermit: KermitLogger = createLogger(tag = "TemerityDevTest")
 
   init {
     coroutineDebugProbes = true
 
-    val dotenv = dotenvVault()
+    val dotenv = dotenvVaultJvm()
 
     lateinit var testTemerity: Temerity
-    val today = Clock.System.todayIn(TimeZone.currentSystemDefault())
+    val today = currentDate()
 
     beforeTest {
       testTemerity = Temerity {
diff --git a/temerity/src/jvmTest/kotlin/edu/ucsc/its/temerity/test/ProdUtilityTests.kt b/temerity/src/jvmTest/kotlin/edu/ucsc/its/temerity/test/ProdUtilityTests.kt
index 0f6deec359cb1b16f0d1208c285fdb22af50d7b6..6e43ac6f5bd8bb39733c6c4f378454d9876b6c16 100644
--- a/temerity/src/jvmTest/kotlin/edu/ucsc/its/temerity/test/ProdUtilityTests.kt
+++ b/temerity/src/jvmTest/kotlin/edu/ucsc/its/temerity/test/ProdUtilityTests.kt
@@ -17,34 +17,29 @@
  */
 package edu.ucsc.its.temerity.test
 
-import co.touchlab.kermit.Logger
 import edu.ucsc.its.temerity.core.Temerity
-import edu.ucsc.its.temerity.di.loggerModule
-import io.kotest.core.extensions.Extension
+import edu.ucsc.its.temerity.core.Temerity.Companion.createLogger
+import io.kotest.common.ExperimentalKotest
 import io.kotest.core.spec.style.FunSpec
-import kotlinx.datetime.Clock
-import kotlinx.datetime.TimeZone
-import kotlinx.datetime.todayIn
-import org.dotenv.vault.dotenvVault
-import org.koin.core.parameter.parametersOf
+import io.kotest.engine.runBlocking
 import org.koin.test.KoinTest
-import org.koin.test.inject
+import kotlin.time.Duration.Companion.hours
 import kotlin.time.Duration.Companion.minutes
 
 class ProdUtilityTests :
   FunSpec(),
   KoinTest {
 
-  override fun extensions(): List<Extension> = listOf(koinExtension(loggerModule()))
-  private val kermit: Logger by inject { parametersOf("ProdUtilityTests") }
+  private val kermit = createLogger("ProdUtilityTests")
 
   init {
     coroutineDebugProbes = true
+    @OptIn(ExperimentalKotest::class)
+    blockingTest = true
 
-    val dotenv = dotenvVault()
+    val dotenv = dotenvVaultJvm()
 
     lateinit var testTemerity: Temerity
-    val today = Clock.System.todayIn(TimeZone.currentSystemDefault())
 
     beforeTest {
       testTemerity = Temerity {
@@ -54,14 +49,19 @@ class ProdUtilityTests :
 
     timeout = 5.minutes.inWholeMilliseconds
 
-    test("Delete all Canvas Test Student Users") {
-      val testStudentList = testTemerity.getUsers().filter {
-        it.firstName.startsWith("test", ignoreCase = true) &&
-          it.lastName.startsWith("student", ignoreCase = true) &&
-          it.emailAddress.isEmpty()
-      }
-      testStudentList.forEach {
-        testTemerity.deleteUser(it.userId)
+    test("Delete all Canvas Test Student Users").config(blockingTest = true, timeout = 12.hours) {
+      runBlocking {
+        var deletedUsers = 0
+        val testStudentList = testTemerity.getUsers().filter {
+          it.firstName.startsWith("test", ignoreCase = true) &&
+            it.lastName.startsWith("student", ignoreCase = true) &&
+            it.emailAddress.isEmpty()
+        }
+        testStudentList.forEach {
+          testTemerity.deleteUser(it.userId)
+          deletedUsers++
+        }
+        kermit.d("Deleted $deletedUsers test student users.")
       }
     }
   }
diff --git a/temerity/src/jvmTest/kotlin/edu/ucsc/its/temerity/test/TemerityDevTest.kt b/temerity/src/jvmTest/kotlin/edu/ucsc/its/temerity/test/TemerityDevTest.kt
index 3ba2506dab26aff2d4818a16c9a7971953896a6e..51fd825ba227df0925ac95ba04aa83696f9dbf26 100644
--- a/temerity/src/jvmTest/kotlin/edu/ucsc/its/temerity/test/TemerityDevTest.kt
+++ b/temerity/src/jvmTest/kotlin/edu/ucsc/its/temerity/test/TemerityDevTest.kt
@@ -22,32 +22,32 @@ import com.skydoves.sandwich.StatusCode
 import com.skydoves.sandwich.ktor.getStatusCode
 import edu.ucsc.its.temerity.AuditLogSortOrder.NEW_FIRST
 import edu.ucsc.its.temerity.core.Temerity
-import edu.ucsc.its.temerity.currentDate
+import edu.ucsc.its.temerity.core.Temerity.Companion.currentDate
 import edu.ucsc.its.temerity.model.EventType.NEW_LOG_IN
 import edu.ucsc.its.temerity.model.NewUser
+import io.github.z4kn4fein.semver.Version
+import io.github.z4kn4fein.semver.toVersion
 import io.kotest.core.spec.style.FunSpec
 import io.kotest.matchers.file.shouldNotBeEmpty
 import io.kotest.matchers.shouldBe
 import kotlinx.coroutines.runBlocking
-import kotlinx.datetime.Clock
 import kotlinx.datetime.DateTimeUnit
-import kotlinx.datetime.TimeZone
 import kotlinx.datetime.minus
 import kotlinx.datetime.plus
-import kotlinx.datetime.toLocalDateTime
-import kotlinx.datetime.todayIn
-import org.dotenv.vault.dotenvVault
 import org.jetbrains.kotlinx.dataframe.api.toDataFrame
 import org.jetbrains.kotlinx.dataframe.io.writeCSV
 import java.io.File
+import kotlin.test.assertNotEquals
 import kotlin.uuid.ExperimentalUuidApi
 import kotlin.uuid.Uuid
 
 @OptIn(ExperimentalUuidApi::class)
-class TemerityDevTest :
-  FunSpec({
+class TemerityDevTest : FunSpec() {
 
-    val dotenv = dotenvVault()
+  private lateinit var kermit: Logger
+
+  init {
+    val dotenv = dotenvVaultJvm()
 
     lateinit var testTemerity: Temerity
 
@@ -55,11 +55,30 @@ class TemerityDevTest :
       testTemerity = Temerity {
         configureDevEnvironment(dotenv)
       }
+      kermit = Temerity.createLogger("TemerityDevTest")
+    }
+
+    test("Temerity client returns a correctly-formatted version String") {
+      val returnedVersion = testTemerity.version.toVersion()
+      kermit.d { "Returned client version: $returnedVersion" }
+      assert(testTemerity.version.isNotEmpty())
+      assert(returnedVersion < Version(0, 1, 0))
+      assert(returnedVersion.isPreRelease)
+    }
+
+    test("Temerity client returns different Koin Application instances on each creation") {
+      val firstInstance = Temerity {
+        configureDevEnvironment(dotenv)
+      }
+      val secondInstance = Temerity {
+        configureDevEnvironment(dotenv)
+      }
+      assertNotEquals(firstInstance.koinContext, secondInstance.koinContext)
     }
 
     test("Get all audit log entries of type \"New Login\" from the past month") {
       runBlocking {
-        val currentDate = Clock.System.todayIn(TimeZone.currentSystemDefault())
+        val currentDate = currentDate()
         val pastDate = currentDate.minus(1, DateTimeUnit.MONTH)
 
         val returnedAuditLogEntries = testTemerity.getAuditLogEntries(
@@ -75,7 +94,7 @@ class TemerityDevTest :
     }
     test("Get all audit logs from the past month") {
       runBlocking {
-        val currentDate = Clock.System.todayIn(TimeZone.currentSystemDefault())
+        val currentDate = currentDate()
         val pastDate = currentDate.minus(1, DateTimeUnit.MONTH)
         val logs = testTemerity.getAuditLogEntries(pastDate, currentDate, sortOrder = NEW_FIRST)
         println("Returned events from the past month:")
@@ -85,7 +104,7 @@ class TemerityDevTest :
     }
     test("Get all audit logs from the past day") {
       runBlocking {
-        val currentDate = Clock.System.todayIn(TimeZone.currentSystemDefault())
+        val currentDate = currentDate()
         val pastDate = currentDate.minus(1, DateTimeUnit.DAY)
         val logs = testTemerity.getAuditLogEntries(pastDate, currentDate, sortOrder = NEW_FIRST)
         Logger.d("Returned events from the past day:")
@@ -95,7 +114,7 @@ class TemerityDevTest :
     }
     test("Get all recording sessions belonging to a single user scheduled for the next week") {
       runBlocking {
-        val pastDate = Clock.System.todayIn(TimeZone.currentSystemDefault()).minus(1, DateTimeUnit.DAY)
+        val pastDate = currentDate().minus(1, DateTimeUnit.DAY)
         val futureDate = pastDate.plus(1, DateTimeUnit.MONTH)
         val sessions = testTemerity.getUserSessions("dotenv[YUJADEV_TEST_USER]", pastDate, futureDate)
         Logger.d("Returned sessions for the next week:")
@@ -122,7 +141,7 @@ class TemerityDevTest :
 
     test("Get all recording sessions on a single device scheduled for the next week") {
       runBlocking {
-        val pastDate = Clock.System.todayIn(TimeZone.currentSystemDefault()).minus(1, DateTimeUnit.DAY)
+        val pastDate = currentDate().minus(1, DateTimeUnit.DAY)
         val futureDate = pastDate.plus(1, DateTimeUnit.MONTH)
         val testDeviceId = dotenv["YUJADEV_TEST_DEVICE_ID"].toLongOrNull()
         val sessions = testTemerity.getDeviceSchedule(testDeviceId!!, pastDate, futureDate)
@@ -133,17 +152,10 @@ class TemerityDevTest :
       }
     }
 
-    test("test datetime tostring formatting") {
-      runBlocking {
-        val time = Clock.System.now().toLocalDateTime(TimeZone.currentSystemDefault())
-        println("Current time: $time")
-      }
-    }
-
     test("Get all audit log entries of type \"New Login\" from the past day") {
       runBlocking {
         val testEventType = NEW_LOG_IN
-        val currentDate = Clock.System.todayIn(TimeZone.currentSystemDefault())
+        val currentDate = currentDate()
         val pastDate = currentDate.minus(1, DateTimeUnit.DAY)
 
         val returnedAuditLogEntries = testTemerity.getAuditLogEntries(
@@ -188,4 +200,5 @@ class TemerityDevTest :
         Logger.d(response.toString())
       }
     }
-  })
+  }
+}
diff --git a/temerity/src/jvmTest/kotlin/edu/ucsc/its/temerity/test/Util.kt b/temerity/src/jvmTest/kotlin/edu/ucsc/its/temerity/test/Util.kt
index 5952412206b4a0b89719df0efa9dd5ac1c40d52a..1f0e87313894ddac9d6c2ceb8b4a9bad9e8166cc 100644
--- a/temerity/src/jvmTest/kotlin/edu/ucsc/its/temerity/test/Util.kt
+++ b/temerity/src/jvmTest/kotlin/edu/ucsc/its/temerity/test/Util.kt
@@ -18,7 +18,9 @@
 package edu.ucsc.its.temerity.test
 
 import edu.ucsc.its.temerity.TemClientConfig
+import io.github.cdimascio.dotenv.Configuration
 import io.github.cdimascio.dotenv.Dotenv
+import io.github.cdimascio.dotenv.dotenv
 import io.kotest.core.extensions.TestCaseExtension
 import io.kotest.core.test.TestCase
 import io.kotest.core.test.TestResult
@@ -85,3 +87,10 @@ internal fun TemClientConfig.configureProdEnvironment(dotenv: Dotenv) {
   serviceToken = dotenv["YUJAPROD_TOKEN"]
   optDebugEnabled = true
 }
+
+fun dotenvVaultJvm(block: (Configuration.() -> Unit)? = null): Dotenv = when (block) {
+  null -> dotenv()
+  else -> dotenv {
+    block()
+  }
+}