diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index ed668f579e5ba0c6963d6a42b41995bfda385153..e2ffce6de3bf17c34bf3e1f83585d47d4272fc65 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -1,5 +1,5 @@
 image:
-  name: wnwalker/android-gradle-build-image:34.0.0-jdk17-gradle8.7
+  name: wnwalker/temerity-build-image@sha256:b1ea20cd261395671a541996f7e0cc2437ecf95a813b6fcc7581b41cde167fbf
 
 variables:
   ORG_GRADLE_PROJECT_signingKey: $SIGNING_KEY
diff --git a/.sdkmanrc b/.sdkmanrc
index 955c2580880005a57da000c2c867ea6a706864bd..dc041b2d9f45321aaea04f45e17d92088480ebb6 100644
--- a/.sdkmanrc
+++ b/.sdkmanrc
@@ -1,3 +1,3 @@
 # Enable auto-env through the sdkman_auto_env config
 # Add key=value pairs of SDKs to use below
-java=17.0.12-tem
\ No newline at end of file
+java=17.0.14-tem
\ No newline at end of file
diff --git a/build-image/temerity-build-image.dockerfile b/build-image/temerity-build-image.dockerfile
index 4363c9d3691899d12946807ecec2f9c50045766a..37e7f9d3d47a5989eddd471b2fdbff00c2d03cd9 100644
--- a/build-image/temerity-build-image.dockerfile
+++ b/build-image/temerity-build-image.dockerfile
@@ -1,10 +1,13 @@
 # Based on code available at https://github.com/MobileDevOps/android-sdk-image/blob/9cab35c5d0433ff08cc34f14b82f0f417464bc8e/Dockerfile
+# To build and publish using experimental Docker buildx: 
+# docker buildx build --platform linux/amd64 --push -t <docker_hub_username>/<image_name>:<tag> . --file temerity-build-image.dockerfile
+
 # To build:
 # docker build -t <docker_hub_username>/<image_name>:<tag> . --file temerity-build-image.dockerfile
 # To publish:
 # docker push <docker_hub_username>/<image_name>:<tag>
 
-FROM --platform=linux/amd64 ubuntu:24.10
+FROM bitnami/minideb:bookworm AS base
 
 LABEL maintainer="wnwalker"
 
@@ -12,8 +15,8 @@ LABEL maintainer="wnwalker"
 # https://developer.android.com/studio/index.html
 ENV ANDROID_SDK_TOOLS_VERSION=11076708
 ENV ANDROID_SDK_TOOLS_CHECKSUM=2d2d50857e4eb553af5a6dc3ad507a17adf43d115264b1afc116f95c92e5e258
-ENV GRADLE_VERSION=8.7
-ENV JAVA_VERSION=17.0.12-tem
+ENV GRADLE_VERSION=8.11
+ENV JAVA_VERSION=17.0.14-tem
 ENV ANDROID_HOME=/opt/android-sdk-linux
 ENV ANDROID_SDK_ROOT=$ANDROID_HOME
 ENV PATH=$PATH:$ANDROID_HOME/cmdline-tools:$ANDROID_HOME/cmdline-tools/bin:$ANDROID_HOME/platform-tools
@@ -42,7 +45,7 @@ RUN apt-get -qq update \
     git > /dev/null \
     && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
 
-ENV HOME=/home/agbi
+ENV HOME=/home/tbi
 WORKDIR $HOME/app
 
 # Install SDKMAN
@@ -50,8 +53,10 @@ RUN curl -s "https://get.sdkman.io" | bash
 SHELL ["/bin/bash", "-c"]
 
 # Set the environment variable for sdkman to auto-answer during installation
-RUN echo "sdkman_auto_answer=true" > "$HOME/.sdkman/etc/config"
-RUN echo "sdkman_auto_env=true" > "$HOME/.sdkman/etc/config"
+RUN mkdir -p "$HOME/.sdkman/etc/"
+RUN touch "$HOME/.sdkman/etc/config"
+RUN echo "sdkman_auto_answer=true" >> "$HOME/.sdkman/etc/config"
+RUN echo "sdkman_auto_env=true" >> "$HOME/.sdkman/etc/config"
 
 # Install Gradle, Java
 RUN source "${HOME}/.sdkman/bin/sdkman-init.sh" \
diff --git a/build-logic/build.gradle.kts b/build-logic/build.gradle.kts
index a7029afc90f32e9216bd7913df3fbb256adfe40a..97212ad8a534c567ec9dae79bd96a7eeffd55960 100644
--- a/build-logic/build.gradle.kts
+++ b/build-logic/build.gradle.kts
@@ -1,6 +1,24 @@
+/*
+ *     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
+ */
 plugins {
     `kotlin-dsl`
     `kotlin-dsl-precompiled-script-plugins`
+    alias(libs.plugins.spotless)
 }
 
 dependencies {
@@ -9,6 +27,55 @@ dependencies {
     implementation(libs.build.gitSemVer)
 }
 
+kotlin {
+    compilerOptions {
+        jvmToolchain(libs.versions.jvm.target.get().toInt())
+    }
+}
+
+gradlePlugin {
+    plugins {
+        register("formatting-convention") {
+            id = "temerity.formatting-convention"
+            implementationClass = "edu.ucsc.its.temerity.buildlogic.convention.FormattingConventionPlugin"
+        }
+    }
+    plugins {
+        register("publication-convention") {
+            id = "temerity.publication-convention"
+            implementationClass = "edu.ucsc.its.temerity.buildlogic.convention.PublicationConventionPlugin"
+        }
+    }
+    plugins {
+        register("version-convention") {
+            id = "temerity.version-convention"
+            implementationClass = "edu.ucsc.its.temerity.buildlogic.convention.VersionConventionPlugin"
+        }
+    }
+}
+
+spotless {
+    kotlin {
+        target("**/*.kt")
+        targetExclude("${project.layout.buildDirectory}/**/*.kt")
+        ktlint().editorConfigOverride(
+            mapOf(
+                "indent_size" to "2",
+                "continuation_indent_size" to "2",
+            )
+        )
+        licenseHeaderFile(file("../spotless/LicenseHeader.kt"))
+        trimTrailingWhitespace()
+        endWithNewline()
+    }
+    format("kts") {
+        target("**/*.kts")
+        targetExclude("${project.layout.buildDirectory}/**/*.kts")
+        licenseHeaderFile(file("../spotless/LicenseHeader.kt"), "(^(?![\\/ ]\\*).*$)")
+        trimTrailingWhitespace()
+        endWithNewline()
+    }
+}
 
 // Excerpt from Project kt-fuzzy licensed under the MIT license
 // By github.com/solo-studios, author solonovamax. See NOTICE file.
@@ -18,14 +85,3 @@ fun gradlePlugin(id: Provider<PluginDependency>, version: Provider<String>): Str
     val pluginId = id.get().pluginId
     return "$pluginId:$pluginId.gradle.plugin:${version.get()}"
 }
-
-
-kotlin {
-    target {
-        compilations.configureEach {
-            kotlinOptions {
-                jvmToolchain(libs.versions.java.get().toInt())
-            }
-        }
-    }
-}
\ No newline at end of file
diff --git a/build-logic/settings.gradle.kts b/build-logic/settings.gradle.kts
index 030b00897134503fe14d2bb0049a47dd1380f8ab..0d67e312ab086538cb490dd4336d7d19e4e3c3ac 100644
--- a/build-logic/settings.gradle.kts
+++ b/build-logic/settings.gradle.kts
@@ -1,3 +1,20 @@
+/*
+ *     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("UnstableApiUsage")
 
 rootProject.name = "build-logic"
@@ -15,4 +32,4 @@ dependencyResolutionManagement {
             from(files("../gradle/libs.versions.toml"))
         }
     }
-}
\ No newline at end of file
+}
diff --git a/build-logic/src/main/kotlin/convention.formatting.gradle.kts b/build-logic/src/main/kotlin/convention.formatting.gradle.kts
deleted file mode 100644
index 29f76de5f911b2e0b4c0e6412953d9496f8b1785..0000000000000000000000000000000000000000
--- a/build-logic/src/main/kotlin/convention.formatting.gradle.kts
+++ /dev/null
@@ -1,26 +0,0 @@
-plugins {
-    id("com.diffplug.spotless")
-}
-
-spotless {
-    kotlin {
-        target("**/*.kt")
-        targetExclude("${layout.buildDirectory}/**/*.kt")
-        ktlint().editorConfigOverride(
-            mapOf(
-                "indent_size" to "2",
-                "continuation_indent_size" to "2",
-            )
-        )
-        licenseHeaderFile(rootProject.file("spotless/libHeader.kt"))
-        trimTrailingWhitespace()
-        endWithNewline()
-    }
-    format("kts") {
-        target("**/*.kts")
-        targetExclude("${layout.buildDirectory}/**/*.kts")
-        licenseHeaderFile(rootProject.file("spotless/libHeader.kt"), "(^(?![\\/ ]\\*).*$)")
-        trimTrailingWhitespace()
-        endWithNewline()
-    }
-}
\ No newline at end of file
diff --git a/build-logic/src/main/kotlin/convention.publication.gradle.kts b/build-logic/src/main/kotlin/convention.publication.gradle.kts
deleted file mode 100644
index 260041957d6228b507e11a6f11163cc8e57f5567..0000000000000000000000000000000000000000
--- a/build-logic/src/main/kotlin/convention.publication.gradle.kts
+++ /dev/null
@@ -1,48 +0,0 @@
-//Publishing your Kotlin Multiplatform library to Maven Central
-//https://dev.to/kotlin/how-to-build-and-publish-a-kotlin-multiplatform-library-going-public-4a8k
-
-import org.gradle.api.publish.maven.MavenPublication
-import org.gradle.api.tasks.bundling.Jar
-import org.gradle.kotlin.dsl.`maven-publish`
-import org.gradle.kotlin.dsl.signing
-import java.util.*
-
-plugins {
-    id("maven-publish")
-    id("signing")
-}
-
-publishing {
-    // Configure maven repository
-    repositories {
-        maven {
-            name = "GitlabProjectRepository"
-            url = uri("https://git.ucsc.edu/api/v4/projects/12162/packages/maven")
-            credentials(HttpHeaderCredentials::class) {
-                name = "Job-Token"
-                value = System.getenv("CI_JOB_TOKEN")
-            }
-            authentication {
-                create("header", HttpHeaderAuthentication::class)
-            }
-        }
-    }
-}
-
-signing {
-    val signingKeyId: String? by project
-    val signingKey: String? by project
-    val signingPassword: String? by project
-    if (!signingKey.isNullOrEmpty()){
-        useInMemoryPgpKeys(signingKeyId, signingKey, signingPassword)
-        publishing.publications {
-            sign(publishing.publications)
-        }
-    }
-}
-
-//https://github.com/gradle/gradle/issues/26132
-val signingTasks = tasks.withType<Sign>()
-tasks.withType<AbstractPublishToMaven>().configureEach {
-    mustRunAfter(signingTasks)
-}
\ No newline at end of file
diff --git a/build-logic/src/main/kotlin/convention.version.gradle.kts b/build-logic/src/main/kotlin/convention.version.gradle.kts
deleted file mode 100644
index 1d740e35091ba4fc2b69e8f5b5e05cd8f1fbfa90..0000000000000000000000000000000000000000
--- a/build-logic/src/main/kotlin/convention.version.gradle.kts
+++ /dev/null
@@ -1,7 +0,0 @@
-plugins {
-    id("org.danilopianini.git-sensitive-semantic-versioning")
-}
-
-gitSemVer {
-    maxVersionLength.set(20)
-}
diff --git a/build-logic/src/main/kotlin/edu/ucsc/its/temerity/buildlogic/convention/FormattingConventionPlugin.kt b/build-logic/src/main/kotlin/edu/ucsc/its/temerity/buildlogic/convention/FormattingConventionPlugin.kt
new file mode 100644
index 0000000000000000000000000000000000000000..418c6591deacedbd9aaa0a5b6107b72964de1c6a
--- /dev/null
+++ b/build-logic/src/main/kotlin/edu/ucsc/its/temerity/buildlogic/convention/FormattingConventionPlugin.kt
@@ -0,0 +1,33 @@
+package edu.ucsc.its.temerity.buildlogic.convention
+
+import com.diffplug.gradle.spotless.SpotlessExtension
+import org.gradle.api.Plugin
+import org.gradle.api.Project
+
+class FormattingConventionPlugin : Plugin<Project> {
+  override fun apply(project: Project) = with(project) {
+    pluginManager.apply("com.diffplug.spotless")
+    extensions.configure(SpotlessExtension::class.java) {
+      kotlin {
+        target("**/*.kt")
+        targetExclude("${layout.buildDirectory}/**/*.kt")
+        ktlint("1.5.0").editorConfigOverride(
+          mapOf(
+            "indent_size" to "2",
+            "continuation_indent_size" to "2",
+          ),
+        )
+        licenseHeaderFile(file("../spotless/LicenseHeader.kt"))
+        trimTrailingWhitespace()
+        endWithNewline()
+      }
+      format("kts") {
+        target("**/*.kts")
+        targetExclude("${layout.buildDirectory}/**/*.kts")
+        licenseHeaderFile(file("../spotless/LicenseHeader.kt"), "(^(?![\\/ ]\\*).*$)")
+        trimTrailingWhitespace()
+        endWithNewline()
+      }
+    }
+  }
+}
diff --git a/build-logic/src/main/kotlin/edu/ucsc/its/temerity/buildlogic/convention/KmpConventionPlugin.kt b/build-logic/src/main/kotlin/edu/ucsc/its/temerity/buildlogic/convention/KmpConventionPlugin.kt
new file mode 100644
index 0000000000000000000000000000000000000000..10bda6e35a811607a7d2a7dfd078d4195c98ff15
--- /dev/null
+++ b/build-logic/src/main/kotlin/edu/ucsc/its/temerity/buildlogic/convention/KmpConventionPlugin.kt
@@ -0,0 +1,4 @@
+package edu.ucsc.its.temerity.buildlogic.convention
+
+class KmpConventionPlugin {
+}
diff --git a/build-logic/src/main/kotlin/edu/ucsc/its/temerity/buildlogic/convention/PublicationConventionPlugin.kt b/build-logic/src/main/kotlin/edu/ucsc/its/temerity/buildlogic/convention/PublicationConventionPlugin.kt
new file mode 100644
index 0000000000000000000000000000000000000000..13886f43166affdda8d741110d1354d0067cf29b
--- /dev/null
+++ b/build-logic/src/main/kotlin/edu/ucsc/its/temerity/buildlogic/convention/PublicationConventionPlugin.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.buildlogic.convention
+
+import org.gradle.api.Plugin
+import org.gradle.api.Project
+import org.gradle.api.credentials.HttpHeaderCredentials
+import org.gradle.api.publish.PublishingExtension
+import org.gradle.api.publish.maven.tasks.AbstractPublishToMaven
+import org.gradle.authentication.http.HttpHeaderAuthentication
+import org.gradle.kotlin.dsl.create
+import org.gradle.kotlin.dsl.credentials
+import org.gradle.kotlin.dsl.provideDelegate
+import org.gradle.kotlin.dsl.withType
+import org.gradle.plugins.signing.Sign
+import org.gradle.plugins.signing.SigningExtension
+import java.net.URI
+
+class PublicationConventionPlugin : Plugin<Project> {
+  override fun apply(project: Project) = with(project) {
+    pluginManager.apply("maven-publish")
+    extensions.configure(PublishingExtension::class.java) {
+      // Configure maven repository
+      repositories {
+        maven {
+          name = "GitlabProjectRepository"
+          url = URI("https://git.ucsc.edu/api/v4/projects/12162/packages/maven")
+          credentials(HttpHeaderCredentials::class) {
+            name = "Job-Token"
+            value = System.getenv("CI_JOB_TOKEN")
+          }
+          authentication {
+            create("header", HttpHeaderAuthentication::class)
+          }
+        }
+      }
+    }
+
+    pluginManager.apply("signing")
+    extensions.configure(SigningExtension::class.java) {
+      val signingKeyId: String? by project
+      val signingKey: String? by project
+      val signingPassword: String? by project
+      if (!signingKey.isNullOrEmpty()) {
+        useInMemoryPgpKeys(signingKeyId, signingKey, signingPassword)
+        extensions.configure(PublishingExtension::class.java) {
+          val pubExt = this
+          publications {
+            sign(pubExt.publications)
+          }
+        }
+      }
+    }
+
+    val signingTasks = tasks.withType<Sign>()
+    tasks.withType<AbstractPublishToMaven>().configureEach {
+      mustRunAfter(signingTasks)
+    }
+  }
+}
diff --git a/build-logic/src/main/kotlin/edu/ucsc/its/temerity/buildlogic/convention/VersionConventionPlugin.kt b/build-logic/src/main/kotlin/edu/ucsc/its/temerity/buildlogic/convention/VersionConventionPlugin.kt
new file mode 100644
index 0000000000000000000000000000000000000000..54f82322fee06e19affb95eaa465ac09899a635f
--- /dev/null
+++ b/build-logic/src/main/kotlin/edu/ucsc/its/temerity/buildlogic/convention/VersionConventionPlugin.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.buildlogic.convention
+
+import org.danilopianini.gradle.gitsemver.GitSemVerExtension
+import org.gradle.api.Plugin
+import org.gradle.api.Project
+
+class VersionConventionPlugin : Plugin<Project> {
+  override fun apply(project: Project) = with(project) {
+    pluginManager.apply("org.danilopianini.git-sensitive-semantic-versioning")
+    extensions.configure(GitSemVerExtension::class.java) {
+      maxVersionLength.set(20)
+    }
+  }
+}
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index 0070fa1bb5ece7cfb86312628307ca39e1160568..778814576534e00e449e40e3be69ba7685e4044e 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -1,47 +1,48 @@
 [versions]
-agp = "8.2.2"
-java = "17"
-kotlin = "2.1.0"
-ksp = "2.1.0-1.0.29"
-
-kotlinx-serialization = "1.8.0-RC"
-ktor = "3.0.3"
-cache4k = "0.13.0"
+agp = "8.7.3"
+jvm-target = "17"
+jvm-version = "17.0.14"
+kotlin = "2.1.10"
+ksp = "2.1.10-1.0.31"
+
+kotlinx-serialization = "1.8.0"
+ktor = "3.1.1"
+cache4k = "0.14.0"
 statelyConcurrentCollections = "2.1.0"
-slf4j = "2.0.16"
-kotlinx-io = "0.6.0"
+slf4j = "2.0.17"
+kotlinx-io = "0.7.0"
 
-ktorfit = "2.2.0"
+ktorfit = "2.4.0"
 coroutines = "1.10.1"
-datetime = "0.6.1"
-sandwich = "2.0.10"
+datetime = "0.6.2"
+sandwich = "2.1.0"
 arrow = "2.0.0"
 # Main Koin version - for core, android deps
-koin = "4.0.1"
-koinTest = "4.0.1"
-gradleBuildConfigPlugin = "5.5.1"
+koin = "4.0.2"
+koinTest = "4.0.2"
+gradleBuildConfigPlugin = "5.5.4"
 akkurate = "0.11.0"
 exposed = "0.56.0"
 
 dotenv-vault = "0.0.3"
-kermit = "2.0.4"
+kermit = "2.0.5"
 appdirs = "1.2.0"
 kstore = "0.9.1"
-kmpIo = "0.1.5"
+kmpIo = "0.1.6"
 kotlinSemver = "2.0.0"
 jansi = "2.4.1"
 temerity = "[0.1.0-dev0z+41180a5,0.1.0]"
 
-kotlinx-dataframe = "0.14.2"
+kotlinx-dataframe = "0.15.0"
 klaxon = "5.6"
-kotest = "6.0.0.M1"
+kotest = "6.0.0.M2"
 kotest-datatest = "5.9.1"
 
 dokka = "2.0.0"
-spotless = "7.0.0.BETA4"
-gitSemVer = "3.1.7"
-conventionalCommits = "1.0.12"
-konsist = "0.16.1"
+spotless = "7.0.0"
+gitSemVer = "4.0.2"
+conventionalCommits = "1.0.15"
+konsist = "0.17.3"
 
 minSdk = "24"
 targetSdk = "34"
@@ -66,6 +67,7 @@ kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serializa
 kotlinx-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "coroutines" }
 kotlinx-coroutines-debug = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-debug", version.ref = "coroutines" }
 kotlinx-coroutines-slf4j = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-slf4j", version.ref = "coroutines" }
+kotlinx-coroutines-test = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test", 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" }
 
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
index b82aa23a4f05d39d81870f8355ca43324f027298..94113f200e61179d552438ad36de4a645738eac2 100644
--- a/gradle/wrapper/gradle-wrapper.properties
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -1,6 +1,6 @@
 distributionBase=GRADLE_USER_HOME
 distributionPath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-8.11-bin.zip
 networkTimeout=10000
 validateDistributionUrl=true
 zipStoreBase=GRADLE_USER_HOME
diff --git a/settings.gradle.kts b/settings.gradle.kts
index 51505d6d476a09b0dd62e478eee32bb068ec655b..241007b54e8ef1c5f9622944596e67f9f34d3ad3 100644
--- a/settings.gradle.kts
+++ b/settings.gradle.kts
@@ -23,6 +23,7 @@ dependencyResolutionManagement {
         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")
+        maven("https://oss.sonatype.org/content/repositories/snapshots")
     }
 }
 
diff --git a/spotless/libHeader.kt b/spotless/LicenseHeader.kt
similarity index 99%
rename from spotless/libHeader.kt
rename to spotless/LicenseHeader.kt
index abf1bcf00a1c94a45c4a4da32a83637499845ea9..ad7271bc23a2838d8e7d8b3e9a61de9bfa2bceb6 100644
--- a/spotless/libHeader.kt
+++ b/spotless/LicenseHeader.kt
@@ -14,4 +14,4 @@
  *     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
- */
\ No newline at end of file
+ */
diff --git a/spotless/appHeader.kt b/spotless/appHeader.kt
deleted file mode 100644
index dd533b16d82499dfccbd78c595c1dc01f1586ec5..0000000000000000000000000000000000000000
--- a/spotless/appHeader.kt
+++ /dev/null
@@ -1,16 +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.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
\ No newline at end of file
diff --git a/spotless/appHeader.xml b/spotless/appHeader.xml
deleted file mode 100644
index 4dcfd18c07968cf26a0c871ed31dbaa1e9c0ede4..0000000000000000000000000000000000000000
--- a/spotless/appHeader.xml
+++ /dev/null
@@ -1,17 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
-    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.
-
-    Licensed under the Apache License, Version 2.0 (the "License");
-    you may not use this file except in compliance with the License.
-    You may obtain a copy of the License at
-
-    http://www.apache.org/licenses/LICENSE-2.0
-
-    Unless required by applicable law or agreed to in writing, software
-    distributed under the License is distributed on an "AS IS" BASIS,
-    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-    See the License for the specific language governing permissions and
-    limitations under the License.
--->
\ No newline at end of file
diff --git a/spotless/libHeader.xml b/spotless/libHeader.xml
deleted file mode 100644
index fac8567168d9f7bdfa1e85605791702396c8bd0e..0000000000000000000000000000000000000000
--- a/spotless/libHeader.xml
+++ /dev/null
@@ -1,18 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
-     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
--->
\ No newline at end of file
diff --git a/temerity/build.gradle.kts b/temerity/build.gradle.kts
index 0ab804f4a3d9707799d5b0532699745587dce8ea..0a4a0f62af75b1a686de1f479579e2cb8e7e0fe2 100644
--- a/temerity/build.gradle.kts
+++ b/temerity/build.gradle.kts
@@ -15,7 +15,6 @@
  *     License along with this library; if not, write to the Free Software
  *     Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  */
-import org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi
 import io.github.andreabrighi.gradle.gitsemver.conventionalcommit.ConventionalCommit
 import io.github.z4kn4fein.semver.Version
 import kotlinx.datetime.Clock
@@ -32,10 +31,10 @@ plugins {
     alias(libs.plugins.kotestMultiplatform)
     alias(libs.plugins.buildConfig)
 
-    id("convention.formatting")
-    id("convention.version")
+    id("temerity.formatting-convention")
+    id("temerity.version-convention")
     alias(libs.plugins.dokka)
-    id("convention.publication")
+    id("temerity.publication-convention")
 
     alias(libs.plugins.dataframe)
 
@@ -68,6 +67,7 @@ buildConfig {
             }
         }
     })
+    buildConfigField("JVM_VERSION", provider { libs.versions.jvm.version.get() })
 }
 
 gitSemVer {
@@ -79,11 +79,10 @@ dependencies {
 }
 
 kotlin {
-    jvmToolchain(libs.versions.java.get().toInt())
+    jvmToolchain(libs.versions.jvm.target.get().toInt())
     applyDefaultHierarchyTemplate()
     explicitApi()
 
-    @OptIn(ExperimentalKotlinGradlePluginApi::class)
     compilerOptions {
         freeCompilerArgs.add("-Xexpect-actual-classes")
     }
@@ -111,6 +110,7 @@ kotlin {
                 implementation(libs.statelyConcurrentCollections)
                 implementation(libs.kmpIo)
                 implementation(libs.kotlinx.io)
+                implementation(libs.kotlinSemver)
             }
         }
         val commonTest by getting {
@@ -132,7 +132,7 @@ kotlin {
         val androidMain by getting {
             dependencies {
                 implementation(libs.ktor.client.android)
-                // TODO: Add slf4f-api
+                // TODO: Add logging for Android
             }
         }
         val jvmTest by getting {
@@ -142,6 +142,7 @@ kotlin {
                 implementation(libs.kotest.runner.junit5)
                 implementation(libs.kotlinx.coroutines.slf4j)
                 implementation(libs.kotlinx.coroutines.debug)
+                implementation(libs.kotlinx.coroutines.test)
                 implementation(libs.kotlinx.dataframe)
                 // Required for report exports
                 // Used to convert dataframe rows to serializable objects
@@ -149,8 +150,6 @@ kotlin {
 
                 implementation(libs.koin.test)
                 implementation(libs.koin.test.junit5)
-
-                implementation(libs.kotlinSemver)
             }
         }
     }
diff --git a/temerity/src/androidMain/kotlin/edu/ucsc/its/temerity/core/LoggerFactory.android.kt b/temerity/src/androidMain/kotlin/edu/ucsc/its/temerity/core/LoggerFactory.android.kt
index bb98a1e554585bbdc31135f62c2040b2caac7e78..5e7ed34b3306cda5702f8a937b282346d9560c59 100644
--- a/temerity/src/androidMain/kotlin/edu/ucsc/its/temerity/core/LoggerFactory.android.kt
+++ b/temerity/src/androidMain/kotlin/edu/ucsc/its/temerity/core/LoggerFactory.android.kt
@@ -18,7 +18,6 @@
 package edu.ucsc.its.temerity.core
 
 import co.touchlab.kermit.Logger
-import edu.ucsc.its.temerity.TemClientConfig
 
 internal actual fun createCommonLogger(
   tag: String?,
diff --git a/temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/TemerityApi.kt b/temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/api/TemerityApi.kt
similarity index 93%
rename from temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/TemerityApi.kt
rename to temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/api/TemerityApi.kt
index cf4110f1d1ccd178aaa4965b05b9222c872ee3b1..04473d6eec4f31b9f8f64b716e1ec7c82fc57f16 100644
--- a/temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/TemerityApi.kt
+++ b/temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/api/TemerityApi.kt
@@ -15,10 +15,12 @@
  *     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.api
 
 import edu.ucsc.its.temerity.extensions.datetime.DateTimeExt.currentDate
 import edu.ucsc.its.temerity.model.AuditLogEntry
+import edu.ucsc.its.temerity.model.AuditLogSortOrder
+import edu.ucsc.its.temerity.model.BuildVariant
 import edu.ucsc.its.temerity.model.Course
 import edu.ucsc.its.temerity.model.Device
 import edu.ucsc.its.temerity.model.DeviceRecordingSession
@@ -261,7 +263,7 @@ public interface TemerityApi {
   public suspend fun getCachedUserRoles(refresh: Boolean = true): List<String>
 }
 
-// Objects used by library consumers and re-implementations of the Temerity API spec
+// Properties used by library consumers and re-implementations of the Temerity API spec
 
 public val AUDIT_LOG_REQUEST_DATE_FORMAT: DateTimeFormat<LocalDate> = LocalDate.Format {
   dayOfMonth()
@@ -291,19 +293,6 @@ public val SCHEDULED_SESSION_DATE_FORMAT: DateTimeFormat<LocalDate> = LocalDate.
   year()
 }
 
-/**
- * Enum class used to define the sort order for audit log entries.
- */
-public enum class AuditLogSortOrder {
-  NEW_FIRST,
-  OLD_FIRST,
-  ;
-
-  public companion object {
-    public fun fromBoolean(value: Boolean): AuditLogSortOrder = if (value) NEW_FIRST else OLD_FIRST
-  }
-}
-
 /**
  * Sorts a list of [AuditLogEntry] objects by their creation date.
  * @param sortOrder The order in which to sort the entries.
@@ -314,21 +303,3 @@ public fun ArrayList<AuditLogEntry>.sortByCreationDate(sortOrder: AuditLogSortOr
     AuditLogSortOrder.OLD_FIRST -> sortBy { auditLogEntry -> auditLogEntry.creationTimestamp }
   }
 }
-
-/**
- * Enum class used to define the build variant of the library.
- * For debugging purposes.
- */
-public enum class BuildVariant(public val variant: String) {
-  RELEASE("RELEASE"),
-  DEBUG("DEBUG"),
-  ;
-
-  internal companion object {
-    internal fun of(variant: String): BuildVariant = when (variant) {
-      "RELEASE" -> RELEASE
-      "DEBUG" -> DEBUG
-      else -> throw IllegalArgumentException("Invalid build variant: $variant")
-    }
-  }
-}
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
index eae0e55d1ac6f140e52c3baaf6a6a7d94766233a..f92bc986b6b0051a6dea235dbc2997a761e30871 100644
--- a/temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/core/LoggerFactory.kt
+++ b/temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/core/LoggerFactory.kt
@@ -18,7 +18,6 @@
 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)
diff --git a/temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/TemClientConfig.kt b/temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/core/TemClientConfig.kt
similarity index 88%
rename from temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/TemClientConfig.kt
rename to temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/core/TemClientConfig.kt
index 491f006cd23c7ba5e4e3b89a6dc90309ea65ef9b..4b2ef17f062902a894325c97f44f6ee2fdd809fa 100644
--- a/temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/TemClientConfig.kt
+++ b/temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/core/TemClientConfig.kt
@@ -15,9 +15,8 @@
  *     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.core
 
-import edu.ucsc.its.temerity.core.TemDsl
 import io.ktor.client.HttpClient
 import io.ktor.client.HttpClientConfig
 import io.ktor.client.engine.HttpClientEngineConfig
@@ -35,8 +34,8 @@ import kotlin.time.Duration
  *
  * @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 optWebTimeoutDuration Specifies the timeout [kotlin.time.Duration] to use when making web requests
+ * @property optCacheTimeoutDuration Specifies the timeout [kotlin.time.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
@@ -80,7 +79,7 @@ public class TemClientConfig {
   }
 
   /**
-   * Creates an custom [HttpClient] with the specified [HttpClientEngineFactory] and optional [block] configuration.
+   * Creates an custom [HttpClient] with the specified [io.ktor.client.engine.HttpClientEngineFactory] and optional [block] configuration.
    * Note that the Temerity config will be added afterwards.
    */
   public fun <T : HttpClientEngineConfig> httpClient(
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 308ae29be437764f40e0f99a025f33276881520e..97261a23863e7f1c0bd6739d798f72434e1062c8 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
@@ -24,17 +24,14 @@ 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.BuildConfig.BUILD_DATE
 import edu.ucsc.its.temerity.BuildConfig.BUILD_TIMEZONE
 import edu.ucsc.its.temerity.BuildConfig.BUILD_VARIANT
 import edu.ucsc.its.temerity.BuildConfig.LIB_VERSION
-import edu.ucsc.its.temerity.BuildVariant
-import edu.ucsc.its.temerity.TemClientConfig
-import edu.ucsc.its.temerity.TemerityApi
 import edu.ucsc.its.temerity.api.PlatformApi
+import edu.ucsc.its.temerity.api.TemerityApi
+import edu.ucsc.its.temerity.api.sortByCreationDate
 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
@@ -43,6 +40,9 @@ import edu.ucsc.its.temerity.extensions.coroutines.createJobScope
 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.AuditLogSortOrder
+import edu.ucsc.its.temerity.model.AuditLogSortOrder.NEW_FIRST
+import edu.ucsc.its.temerity.model.BuildVariant
 import edu.ucsc.its.temerity.model.Course
 import edu.ucsc.its.temerity.model.Device
 import edu.ucsc.its.temerity.model.DeviceRecordingSession
@@ -54,7 +54,6 @@ import edu.ucsc.its.temerity.model.User
 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.ktor.client.HttpClient
 import io.ktor.client.HttpClientConfig
 import io.ktor.client.engine.HttpClientEngine
@@ -112,6 +111,7 @@ public class Temerity internal constructor(
    * Constructor for Temerity
    * @param temApiToken The [String] API token to use for the Temerity client
    */
+  @Suppress("Unused")
   internal constructor(temApiToken: String) : this(TemClientConfig.withToken(temApiToken))
 
   /**
@@ -300,80 +300,72 @@ public class Temerity internal constructor(
     }
   }
 
-  public override suspend fun getUsers(): List<User> =
-    withContext(libraryCoroutineDispatcher) {
-      val userRequest = platformApi.getUsers()
-      val response = userRequest.executeApiResponse<String>()
-      decodeResponseCatching(response)
-    }
-
-  public override suspend fun getUser(userId: Long): User =
-    withContext(libraryCoroutineDispatcher) {
-      val returnedUsers = getUsers()
-      returnedUsers.first { it.userId == userId }
-    }
+  public override suspend fun getUsers(): List<User> = withContext(libraryCoroutineDispatcher) {
+    val userRequest = platformApi.getUsers()
+    val response = userRequest.executeApiResponse<String>()
+    decodeResponseCatching(response)
+  }
 
-  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 getUser(userId: Long): User = withContext(libraryCoroutineDispatcher) {
+    val returnedUsers = getUsers()
+    returnedUsers.first { it.userId == userId }
+  }
 
-  public override suspend fun updateUser(userId: Long, userUpdate: UserUpdate): HttpResponse =
-    withContext(libraryCoroutineDispatcher) {
-      val serializedUpdatedUser = createJobScope(jsonProcessingDispatcher).run {
-        json.encodeToString(userUpdate)
+  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.setUser(userId, serializedUpdatedUser)
+      val request = platformApi.createUser(primaryIdentifier, serializedNewUser)
       request.getOrThrow()
     }
+  }
 
-  public override suspend fun deleteUser(userId: Long): HttpResponse =
-    withContext(libraryCoroutineDispatcher) {
-      val request = platformApi.deleteUser(userId)
-      request.getOrThrow()
+  public override suspend fun updateUser(userId: Long, userUpdate: UserUpdate): HttpResponse = withContext(libraryCoroutineDispatcher) {
+    val serializedUpdatedUser = createJobScope(jsonProcessingDispatcher).run {
+      json.encodeToString(userUpdate)
     }
+    val request = platformApi.setUser(userId, serializedUpdatedUser)
+    request.getOrThrow()
+  }
 
-  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
-    }
+  public override suspend fun deleteUser(userId: Long): HttpResponse = withContext(libraryCoroutineDispatcher) {
+    val request = platformApi.deleteUser(userId)
+    request.getOrThrow()
+  }
 
-  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 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.put(it.hashCode(), it)
     }
+    roleTypes
+  }
 
-  public override suspend fun getUserGroups(userId: Long): List<UserGroup> =
-    withContext(libraryCoroutineDispatcher) {
-      val request = platformApi.getUserGroups(userId)
-      val response = request.executeApiResponse<String>()
-      decodeResponseCatching(response)
+  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> = withContext(libraryCoroutineDispatcher) {
+    val request = platformApi.getUserGroups(userId)
+    val response = request.executeApiResponse<String>()
+    decodeResponseCatching(response)
+  }
 
   public override suspend fun getUserGroupsOwned(userId: Long): List<UserGroup> = withContext(libraryCoroutineDispatcher) {
     val request = platformApi.getUserGroupsOwned(userId)
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
index d6fbe81f556afa013b7acc10362d538977c9b6a3..6f236350e0fcb19ecc2dc3ed45f637d8af302c6b 100644
--- a/temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/di/LibModule.kt
+++ b/temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/di/LibModule.kt
@@ -19,9 +19,9 @@ 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.TemClientConfig
 import edu.ucsc.its.temerity.core.Temerity.Companion.createLogger
 import edu.ucsc.its.temerity.core.buildHttpClient
 import edu.ucsc.its.temerity.extensions.coroutines.createLibraryScope
diff --git a/temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/extensions/datetime/DateTimeExt.kt b/temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/extensions/datetime/DateTimeExt.kt
index f10a839c34c9e7dffb0ccc0f5ba63bc6c1ff3ef4..6251f96c2fcf2df7952b86e1aac37be3fd86dcaf 100644
--- a/temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/extensions/datetime/DateTimeExt.kt
+++ b/temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/extensions/datetime/DateTimeExt.kt
@@ -15,6 +15,8 @@
  *     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("MemberVisibilityCanBePrivate")
+
 package edu.ucsc.its.temerity.extensions.datetime
 
 import kotlinx.datetime.Clock
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
index baf4c7d887f311954ef2b7cd7fe29e6472e57d47..6389ef182bcf2867d8aebb4521ff7cfca8ed1b5e 100644
--- 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
@@ -56,11 +56,10 @@ internal class FilesystemLogWriter internal constructor(
   }
 
   companion object {
-    operator fun invoke(block: Builder.() -> Unit) =
-      with(FilesystemLogWriterBuilder()) {
-        block(this)
-        build()
-      }
+    operator fun invoke(block: Builder.() -> Unit) = with(FilesystemLogWriterBuilder()) {
+      block(this)
+      build()
+    }
   }
 
   interface Builder {
diff --git a/temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/extensions/time/DateTimeExtensions.kt b/temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/extensions/time/DateTimeExtensions.kt
index 7ed64322b5d44f0fd76206566d0b49c49e09f028..c007a54eb15a376260a7090304095527df34c294 100644
--- a/temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/extensions/time/DateTimeExtensions.kt
+++ b/temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/extensions/time/DateTimeExtensions.kt
@@ -17,8 +17,8 @@
  */
 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
+import edu.ucsc.its.temerity.api.AUDIT_LOG_REQUEST_DATE_FORMAT
+import edu.ucsc.its.temerity.api.SCHEDULED_SESSION_DATE_FORMAT
 import kotlinx.datetime.LocalDate
 import kotlinx.datetime.format
 
@@ -26,12 +26,10 @@ import kotlinx.datetime.format
  * Formats a [LocalDate] object as a string in the format used by the YuJa API for audit log requests.
  * @return A string representation of the date in the format "dd/MM/yyyy".
  */
-internal fun LocalDate.applyAuditLogFormat(): String =
-  format(AUDIT_LOG_REQUEST_DATE_FORMAT)
+internal fun LocalDate.applyAuditLogFormat(): String = format(AUDIT_LOG_REQUEST_DATE_FORMAT)
 
 /**
  * Formats a [LocalDate] object as a string in the format used by the YuJa API for sessions.
  * @return A string representation of the date in the format "dd-MM-yyyy".
  */
-internal fun LocalDate.applyScheduledSessionDateFormat(): String =
-  format(SCHEDULED_SESSION_DATE_FORMAT)
+internal fun LocalDate.applyScheduledSessionDateFormat(): String = format(SCHEDULED_SESSION_DATE_FORMAT)
diff --git a/temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/extensions/time/LocalDateTimeSerializer.kt b/temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/extensions/time/LocalDateTimeSerializer.kt
index 96695252d3ce72acaa684e88cb9f2a96b62533bb..030e4ecb6b50206ee0024b2ece33b3be39a6bd99 100644
--- a/temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/extensions/time/LocalDateTimeSerializer.kt
+++ b/temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/extensions/time/LocalDateTimeSerializer.kt
@@ -17,7 +17,7 @@
  */
 package edu.ucsc.its.temerity.extensions.time
 
-import edu.ucsc.its.temerity.AUDIT_LOG_TIMESTAMP_FORMAT
+import edu.ucsc.its.temerity.api.AUDIT_LOG_TIMESTAMP_FORMAT
 import edu.ucsc.its.temerity.model.AuditLogEntry
 import kotlinx.datetime.LocalDateTime
 import kotlinx.datetime.format
diff --git a/temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/model/AuditLogSortOrder.kt b/temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/model/AuditLogSortOrder.kt
new file mode 100644
index 0000000000000000000000000000000000000000..71fd0780714ddddb0726e9a505d618e2ec205280
--- /dev/null
+++ b/temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/model/AuditLogSortOrder.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.model
+
+/**
+ * Enum class used to define the sort order for audit log entries.
+ */
+public enum class AuditLogSortOrder {
+  NEW_FIRST,
+  OLD_FIRST,
+  ;
+
+  public companion object {
+    public fun fromBoolean(value: Boolean): AuditLogSortOrder = if (value) NEW_FIRST else OLD_FIRST
+  }
+}
diff --git a/temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/model/BuildVariant.kt b/temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/model/BuildVariant.kt
new file mode 100644
index 0000000000000000000000000000000000000000..9276003d9f0a61ba0aa0e39d586b98f9402dcd2c
--- /dev/null
+++ b/temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/model/BuildVariant.kt
@@ -0,0 +1,36 @@
+/*
+ *     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.model
+
+/**
+ * Enum class used to define the build variant of the library.
+ * For debugging purposes.
+ */
+public enum class BuildVariant(public val variant: String) {
+  RELEASE("RELEASE"),
+  DEBUG("DEBUG"),
+  ;
+
+  internal companion object {
+    internal fun of(variant: String): BuildVariant = when (variant) {
+      "RELEASE" -> RELEASE
+      "DEBUG" -> DEBUG
+      else -> throw IllegalArgumentException("Invalid build variant: $variant")
+    }
+  }
+}
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
index 9cb64854d8f7a4c32115b92efa2ad55f5cc893cd..a076421f0c82a0085d521c473b7c80225a3a1add 100644
--- 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
@@ -23,7 +23,6 @@ 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 -> {
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
index d11bb8f44f4a9c7949808ddded9343f09f56e290..1b15cc11b595bf9a03b2f503136c03539f55395e 100644
--- 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
@@ -33,18 +33,15 @@ private fun withBrightColor(messageStringFormatter: MessageStringFormatter = Def
 internal class ColorFormatter(
   messageStringFormatter: MessageStringFormatter = DefaultFormatter,
 ) : WrappingFormatter(messageStringFormatter) {
-  override fun prefix(severity: Severity?, tag: Tag?, message: Message) =
-    severity?.toAnsiColor() ?: ""
+  override fun prefix(severity: Severity?, tag: Tag?, message: Message) = severity?.toAnsiColor() ?: ""
 
-  override fun suffix(severity: Severity?, tag: Tag?, message: Message) =
-    resetColor()
+  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 prefix(severity: Severity?, tag: Tag?, message: Message) = severity?.toBrightAnsiColor() ?: ""
 
   override fun suffix(severity: Severity?, tag: Tag?, message: Message) = resetColor()
 }
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 efec7524c820719f11fafb4a3f51598cb40545d1..4c6b0b83626df5ed163ccccab912786e1a3196ba 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
@@ -17,17 +17,10 @@
  */
 package edu.ucsc.its.temerity.test
 
-import edu.ucsc.its.temerity.core.Temerity
 import kotlinx.coroutines.runBlocking
 
-private class DevDeviceApiTests : TemerityFunSpec() {
+private class DevDeviceApiTests : TemDevFunSpec() {
   init {
-    beforeTest {
-      testTemerity = Temerity {
-        configureDevEnvironment(dotenv)
-      }
-    }
-
     test("4.2.1 - PlatformClient can fetch a list of devices") {
       runBlocking {
         val returnedDevices = testTemerity.getDevices()
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 521acc2a60bb99349dfffc7102a1e740e8bb94f8..41be5fea20aa996d8fef57a874c3e3b210090276 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
@@ -17,17 +17,10 @@
  */
 package edu.ucsc.its.temerity.test
 
-import edu.ucsc.its.temerity.core.Temerity
 import kotlinx.coroutines.runBlocking
 
-private class DevGroupApiTests : TemerityFunSpec() {
+private class DevGroupApiTests : TemDevFunSpec() {
   init {
-    beforeTest {
-      testTemerity = Temerity {
-        configureDevEnvironment(dotenv)
-      }
-    }
-
     test("2.2.1 - PlatformClient can fetch a list of groups") {
       runBlocking {
         val returnedGroups = testTemerity.getGroups()
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 43885a0c174e9dabb24a2fffc3bb777ff28d2e0e..07c2e7e8027e19d3444265dc92a94a14b61bc3fc 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
@@ -20,7 +20,6 @@ package edu.ucsc.its.temerity.test
 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.model.NewUser
 import edu.ucsc.its.temerity.model.UserUpdate
 import io.kotest.common.ExperimentalKotest
@@ -28,17 +27,11 @@ import io.kotest.matchers.shouldBe
 import io.ktor.http.HttpStatusCode
 import kotlinx.coroutines.runBlocking
 
-private class DevUserApiTests : TemerityFunSpec() {
+private class DevUserApiTests : TemDevFunSpec() {
   init {
     @OptIn(ExperimentalKotest::class)
     blockingTest = true
 
-    beforeTest {
-      testTemerity = Temerity {
-        configureDevEnvironment(dotenv)
-      }
-    }
-
     test("1.2.1 - PlatformClient can fetch a list of users") {
       runBlocking {
         val returnedUsers = testTemerity.getUsers()
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 aa3e10bdf99fe3a9071122dffa4839d8bd57ed9b..64b5cbc5ff3176921774331c3982695df4e03c86 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
@@ -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
  */
+@file:OptIn(KotestInternal::class)
+
 package edu.ucsc.its.temerity.test
 
-import edu.ucsc.its.temerity.core.Temerity
 import io.kotest.common.ExperimentalKotest
+import io.kotest.common.KotestInternal
 import io.kotest.engine.runBlocking
 import kotlin.time.Duration.Companion.hours
 import kotlin.time.Duration.Companion.minutes
 
-private class DevUtilityTests : TemerityFunSpec() {
-
+private class DevUtilityTests : TemDevFunSpec() {
   init {
     @OptIn(ExperimentalKotest::class)
     blockingTest = true
 
-    beforeTest {
-      testTemerity = Temerity {
-        configureDevEnvironment(dotenv)
-      }
-    }
     timeout = 5.minutes.inWholeMilliseconds
 
     test("Delete all Canvas Test Student Users").config(blockingTest = true, timeout = 12.hours) {
diff --git a/temerity/src/jvmTest/kotlin/edu/ucsc/its/temerity/test/ModelTests.kt b/temerity/src/jvmTest/kotlin/edu/ucsc/its/temerity/test/ModelTests.kt
index cec8e213c84032918802ac2e63629c41aa607c97..7ef1162569b947f7fe90f38ae471a216b2e198bb 100644
--- a/temerity/src/jvmTest/kotlin/edu/ucsc/its/temerity/test/ModelTests.kt
+++ b/temerity/src/jvmTest/kotlin/edu/ucsc/its/temerity/test/ModelTests.kt
@@ -21,7 +21,6 @@ import edu.ucsc.its.temerity.model.EventType
 import io.kotest.common.ExperimentalKotest
 
 private class ModelTests : TemerityFunSpec() {
-
   init {
     @OptIn(ExperimentalKotest::class)
     blockingTest = true
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 9589c8b017e9964c2fd66982fdc5d5f256efb1c9..87a59821f0318ea4551199a3d0f5a614608a5df0 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
@@ -17,8 +17,7 @@
  */
 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.model.AuditLogSortOrder.NEW_FIRST
 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
@@ -47,7 +46,7 @@ import kotlin.uuid.ExperimentalUuidApi
 import kotlin.uuid.Uuid
 
 @OptIn(ExperimentalUuidApi::class)
-private class ProdReportTests : TemerityFunSpec() {
+private class ProdReportTests : TemProdFunSpec() {
 
   init {
     @OptIn(ExperimentalKotest::class)
@@ -55,12 +54,6 @@ private class ProdReportTests : TemerityFunSpec() {
 
     val today = currentDate()
 
-    beforeTest {
-      testTemerity = Temerity {
-        configureProdEnvironment(dotenv)
-      }
-    }
-
     timeout = 5.minutes.inWholeMilliseconds
 
     test("PlatformClient can fetch a list of devices") {
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 632ec391b6437708d66aeb8e13061be3e9c6426d..4dfbc5f4ee81950826933234163307c86fa64412 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
@@ -15,26 +15,22 @@
  *     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:OptIn(KotestInternal::class)
+
 package edu.ucsc.its.temerity.test
 
-import edu.ucsc.its.temerity.core.Temerity
 import io.kotest.common.ExperimentalKotest
+import io.kotest.common.KotestInternal
 import io.kotest.engine.runBlocking
 import kotlin.time.Duration.Companion.hours
 import kotlin.time.Duration.Companion.minutes
 
-private class ProdUtilityTests : TemerityFunSpec() {
+private class ProdUtilityTests : TemProdFunSpec() {
 
   init {
     @OptIn(ExperimentalKotest::class)
     blockingTest = true
 
-    beforeTest {
-      testTemerity = Temerity {
-        configureProdEnvironment(dotenv)
-      }
-    }
-
     timeout = 5.minutes.inWholeMilliseconds
 
     test("Delete all Canvas Test Student Users").config(blockingTest = true, timeout = 12.hours) {
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 dd216fc4d88704d0742311d2397f67286a3302c7..c3c4f8cc5c9e9878129c86fe1ec9c7bd5c771d77 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
@@ -20,8 +20,8 @@ package edu.ucsc.its.temerity.test
 import co.touchlab.kermit.Logger
 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.model.AuditLogSortOrder.NEW_FIRST
 import edu.ucsc.its.temerity.model.EventType.NEW_LOG_IN
 import edu.ucsc.its.temerity.model.NewUser
 import edu.ucsc.its.temerity.util.currentDate
@@ -42,17 +42,11 @@ import kotlin.uuid.ExperimentalUuidApi
 import kotlin.uuid.Uuid
 
 @OptIn(ExperimentalUuidApi::class)
-private class TemerityDevTest : TemerityFunSpec() {
+private class TemerityDevTest : TemDevFunSpec() {
   init {
     @OptIn(ExperimentalKotest::class)
     blockingTest = true
 
-    beforeTest {
-      testTemerity = Temerity {
-        configureDevEnvironment(dotenv)
-      }
-    }
-
     test("Temerity client returns a correctly-formatted version String") {
       val returnedVersion = testTemerity.version.toVersion()
       kermit.d { "Returned client version: $returnedVersion" }
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 299c20b2fa6bc737fe5c8a424bff81f6d6256db8..f2c8893b9ef6c7c056651f846f405426c55c93d9 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
@@ -15,15 +15,19 @@
  *     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:OptIn(KotestInternal::class)
+
 package edu.ucsc.its.temerity.test
 
-import edu.ucsc.its.temerity.TemClientConfig
+import edu.ucsc.its.temerity.core.TemClientConfig
 import edu.ucsc.its.temerity.core.Temerity
 import edu.ucsc.its.temerity.core.Temerity.Companion.createLogger
 import io.github.cdimascio.dotenv.Configuration
 import io.github.cdimascio.dotenv.Dotenv
 import io.github.cdimascio.dotenv.dotenv
+import io.kotest.common.KotestInternal
 import io.kotest.core.extensions.TestCaseExtension
+import io.kotest.core.spec.Spec
 import io.kotest.core.spec.style.FunSpec
 import io.kotest.core.test.TestCase
 import io.kotest.core.test.TestResult
@@ -68,11 +72,10 @@ private class KoinExtensionImpl(
     execute(testCase)
   }
 
-  private fun TestCase.isApplicable() =
-    mode == KoinLifecycleMode.Root &&
-      isRootTest() ||
-      mode == KoinLifecycleMode.Test &&
-      type == TestType.Test
+  private fun TestCase.isApplicable() = mode == KoinLifecycleMode.Root &&
+    isRootTest() ||
+    mode == KoinLifecycleMode.Test &&
+    type == TestType.Test
 }
 
 enum class KoinLifecycleMode {
@@ -104,3 +107,21 @@ internal abstract class TemerityFunSpec : FunSpec() {
   internal val kermit: KermitLogger = createLogger()
   internal lateinit var testTemerity: Temerity
 }
+
+internal abstract class TemDevFunSpec : TemerityFunSpec() {
+  override suspend fun beforeSpec(spec: Spec) {
+    testTemerity = Temerity {
+      configureDevEnvironment(dotenv)
+    }
+    super.beforeSpec(spec)
+  }
+}
+
+internal abstract class TemProdFunSpec : TemerityFunSpec() {
+  override suspend fun beforeSpec(spec: Spec) {
+    testTemerity = Temerity {
+      configureProdEnvironment(dotenv)
+    }
+    super.beforeSpec(spec)
+  }
+}
diff --git a/temerity/src/jvmTest/kotlin/edu/ucsc/its/temerity/test/konsist/TestKonsistTest.kt b/temerity/src/jvmTest/kotlin/edu/ucsc/its/temerity/test/konsist/TestKonsistTest.kt
index b8920bc3153e0ab7b066652e0464589d0b6060c1..e988c97a74fe019b386df1321ab401a7d2c87427 100644
--- a/temerity/src/jvmTest/kotlin/edu/ucsc/its/temerity/test/konsist/TestKonsistTest.kt
+++ b/temerity/src/jvmTest/kotlin/edu/ucsc/its/temerity/test/konsist/TestKonsistTest.kt
@@ -27,7 +27,7 @@ class TestKonsistTest : FunSpec() {
 
   init {
 
-    beforeTest {
+    beforeSpec {
       konsist = Konsist.scopeFromProject()
     }