diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index 1de2d6856a32a75bd1594d495e3061196c1692a0..333c71451eed5cdda49f09da06145f3c4fd1f4dc 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -5,8 +5,8 @@ java = "17"
 kotlin = "2.0.20"
 ksp = "2.0.20-1.0.24"
 
-ktor = "3.0.0-beta-2"
-kotlinx-serialization = "1.7.1"
+ktor = "3.0.0-rc-1"
+kotlinx-serialization = "1.7.2"
 slf4j = "2.0.13"
 
 
@@ -16,15 +16,15 @@ datetime = "0.6.1"
 sandwich = "2.0.9"
 arrow = "1.2.4"
 # Main Koin version - for core, android deps
-koin = "3.6.0-Beta5"
-koinTest = "3.6.0-Beta5"
+koin = "4.0.0"
+koinTest = "4.0.0"
 # Koin version for Compose multiplatform
-koinComposeMultiplatform = "1.2.0-Beta4"
+koinComposeMultiplatform = "4.0.0"
 mosaic = "0.13.0"
 molecule = "2.0.0"
 jetbrainsNavigationCompose = "2.8.0-alpha09"
 jetbrainsLifecycle = "2.8.2"
-androidxRoom = "2.7.0-alpha08"
+androidxRoom = "2.7.0-alpha09"
 sqlite = "2.5.0-SNAPSHOT"
 androidxDatastore = "1.1.1"
 exposed = "0.53.0"
@@ -44,7 +44,7 @@ kotlinx-dataframe = "0.13.1"
 klaxon = "5.6"
 kotest = "5.9.1"
 
-composables = "1.11.2"
+composables = "1.12.0"
 bottomsheet = "0.1.5"
 alertKmp = "0.0.7"
 filekit = "0.7.0"
@@ -141,7 +141,7 @@ build-spotless = { module = "com.diffplug.spotless:spotless-plugin-gradle", vers
 dokka-base = { module = "org.jetbrains.dokka:dokka-base", version.ref = "dokka" }
 build-androidDocumentationPlugin = { module = "org.jetbrains.dokka:android-documentation-plugin", version.ref = "dokka" }
 build-gitSemVer = { module = "org.danilopianini:git-sensitive-semantic-versioning-gradle-plugin", version.ref = "gitSemVer"}
-build-conventionalCommits = { module = "io.github.andreabrighi:conventional-commit-strategy-for-git-sensitive-semantic-versioning-gradle-plugin", version.ref = "conventionalCommits"}
+build-conventionalCommits = { module = "io.github.andreabrighi:conventional-commit-strategy-for-git-sensitive-semantic-versioning-gradle-plugin", version.ref = "conventionalCommits" }
 
 # Testing deps
 dotenv-vault = { module = "com.github.dotenv-org:dotenv-vault-kotlin", version.ref = "dotenv-vault" }
diff --git a/shared/compose/src/commonMain/kotlin/edu/ucsc/its/temerity/shared/ui/MainScreen.kt b/shared/compose/src/commonMain/kotlin/edu/ucsc/its/temerity/shared/ui/MainScreen.kt
index bc3c2d91b627892f41494ed213a5f6ec1c842e6b..ce6bf22e80b4087b6f984d9883de82011cc17a75 100644
--- a/shared/compose/src/commonMain/kotlin/edu/ucsc/its/temerity/shared/ui/MainScreen.kt
+++ b/shared/compose/src/commonMain/kotlin/edu/ucsc/its/temerity/shared/ui/MainScreen.kt
@@ -89,7 +89,7 @@ import androidx.navigation.createGraph
 import androidx.navigation.toRoute
 import com.composables.composetheme.ComposeTheme
 import com.composables.composetheme.material3.colorScheme
-import edu.ucsc.its.temerity.Temerity
+import edu.ucsc.its.temerity.core.Temerity
 import edu.ucsc.its.temerity.model.Course
 import edu.ucsc.its.temerity.model.Device
 import edu.ucsc.its.temerity.model.User
diff --git a/shared/compose/src/commonMain/kotlin/edu/ucsc/its/temerity/shared/ui/TemerityApp.kt b/shared/compose/src/commonMain/kotlin/edu/ucsc/its/temerity/shared/ui/TemerityApp.kt
index 368ff87c3eca13c2ccb5a9d4f86e8e7cfe3d0e1d..cc8b5a292679fb55cda52df09969c905ad9c57fb 100644
--- a/shared/compose/src/commonMain/kotlin/edu/ucsc/its/temerity/shared/ui/TemerityApp.kt
+++ b/shared/compose/src/commonMain/kotlin/edu/ucsc/its/temerity/shared/ui/TemerityApp.kt
@@ -36,7 +36,6 @@ import org.koin.core.annotation.KoinExperimentalAPI
 
 private val koin = initKoin().koin
 
-@OptIn(KoinExperimentalAPI::class)
 @Composable
 internal fun TemerityApp(
   appStateViewModel: AppViewModel = koinViewModel(),
diff --git a/shared/shared/src/commonMain/kotlin/edu/ucsc/its/temerity/shared/data/repository/PlatformRepository.kt b/shared/shared/src/commonMain/kotlin/edu/ucsc/its/temerity/shared/data/repository/PlatformRepository.kt
index 384a666f2175e5be9eba5f79dbb9688e4303e60b..e2b02413e6f0fe875ae500bc8a5fcc8286a00db3 100644
--- a/shared/shared/src/commonMain/kotlin/edu/ucsc/its/temerity/shared/data/repository/PlatformRepository.kt
+++ b/shared/shared/src/commonMain/kotlin/edu/ucsc/its/temerity/shared/data/repository/PlatformRepository.kt
@@ -35,7 +35,7 @@ package edu.ucsc.its.temerity.shared.data.repository
 
 import androidx.compose.runtime.mutableLongStateOf
 import co.touchlab.kermit.Logger
-import edu.ucsc.its.temerity.Temerity
+import edu.ucsc.its.temerity.core.Temerity
 import edu.ucsc.its.temerity.model.Device
 import edu.ucsc.its.temerity.model.Group
 import edu.ucsc.its.temerity.model.User
diff --git a/shared/shared/src/commonMain/kotlin/edu/ucsc/its/temerity/shared/di/CommonModule.kt b/shared/shared/src/commonMain/kotlin/edu/ucsc/its/temerity/shared/di/CommonModule.kt
index 6a6a6573b8ad7196c2b709d87408ef09eece370c..b451b1c891624aedbaf57ee15cf4c51c43649b5b 100644
--- a/shared/shared/src/commonMain/kotlin/edu/ucsc/its/temerity/shared/di/CommonModule.kt
+++ b/shared/shared/src/commonMain/kotlin/edu/ucsc/its/temerity/shared/di/CommonModule.kt
@@ -19,7 +19,7 @@ package edu.ucsc.its.temerity.shared.di
 import ca.gosyer.appdirs.AppDirs
 import co.touchlab.kermit.Logger
 import co.touchlab.kermit.koin.kermitLoggerModule
-import edu.ucsc.its.temerity.core.temerityInstance
+import edu.ucsc.its.temerity.core.Temerity
 import edu.ucsc.its.temerity.shared.AppSettings
 import edu.ucsc.its.temerity.shared.data.repository.PlatformRepository
 import edu.ucsc.its.temerity.shared.data.storageModule
@@ -27,8 +27,8 @@ import edu.ucsc.its.temerity.shared.viewmodel.AppViewModel
 import edu.ucsc.its.temerity.shared.viewmodel.LoginViewModel
 import edu.ucsc.its.temerity.shared.viewmodel.RootViewModel
 import edu.ucsc.its.temerity.shared.viewmodel.UserEditViewModel
-import org.koin.compose.viewmodel.dsl.viewModelOf
 import org.koin.core.context.startKoin
+import org.koin.core.module.dsl.viewModelOf
 import org.koin.core.qualifier.named
 import org.koin.dsl.KoinAppDeclaration
 import org.koin.dsl.koinApplication
@@ -46,10 +46,10 @@ internal fun commonModule(isDebuggingEnabled: Boolean = false) = module {
   viewModelOf(::RootViewModel)
   viewModelOf(::UserEditViewModel)
   single { (serviceEndpoint: String, serviceToken: String) ->
-    temerityInstance {
-      debugEnabled(isDebuggingEnabled)
-      serviceEndpoint(serviceEndpoint)
+    Temerity {
+      serviceUrl(serviceEndpoint)
       serviceToken(serviceToken)
+      debugEnabled(isDebuggingEnabled)
     }
   }
   single { PlatformRepository() }
diff --git a/shared/shared/src/commonMain/kotlin/edu/ucsc/its/temerity/shared/viewmodel/AppViewModel.kt b/shared/shared/src/commonMain/kotlin/edu/ucsc/its/temerity/shared/viewmodel/AppViewModel.kt
index 0d1ed546722a9499b9e7056e53ed05679e209267..ea92f9ccc8433cb3fde90bb19e5ddab108128ed3 100644
--- a/shared/shared/src/commonMain/kotlin/edu/ucsc/its/temerity/shared/viewmodel/AppViewModel.kt
+++ b/shared/shared/src/commonMain/kotlin/edu/ucsc/its/temerity/shared/viewmodel/AppViewModel.kt
@@ -22,7 +22,7 @@ import androidx.compose.runtime.getValue
 import androidx.compose.runtime.mutableStateOf
 import androidx.compose.runtime.setValue
 import androidx.lifecycle.viewModelScope
-import edu.ucsc.its.temerity.Temerity
+import edu.ucsc.its.temerity.core.Temerity
 import edu.ucsc.its.temerity.shared.viewmodel.AppStateEvent.ServiceEndpointUpdated
 import edu.ucsc.its.temerity.shared.viewmodel.AppStateEvent.ServiceTokenUpdated
 import edu.ucsc.its.temerity.shared.viewmodel.AppStateEvent.UserLoggedIn
diff --git a/shared/shared/src/commonMain/kotlin/edu/ucsc/its/temerity/shared/viewmodel/DevicesScreenViewModel.kt b/shared/shared/src/commonMain/kotlin/edu/ucsc/its/temerity/shared/viewmodel/DevicesScreenViewModel.kt
index 194902f2460fdd7505a540b753c1b8290a0d5940..291417b8d7839e393c4fdd816a5faa5bfa62cc89 100644
--- a/shared/shared/src/commonMain/kotlin/edu/ucsc/its/temerity/shared/viewmodel/DevicesScreenViewModel.kt
+++ b/shared/shared/src/commonMain/kotlin/edu/ucsc/its/temerity/shared/viewmodel/DevicesScreenViewModel.kt
@@ -22,7 +22,7 @@ import androidx.compose.runtime.getValue
 import androidx.compose.runtime.mutableStateOf
 import androidx.compose.runtime.remember
 import androidx.compose.runtime.setValue
-import edu.ucsc.its.temerity.Temerity
+import edu.ucsc.its.temerity.core.Temerity
 import edu.ucsc.its.temerity.model.Device
 import kotlinx.coroutines.flow.Flow
 import org.koin.compose.getKoin
diff --git a/shared/shared/src/commonMain/kotlin/edu/ucsc/its/temerity/shared/viewmodel/RootViewModel.kt b/shared/shared/src/commonMain/kotlin/edu/ucsc/its/temerity/shared/viewmodel/RootViewModel.kt
index e29f377d368a39623dd4b2fffddf65bcc70c85e0..ef6c5db27f31faedda5bda7793cc1e5235d05455 100644
--- a/shared/shared/src/commonMain/kotlin/edu/ucsc/its/temerity/shared/viewmodel/RootViewModel.kt
+++ b/shared/shared/src/commonMain/kotlin/edu/ucsc/its/temerity/shared/viewmodel/RootViewModel.kt
@@ -17,7 +17,7 @@
 package edu.ucsc.its.temerity.shared.viewmodel
 
 import androidx.compose.runtime.Composable
-import edu.ucsc.its.temerity.Temerity
+import edu.ucsc.its.temerity.core.Temerity
 import kotlinx.coroutines.flow.Flow
 import org.koin.core.component.KoinComponent
 import org.koin.core.component.get
diff --git a/temerity/.env.vault b/temerity/.env.vault
index 176df552965dd99fce402512dac1e512d8be3341..25e8b67272661ad9921e0da0b2e78abcf851b6d9 100644
--- a/temerity/.env.vault
+++ b/temerity/.env.vault
@@ -4,8 +4,8 @@
 #/--------------------------------------------------/
 
 # development
-DOTENV_VAULT_DEVELOPMENT="U+0XF45FTYpDQEk5bU9LwOWezR5/PooEYuBsMa32Y5SmRgqaGutPs8bAVrPknRGR9hQG3J69xW9fG+WTMq4jHR5eonHLgitwoYNFPNzhyDEIIvL9ihp+kJdCKdxlmJUOOVqUdkckXgQJMy+CeQf5Td09NmQCRRlL1Sct0/X0pA+uq+osz2NObKr+NBJJuNSCofrfHR7bVxtBRLbBsAvTXAwD5lavNjtKf9i7sjALbmWQDPoZS+5WQfzPPv5B9Jp2Fe6GWvTCcjnctyMFMxxAY7I60uxpRxLJY7jdZRxKVCmUThcak7D7cie9nzMZLvLe9ME0y/ABNB4bgE5Rb1HXvl7yOl7C7f+CbcF+ExxVc6bPCSv5szOmYptT2CqY+85hN+LEpFWjK58GzfnupKE8exc5PSiIPjIwoB0tibZDX3oAeUMWmuQSMH/l8pj4PdxEJkIMJ5b9Kn3oGvIdwY9sKVKWfG57xg3XB3Pa5TmpBOv2DvGx2Qin1r2gTFlFxAgKpQGYSmoDREyNir2uKTlPdPSodBsyb5Srg4jpSQ5XuKMgWdDaf+heualPfddz+qP0wcd3zu/NCw6LwrHgiOQQtXkSQ+4WL8MAqRu5Sz7Z1FrF8MEiR19QrCeTFU1i2cACem6JmDrApJVpR+OpIwHVp2SdsfpGwHQGpa6vsdjeW8ZxwkXugOgqICtxtv7P1N80pi38UCDheIU/yabi7Qe2oC22wJ3A0+/BvNNaXC7J9IcdAimzJfkftrwppGqAndxhHzEeaHgC3UHvpx8ouDkWtf3pKVWka5inFXDaEqQ46Em53W9TKDvcBm1LQ97WKQTbBWx3Q7PG1tEUgmss/hzMzxb5/dlBDhL6j1OUPUTN1ctP7CZVC7irhCD4LWFcVBpivZJQJdOwIWpOjIpHtUbzJZQQh3gljcM+dOjKUuhscCvCvRRP2nbKVOMMM4TVfkWEhCUkGVqyu60q4GKrCWoljvAJJPo="
-DOTENV_VAULT_DEVELOPMENT_VERSION=19
+DOTENV_VAULT_DEVELOPMENT="DCHgKIC5D+Tef2A5UpGbSV6legsP/JCF7fUpqRdMwFsKbMst7VDmKDo4oLOvRCCgJl6ADvCm9oI2B79zntB1K5zRFo7TTbBtw9Vova+6X5BpoA7YYsf5N44aYKwsWqkRp6JD0p4VO0arpX1XWNjmK1LG2UPsfUU3gLu2I7plsYtSi4YRhNSh2gHccQCgC6FmQS9CG8KoQQFikW72DYmIMNALDq5xxKG2DgfRYTBER+qPwFAg7IQACIt/N4L9LEsrcnE1xFoPgwlLXUzLHlYpQpvfkg6V7brGobbcvUwf0+YsDHGFf48y9CGAOIlUYk9TgXyxi9T4Fa/bSwm+vLUDK7Mo26FI+8HIyaPgramA8X2+o6k73ULUfF8DAD2JiC5AWe42CSGaZAwCe0XILX/7iS4NTHnmX1Fx/zRAMxDZmPd0BTm9Mw9cXxtk1zeDAo37EUqKpztwMQ7PbMNjLeDzy/5DNAlO6Ke9u2RoUsphpolS6vm4Lnuheo9BlAk3N8inAzdPG8NMPhCfTmy1GmiseQtsUUdP4QtFRJtBSqJ2BBNTkD8bmwPWBdePM/w1jyLQhg4TRSpQtMydCJKt/WqqSzXq7JQJDz+9T3yH+snH8xiac/nQAvxocEw/CmA4kYsXsvYgFR7FgBTBW1ugcldfHx88eFZyPfJ/T9z0+IVovlTSUDOlWgFV8i4DbTUzYEu3mnoVTr2j98z0qzhpYu50MLl34ZtGVzQAuVWGmGvr+xueGQWhBzhKlPnNAmi5WEnnOY+ysnMaP+bIWHLbRmKyvWdRrxX8xySOEgsn7oH0mQZwWZZuYlHHGwT5CSSqe/LBNPTmSmOC6ZCj5Gf2q2dgCri1rzyyd2Ny7sI/22lB5WE1c7ltdxaVGtTHFt7wOrOrzAaklF4TlJD0gonje6ii+V7di/sn5uVbhpxvjbfnhWt82VYFK1F6Zr0xYwIjSehs7YUnlSLgsUzaWt6E/HbBB+3/M/0="
+DOTENV_VAULT_DEVELOPMENT_VERSION=20
 
 # ci
 DOTENV_VAULT_CI="n69uhs4m6qCfBKFNpDoqnpPfqeXrU+T3VIO+eseGVKWbRjZhRxx3VqAqq1P5Pml/YNJSR3t7N48cOQEhQdEvgwgIceG6DtJClkHZy712jhivwt6CVF7Hi0znBM9G0mub/guVwTUngFBRsQKw3XpDnxrzfo/FUPP09Zhy3D604wDnKuk/M6cUgT4o3jucOXmsjnwZTm/UUz+65eRSB0L5O8Gy7tB1e30eH5WUmx+hnG85v9+QENjt1inU9bZuefwnf7XzSaPKisVZVm4AYxXWnGIVP84Wy2O2h5nytLPdw2Z72mwaGSTRp0miUtUynTtFsm/SeoIOAgbixm7ScF4o/juwfq6IhVUBDfZStjXMx/150U2QZVbseVHKWrIdpPy9bZ7gO2ufMDps+daXWsJsHRqe11UTPhyZ8jA5V0oxDHe1y1cf0Dr9ZUr0g4E="
diff --git a/temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/TemClientConfig.kt b/temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/TemClientConfig.kt
new file mode 100644
index 0000000000000000000000000000000000000000..c4463bab1f6a9d689fbeaaf03d732683f70bb8c1
--- /dev/null
+++ b/temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/TemClientConfig.kt
@@ -0,0 +1,72 @@
+/*
+ *     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
+
+import edu.ucsc.its.temerity.core.TemDsl
+import io.ktor.client.HttpClient
+import io.ktor.client.HttpClientConfig
+import io.ktor.client.plugins.logging.LoggingConfig
+
+/**
+ * Configuration 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/TmdbClientConfig.kt
+ */
+@TemDsl
+public class TemClientConfig {
+  
+  /**
+   * Specifies URL of platform instance to make API calls to
+   */
+  public var serviceUrl: String? = null
+  /**
+   * Specifies the authorization token to use for API calls
+   */
+  public var serviceToken: String? = null
+  /**
+   * Specifies whether debug logging should be enabled
+   * Enable this for debugging purposes only
+   */
+  public var optDebugEnabled: Boolean = false
+
+  public var expectSuccess: Boolean = true
+  public var useTimeout: Boolean = false
+  public var maxRetriesOnException: 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
+
+  public fun logging(block: LoggingConfig.() -> Unit) {
+    httpClientLoggingBlock = block
+  }
+
+  public fun httpClient(block: () -> HttpClient) {
+    httpClientBuilder = 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/Temerity.kt b/temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/TemerityApi.kt
similarity index 78%
rename from temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/Temerity.kt
rename to temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/TemerityApi.kt
index 1750f49d452d92ef4edcdf1c80ee3ad71a16c25d..37a06a5588372b3cea2d518f723343e3d8a4748a 100644
--- a/temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/Temerity.kt
+++ b/temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/TemerityApi.kt
@@ -17,7 +17,6 @@
  */
 package edu.ucsc.its.temerity
 
-import edu.ucsc.its.temerity.core.TemerityImpl
 import edu.ucsc.its.temerity.model.AuditLogEntry
 import edu.ucsc.its.temerity.model.Course
 import edu.ucsc.its.temerity.model.Device
@@ -34,24 +33,17 @@ import io.ktor.client.statement.HttpResponse
 import kotlinx.datetime.DateTimeUnit
 import kotlinx.datetime.LocalDate
 import kotlinx.datetime.LocalDateTime
-import kotlinx.datetime.format
 import kotlinx.datetime.format.DateTimeFormat
 import kotlinx.datetime.format.char
 import kotlinx.datetime.plus
-import kotlinx.serialization.KSerializer
-import kotlinx.serialization.descriptors.PrimitiveKind
-import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor
-import kotlinx.serialization.descriptors.SerialDescriptor
-import kotlinx.serialization.encoding.Decoder
-import kotlinx.serialization.encoding.Encoder
 
 /**
  * This interface defines top-level library functions for interacting with the enterprise video platform API.
- * The functions defined in this interface are implemented by the [TemerityImpl] class.
- * @see [TemerityImpl]
+ * The functions defined in this interface are implemented by the [edu.ucsc.its.temerity.core.Temerity] class.
+ * @see [edu.ucsc.its.temerity.core.Temerity]
  */
 
-public interface Temerity {
+public interface TemerityApi {
 
   // User API public library functions
 
@@ -150,11 +142,17 @@ public interface Temerity {
 
   // Group API public library functions
 
-  public suspend fun getGroups(paginated: Boolean = true): List<Group>
+  public suspend fun getGroups(
+    // TODO: Make this field instance-wide and depend on API version
+    paginated: Boolean = true,
+  ): List<Group>
 
   public suspend fun getCourse(courseCode: String): Course
 
-  public suspend fun getCourses(paginated: Boolean = true): List<Course>
+  public suspend fun getCourses(
+    // TODO: Make this field instance-wide and depend on API version
+    paginated: Boolean = true,
+  ): List<Course>
 
   /**
    * Fetches a list of audit log entries from the platform for a given time period and list of event types.
@@ -184,6 +182,8 @@ public interface Temerity {
     endTime: LocalDate,
     eventType: EventType,
     sortOrder: AuditLogSortOrder? = null,
+    // TODO: Make this field instance-wide and depend on API version
+    paginated: Boolean = true,
   ): List<AuditLogEntry>
 
   /**
@@ -222,35 +222,18 @@ public interface Temerity {
    * @param groupId The ID of the group to fetch the storage usage for.
    */
   public suspend fun getStorageAnalyticsReport(groupId: Long): ByteArray
-
-  /**
-   * This interface represents the default PlatformClient builder pattern.
-   * It provides methods to set the service endpoint and token, and to build the PlatformClient.
-   */
-  public interface Builder {
-    public fun debugEnabled(debugEnabled: Boolean)
-
-    public fun serviceEndpoint(endpoint: String) {}
-    public fun serviceToken(token: String) {}
-    public fun build(): Temerity {
-      TODO()
-    }
-
-    @Suppress("CanBePrivate")
-    public fun build(builder: Builder.() -> Unit): Temerity
-  }
 }
 
-// Objects used by implementations of Temerity's PlatformClient
+// Objects used by Temerity implementations, and possibly library consumers
 
-private val AUDIT_LOG_REQUEST_DATE_FORMAT: DateTimeFormat<LocalDate> = LocalDate.Format {
+public val AUDIT_LOG_REQUEST_DATE_FORMAT: DateTimeFormat<LocalDate> = LocalDate.Format {
   dayOfMonth()
   char('/')
   monthNumber()
   char('/')
   year()
 }
-private val AUDIT_LOG_TIMESTAMP_FORMAT: DateTimeFormat<LocalDateTime> = LocalDateTime.Format {
+public val AUDIT_LOG_TIMESTAMP_FORMAT: DateTimeFormat<LocalDateTime> = LocalDateTime.Format {
   year()
   char('-')
   monthNumber()
@@ -263,7 +246,7 @@ private val AUDIT_LOG_TIMESTAMP_FORMAT: DateTimeFormat<LocalDateTime> = LocalDat
   char(':')
   second()
 }
-private val SCHEDULED_SESSION_DATE_FORMAT: DateTimeFormat<LocalDate> = LocalDate.Format {
+public val SCHEDULED_SESSION_DATE_FORMAT: DateTimeFormat<LocalDate> = LocalDate.Format {
   dayOfMonth()
   char('-')
   monthNumber()
@@ -286,40 +269,6 @@ public enum class AuditLogSortOrder {
   }
 }
 
-/**
- * Serializer for LocalDateTime objects used in the [AuditLogEntry] class.
- * This object is used to serialize and deserialize LocalDateTime objects to and from strings in the format used by the YuJa API.
- */
-internal object LocalDateTimeSerializer : KSerializer<LocalDateTime> {
-  override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("LocalDateTime", PrimitiveKind.STRING)
-
-  override fun serialize(encoder: Encoder, value: LocalDateTime) {
-    encoder.encodeString(value.format(AUDIT_LOG_TIMESTAMP_FORMAT))
-  }
-
-  override fun deserialize(decoder: Decoder): LocalDateTime {
-    return LocalDateTime.parse(decoder.decodeString(), AUDIT_LOG_TIMESTAMP_FORMAT)
-  }
-}
-
-/**
- * Extension functions used by Temerity's PlatformClient
- */
-
-/**
- * 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)
-
-/**
- * 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)
-
 /**
  * Sorts a list of [AuditLogEntry] objects by their creation date.
  * @param sortOrder The order in which to sort the entries.
diff --git a/temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/Util.kt b/temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/Util.kt
index 71a62b863d47861d0ce42188ba4adabe8d4b7b17..157f53f1a2dd182e0b04c6f8418679fc667dd3c0 100644
--- a/temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/Util.kt
+++ b/temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/Util.kt
@@ -25,12 +25,15 @@ 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 getDateTime() = Clock.System.now().toLocalDateTime(TimeZone.currentSystemDefault())
+internal fun currentTz(): TimeZone = TimeZone.currentSystemDefault()
 
-internal fun currentDate(): LocalDate = getDateTime().date
+internal fun thisInstant() = Clock.System.now()
 
-internal fun currentTime(): LocalTime = getDateTime().time
+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())
diff --git a/temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/api/PlatformApi.kt b/temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/api/PlatformApi.kt
index ae008b259d58eb65584c8d393daac422b827186e..5fdf5b6ddaeface4c7c4a6d2f52c9ddbec21ed38 100644
--- a/temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/api/PlatformApi.kt
+++ b/temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/api/PlatformApi.kt
@@ -154,8 +154,8 @@ internal interface PlatformApi {
   suspend fun getAuditLogEntries(
     @Query("startTime") startTime: String?,
     @Query("endTime") endTime: String?,
-    @Query("eventType") eventType: String,
     @Query("offset") entryOffset: Int? = 0,
+    @Query("eventType") eventType: String,
   ): HttpStatement
 
   @Streaming
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
new file mode 100644
index 0000000000000000000000000000000000000000..ccb9dd70ca94ab87e60180ea4f9603e656bdb06e
--- /dev/null
+++ b/temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/core/HttpClientFactory.kt
@@ -0,0 +1,104 @@
+/*
+ *     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 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,
+    timeoutDuration: Long? = TemerityLibrary.DEFAULT_TIMEOUT,
+    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.useTimeout) {
+        install(HttpTimeout) {
+          connectTimeoutMillis = timeoutDuration
+          requestTimeoutMillis = timeoutDuration
+          socketTimeoutMillis = timeoutDuration
+        }
+      }
+
+      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/JsonFactory.kt b/temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/core/JsonFactory.kt
new file mode 100644
index 0000000000000000000000000000000000000000..02f61f54d0c020a81d018f20a3fb4642f3659449
--- /dev/null
+++ b/temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/core/JsonFactory.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.core
+
+import kotlinx.serialization.json.Json
+
+/**
+ * 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
+  }
+}
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
index 760709e708e3ef954f1f05b5fd273a82dee24d08..0a709b19376256999d53fc9622e91fff23a46f5b 100644
--- a/temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/core/Library.kt
+++ b/temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/core/Library.kt
@@ -17,7 +17,7 @@
  */
 package edu.ucsc.its.temerity.core
 
-import edu.ucsc.its.temerity.Temerity
+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
@@ -45,12 +45,10 @@ public object TemerityLibrary {
 
 /**
  * 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
  */
-public fun temerityInstance(builder: Temerity.Builder.() -> Unit): Temerity =
-  TemerityLibrary.koin.get<Temerity.Builder>().build(builder)
-
-/**
- * Creates a PlatformClient.Builder instance using Kotlin-DSL.
- */
-public fun temerityBuilder(builder: Temerity.Builder.() -> Unit): Temerity.Builder =
-  TemerityLibrary.koin.get<Temerity.Builder>().apply(builder)
+@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/TemDsl.kt b/temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/core/TemDsl.kt
new file mode 100644
index 0000000000000000000000000000000000000000..1e31cc55353942773def5c3cf3e8a5e6c18855dc
--- /dev/null
+++ b/temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/core/TemDsl.kt
@@ -0,0 +1,25 @@
+/*
+ *     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
+
+/**
+ * DSL marker for the Temerity library.
+ */
+@DslMarker
+@Target(AnnotationTarget.CLASS, AnnotationTarget.TYPEALIAS, AnnotationTarget.TYPE, AnnotationTarget.FUNCTION)
+internal annotation class TemDsl
diff --git a/temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/core/TemerityImpl.kt b/temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/core/Temerity.kt
similarity index 77%
rename from temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/core/TemerityImpl.kt
rename to temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/core/Temerity.kt
index dda90584d67c4771fae74fe10bb7a7f7fcfb358a..eefc882f2e7ac0990ae654d8eb65b52b178d0637 100644
--- a/temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/core/TemerityImpl.kt
+++ b/temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/core/Temerity.kt
@@ -24,12 +24,14 @@ 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.Temerity
+import edu.ucsc.its.temerity.TemClientConfig
+import edu.ucsc.its.temerity.TemerityApi
 import edu.ucsc.its.temerity.api.PlatformApi
-import edu.ucsc.its.temerity.applyAuditLogFormat
-import edu.ucsc.its.temerity.applyScheduledSessionDateFormat
+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.di.TemerityKoinComponent
+import edu.ucsc.its.temerity.extensions.applyAuditLogFormat
+import edu.ucsc.its.temerity.extensions.applyScheduledSessionDateFormat
 import edu.ucsc.its.temerity.model.AuditLogEntry
 import edu.ucsc.its.temerity.model.Course
 import edu.ucsc.its.temerity.model.Device
@@ -44,6 +46,7 @@ import edu.ucsc.its.temerity.model.UserRecordingSession
 import edu.ucsc.its.temerity.model.UserUpdate
 import edu.ucsc.its.temerity.sortByCreationDate
 import io.ktor.client.statement.HttpResponse
+import io.ktor.http.parametersOf
 import io.ktor.util.cio.toByteArray
 import io.ktor.utils.io.ByteReadChannel
 import kotlinx.coroutines.Dispatchers
@@ -60,84 +63,40 @@ import org.koin.core.context.GlobalContext.get
 import org.koin.core.parameter.parametersOf
 import co.touchlab.kermit.Logger as KermitLogger
 
-// Based on sample code available from https://github.com/santimattius/kmp-networking/blob/main/README.md
-// Publicly instantiable only via factory method
-public class TemerityImpl private constructor(
-  private val debugEnabled: Boolean,
-  private var platformApi: PlatformApi,
-) : Temerity {
-
-  /**
-   * Exception class used to break out of a loop when reading paginated API responses.
-   */
-  private class BreakException(val page: Int) : RuntimeException() {
-    fun lastPage(): Int = page
-  }
-
-  private val json = Json {
-    coerceInputValues = true
-  }
+/*
+ * Adapted from https://github.com/santimattius/kmp-networking/blob/main/README.md
+ */
+public class Temerity internal constructor(
+  private val config: TemClientConfig,
+) : TemerityApi {
 
   /**
-   * Builder class for Temerity
+   * Constructor for Temerity
    */
-  internal class Builder : TemerityKoinComponent(), Temerity.Builder {
+  internal constructor(temApiToken: String) : this(TemClientConfig.withToken(temApiToken))
 
-    private var optDebugEnabled: Boolean = false
+  private val json: Json = buildJson()
+  private val platformApi: PlatformApi
 
-    /**
-     * Specifies whether debug logging should be enabled
-     * @param debugEnabled
-     * Enable this for debugging purposes only
-     */
-    override fun debugEnabled(debugEnabled: Boolean) {
-      optDebugEnabled = debugEnabled
+  init {
+    check(!config.serviceToken.isNullOrBlank()) {
+      "Service API auth token must be provided. Set it using TemClientConfig.serviceToken(token)"
     }
 
-    private var serviceEndpoint: String = ""
-
-    /**
-     * Specifies URL of platform instance to make API calls to
-     * @param endpoint The URL of the platform instance to make API calls to
-     */
-    override fun serviceEndpoint(endpoint: String) {
-      serviceEndpoint = endpoint
+    check(!config.serviceUrl.isNullOrBlank()) {
+      "Service url must be provided. Set it using TemClientConfig.serviceUrl(url)"
     }
 
-    private var serviceToken: String = ""
-
-    /**
-     * Specifies the authorization token to use for API calls
-     *  @param token The authorization token to use for API calls
-     */
-    override fun serviceToken(token: String) {
-      serviceToken = token
+    platformApi = koin.get<PlatformApi> {
+      parametersOf(config)
     }
+  }
 
-    /**
-     * Apply changes to builder and get the PlatformClient instance without needing to call [build] afterward.
-     */
-
-    @Suppress("CanBePrivate")
-    override fun build(builder: Temerity.Builder.() -> Unit): Temerity = this.apply(builder).build()
-
-    /**
-     * Creates a Temerity instance with specified service endpoint and token.
-     */
-    override fun build(): Temerity {
-      require(serviceEndpoint.isNotBlank()) {
-        "Must pass platform service endpoint as parameter"
-      }
-      require(serviceToken.isNotBlank()) {
-        "Must pass platform authorization token as parameter"
-      }
-      val koin = getKoin()
-      val platformApi: PlatformApi = koin.get { parametersOf(serviceEndpoint, optDebugEnabled, serviceToken) }
-      return TemerityImpl(
-        debugEnabled = optDebugEnabled,
-        platformApi,
-      )
-    }
+  /**
+   * Exception class used to break out of a loop when reading paginated API responses.
+   */
+  internal class BreakException(val page: Int = -1) : RuntimeException() {
+    fun lastPage(): Int = page
   }
 
   // Inlining advantageous here since copied parameter (ApiResponse<String>) is small and code fragment is used constantly (to validate every platform API request)
@@ -149,14 +108,13 @@ public class TemerityImpl private 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)
-        }
+        StatusCode.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 {
       json.decodeFromString<T>(apiResponsePayload)
@@ -166,6 +124,7 @@ public class TemerityImpl private 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")
         }
 
@@ -231,7 +190,7 @@ public class TemerityImpl private constructor(
         } catch (e: Exception) {
           when (e) {
             is BreakException -> {
-              if (debugEnabled) {
+              if (config.optDebugEnabled) {
                 get().get<KermitLogger>().d("Caught BreakException() from decodeResponseCatching() notifying we're done reading groups from API: ${e.page} pages read")
               }
               return returnedGroups
@@ -267,7 +226,7 @@ public class TemerityImpl private constructor(
         } catch (e: Exception) {
           when (e) {
             is BreakException -> {
-              if (debugEnabled) {
+              if (config.optDebugEnabled) {
                 get().get<KermitLogger>().d("Caught BreakException() from decodeResponseCatching() notifying we're done reading groups from API: $e")
               }
               return returnedCourses
@@ -320,25 +279,56 @@ public class TemerityImpl private constructor(
     endTime: LocalDate,
     eventType: EventType,
     sortOrder: AuditLogSortOrder?,
+    paginated: Boolean,
   ): List<AuditLogEntry> {
-    val returnedEntries = ArrayList<AuditLogEntry>()
-    var windowStart = endTime.minus(1, DateTimeUnit.Companion.MONTH)
-    var windowEnd = endTime
-    while (windowStart < windowEnd) {
-      val endpointRequest = platformApi.getAuditLogEntries(
-        startTime = windowStart.applyAuditLogFormat(),
-        endTime = windowEnd.applyAuditLogFormat(),
-        eventType = eventType.value,
-      )
-      val logEventEntries: List<AuditLogEntry> = decodeResponseCatching(endpointRequest.executeApiResponse<String>())
-      returnedEntries.addAll(logEventEntries)
-      windowEnd = windowStart
-      val backStep = windowStart.minus(1, DateTimeUnit.Companion.MONTH)
-      windowStart = if (backStep < startTime) startTime else backStep
+    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) {
+          startTime
+        } else {
+          endTime.minus(1, DateTimeUnit.Companion.MONTH)
+        }
+        var windowEnd = endTime
+        while (windowStart < windowEnd) {
+          try {
+            for (x in (0..Int.MAX_VALUE)) {
+              val endpointRequest = platformApi.getAuditLogEntries(
+                startTime = windowStart.applyAuditLogFormat(),
+                endTime = windowEnd.applyAuditLogFormat(),
+                entryOffset = x * 75,
+                eventType = eventType.value,
+              )
+              val logEventEntries: List<AuditLogEntry> = decodeResponseCatching(endpointRequest.executeApiResponse<String>(), 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")
+                }
+                break
+              }
+
+              else -> throw e
+            }
+          }
+          windowEnd = windowStart
+          val backStep = windowStart.minus(1, DateTimeUnit.Companion.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
+      }
+      false -> {
+        TODO("Implement non-paginated audit log entry retrieval")
+      }
     }
-    // Apply passed sortOrder, or default to NEW_FIRST TODO: Read from user settings
-    returnedEntries.applyOrDefault(sortOrder ?: AuditLogSortOrder.NEW_FIRST)
-    return returnedEntries
   }
 
   private fun ArrayList<AuditLogEntry>.applyOrDefault(sortOrder: AuditLogSortOrder?): ArrayList<AuditLogEntry> {
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
index 8a75898605643259cab57fb0b938847dbbed2c33..92400c898f7ac651fe51ceb7556436fece7e64e2 100644
--- a/temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/di/Koin.kt
+++ b/temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/di/Koin.kt
@@ -23,20 +23,12 @@ 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.Temerity
+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.TemerityImpl
-import edu.ucsc.its.temerity.core.TemerityLibrary
+import edu.ucsc.its.temerity.core.HttpClientFactory.buildHttpClient
 import edu.ucsc.its.temerity.core.TemerityLibrary.koin
 import io.ktor.client.HttpClient
-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.Logger
-import io.ktor.client.plugins.logging.Logging
-import io.ktor.client.request.header
 import org.koin.core.Koin
 import org.koin.core.component.KoinComponent
 import org.koin.core.parameter.parametersOf
@@ -51,38 +43,27 @@ internal fun loggerModule() = module {
 
 /**
  * This function provides the Koin module for the Temerity library.
- * It includes the platform module and provides factories for [HttpClient], [PlatformApi], [Temerity.Builder] and [Temerity].
- * Factories for the last two default to using [TemerityImpl] as the implementation.
+ * It includes the platform module and provides factories for [HttpClient] and [PlatformApi].
  */
 internal val libModule = module {
-  factory { (enableNetworkLogs: Boolean, authToken: String) ->
-    createHttpClient(
+  factory { (config: TemClientConfig) ->
+    buildHttpClient(
       httpClientEngine = get(),
-      enableNetworkLogs = enableNetworkLogs,
-      authToken = authToken,
+      config = config,
     )
   }
-  factory { (serviceEndpoint: String, enableNetworkLogs: Boolean, authToken: String) ->
-    val client: HttpClient = get { parametersOf(enableNetworkLogs, authToken) }
+  factory { (config: TemClientConfig) ->
+    val client: HttpClient = get { parametersOf(config) }
     val ktorfit = ktorfit {
-      baseUrl(serviceEndpoint)
+      config.serviceUrl?.let { baseUrl(it) }
       httpClient(client)
       converterFactories(ApiResponseConverterFactory.create())
     }
     ktorfit.createPlatformApi()
   }
-  factory<Temerity.Builder> {
-    TemerityImpl.Builder()
-  }
-  factory<Temerity> { (serviceEndpoint: String, authToken: String) ->
-    TemerityImpl.Builder().apply {
-      serviceEndpoint(serviceEndpoint)
-      serviceToken(authToken)
-    }.build()
-  }
 }
 
-private fun createLogger(tag: String?): co.touchlab.kermit.Logger = KermitLogger(
+private fun createLogger(tag: String?): KermitLogger = KermitLogger(
   config = loggerConfigInit(
     platformLogWriter(NoTagFormatter),
     minSeverity = Severity.Debug,
@@ -90,47 +71,6 @@ private fun createLogger(tag: String?): co.touchlab.kermit.Logger = KermitLogger
   tag = tag ?: "TemerityLib",
 )
 
-/**
- * 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 enableNetworkLogs Whether to log all requests and responses.
- * @param authToken The authToken to set as a default request header.
- * @return The created HttpClient.
- */
-private fun createHttpClient(httpClientEngine: HttpClientEngine, timeoutDuration: Long? = TemerityLibrary.DEFAULT_TIMEOUT, enableNetworkLogs: Boolean, authToken: String) = HttpClient(httpClientEngine) {
-  // 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
-  if (enableNetworkLogs) {
-    install(Logging) {
-      val kermit = koin.get<KermitLogger>()
-      logger = object : Logger {
-        override fun log(message: String) {
-          kermit.d(message)
-        }
-      }
-      level = LogLevel.ALL
-      sanitizeHeader { headerKey ->
-        headerKey == "authToken"
-      }
-    }
-  }
-
-  install(HttpTimeout) {
-    connectTimeoutMillis = timeoutDuration
-    requestTimeoutMillis = timeoutDuration
-    socketTimeoutMillis = timeoutDuration
-  }
-  // Pass the stored authToken as a header in every request
-  defaultRequest {
-    header("authToken", authToken)
-  }
-}
-
 internal abstract class TemerityKoinComponent : KoinComponent {
   override fun getKoin(): Koin = koin
 }
diff --git a/temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/extensions/DateTimeExtensions.kt b/temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/extensions/DateTimeExtensions.kt
new file mode 100644
index 0000000000000000000000000000000000000000..bfb164a31af64f1e313fb4c280d0e6161b6ff0ba
--- /dev/null
+++ b/temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/extensions/DateTimeExtensions.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
+
+import edu.ucsc.its.temerity.AUDIT_LOG_REQUEST_DATE_FORMAT
+import edu.ucsc.its.temerity.SCHEDULED_SESSION_DATE_FORMAT
+import kotlinx.datetime.LocalDate
+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)
+
+/**
+ * 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)
diff --git a/temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/extensions/LocalDateTimeSerializer.kt b/temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/extensions/LocalDateTimeSerializer.kt
new file mode 100644
index 0000000000000000000000000000000000000000..c4892dd3e20d5b832b5dd03686092d8895e9484e
--- /dev/null
+++ b/temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/extensions/LocalDateTimeSerializer.kt
@@ -0,0 +1,45 @@
+/*
+ *     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
+
+import edu.ucsc.its.temerity.AUDIT_LOG_TIMESTAMP_FORMAT
+import edu.ucsc.its.temerity.model.AuditLogEntry
+import kotlinx.datetime.LocalDateTime
+import kotlinx.datetime.format
+import kotlinx.serialization.KSerializer
+import kotlinx.serialization.descriptors.PrimitiveKind
+import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor
+import kotlinx.serialization.descriptors.SerialDescriptor
+import kotlinx.serialization.encoding.Decoder
+import kotlinx.serialization.encoding.Encoder
+
+/**
+ * Serializer for LocalDateTime objects used in the [AuditLogEntry] class.
+ * This object is used to serialize and deserialize LocalDateTime objects to and from strings in the format used by the YuJa API.
+ */
+internal object LocalDateTimeSerializer : KSerializer<LocalDateTime> {
+  override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("LocalDateTime", PrimitiveKind.STRING)
+
+  override fun serialize(encoder: Encoder, value: LocalDateTime) {
+    encoder.encodeString(value.format(AUDIT_LOG_TIMESTAMP_FORMAT))
+  }
+
+  override fun deserialize(decoder: Decoder): LocalDateTime {
+    return LocalDateTime.parse(decoder.decodeString(), AUDIT_LOG_TIMESTAMP_FORMAT)
+  }
+}
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 dc4d6eea6c8cf32b135aceb9865694639edf3f02..538b45508ef65fd405ae3a10fbc0583fec87b7a5 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.LocalDateTimeSerializer
+import edu.ucsc.its.temerity.extensions.LocalDateTimeSerializer
 import kotlinx.datetime.LocalDateTime
 import kotlinx.serialization.Serializable
 
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 eda775670db0f1c0711f8593335c11fbbe71c249..e00b4dd812aded46bf37e71867ea2a58e801e81a 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,8 +17,7 @@
  */
 package edu.ucsc.its.temerity.test
 
-import edu.ucsc.its.temerity.Temerity
-import edu.ucsc.its.temerity.core.temerityInstance
+import edu.ucsc.its.temerity.core.Temerity
 import io.kotest.core.spec.style.FunSpec
 import kotlinx.coroutines.runBlocking
 import org.dotenv.vault.dotenvVault
@@ -26,19 +25,19 @@ import org.dotenv.vault.dotenvVault
 class DevDeviceApiTests : FunSpec({
   val dotenv = dotenvVault()
 
-  lateinit var classUnderTest: Temerity
+  lateinit var temerityTest: Temerity
 
   beforeTest {
-    classUnderTest = temerityInstance {
-      debugEnabled(true)
-      serviceEndpoint(dotenv["YUJADEV_API_URL"])
-      serviceToken(dotenv["YUJADEV_TOKEN"])
+    temerityTest = Temerity {
+      serviceUrl = dotenv["YUJADEV_API_URL"]
+      serviceToken = dotenv["YUJADEV_TOKEN"]
+      optDebugEnabled = true
     }
   }
 
   test("4.2.1 - PlatformClient can fetch a list of devices") {
     runBlocking {
-      val returnedDevices = classUnderTest.getDevices()
+      val returnedDevices = temerityTest.getDevices()
       println("Returned devices:")
       returnedDevices.forEach { println(it) }
       assert(returnedDevices.isNotEmpty())
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 6658a22727339d7c920deb811b98740949aef378..0ee6f67c2229ce5f64d3601d723d9978b8964e4e 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,8 +17,7 @@
  */
 package edu.ucsc.its.temerity.test
 
-import edu.ucsc.its.temerity.Temerity
-import edu.ucsc.its.temerity.core.temerityInstance
+import edu.ucsc.its.temerity.core.Temerity
 import io.kotest.core.spec.style.FunSpec
 import kotlinx.coroutines.runBlocking
 import org.dotenv.vault.dotenvVault
@@ -27,19 +26,19 @@ class DevGroupApiTests : FunSpec({
 
   val dotenv = dotenvVault()
 
-  lateinit var classUnderTest: Temerity
+  lateinit var testTemerity: Temerity
 
   beforeTest {
-    classUnderTest = temerityInstance {
-      debugEnabled(true)
-      serviceEndpoint(dotenv["YUJADEV_API_URL"])
-      serviceToken(dotenv["YUJADEV_TOKEN"])
+    testTemerity = Temerity {
+      serviceUrl = dotenv["YUJADEV_API_URL"]
+      serviceToken = dotenv["YUJADEV_TOKEN"]
+      optDebugEnabled = true
     }
   }
 
   test("2.2.1 - PlatformClient can fetch a list of groups") {
     runBlocking {
-      val returnedGroups = classUnderTest.getGroups()
+      val returnedGroups = testTemerity.getGroups()
       println("Returned groups:")
       returnedGroups.forEach { println(it) }
       assert(returnedGroups.isNotEmpty())
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 b596f30dad95fab7125b30306b5eea1150e5fee7..40bd76b2787de078b7753e0404d325aa1fbcd2c7 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,8 +20,7 @@ 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.Temerity
-import edu.ucsc.its.temerity.core.temerityInstance
+import edu.ucsc.its.temerity.core.Temerity
 import edu.ucsc.its.temerity.model.NewUser
 import edu.ucsc.its.temerity.model.UserUpdate
 import io.kotest.core.spec.style.FunSpec
@@ -34,19 +33,18 @@ class DevUserApiTests : FunSpec({
 
   val dotenv = dotenvVault()
 
-  lateinit var classUnderTest: Temerity
-
+  lateinit var testTemerity: Temerity
   beforeTest {
-    classUnderTest = temerityInstance {
-      debugEnabled(true)
-      serviceEndpoint(dotenv["YUJADEV_API_URL"])
-      serviceToken(dotenv["YUJADEV_TOKEN"])
+    testTemerity = Temerity {
+      serviceUrl = dotenv["YUJADEV_API_URL"]
+      serviceToken = dotenv["YUJADEV_TOKEN"]
+      optDebugEnabled = true
     }
   }
 
   test("1.2.1 - PlatformClient can fetch a list of users") {
     runBlocking {
-      val returnedUsers = classUnderTest.getUsers()
+      val returnedUsers = testTemerity.getUsers()
       println("Returned users:")
       returnedUsers.forEach { Logger.d(it.toString()) }
       assert(returnedUsers.isNotEmpty())
@@ -55,10 +53,10 @@ class DevUserApiTests : FunSpec({
 
   test("1.2.2 - PlatformClient can fetch a single user") {
     runBlocking {
-      val users = classUnderTest.getUsers()
+      val users = testTemerity.getUsers()
       val userRef = users.find { user -> dotenv["YUJADEV_TEST_USER"] == user.loginId }
       requireNotNull(userRef)
-      val user = classUnderTest.getUser(userRef.userId)
+      val user = testTemerity.getUser(userRef.userId)
       Logger.d("Returned user with details:\n$user")
       assert(user.toString().isNotEmpty())
     }
@@ -74,14 +72,14 @@ class DevUserApiTests : FunSpec({
         loginPassword = "asdadfadf",
         userType = "Student",
       )
-      val response = classUnderTest.createUser(newUser = newUserRef)
+      val response = testTemerity.createUser(newUser = newUserRef)
       Logger.d("response: $response")
       response.getStatusCode().shouldBe(StatusCode.OK)
     }
   }
 
   test("1.2.4. - PlatformClient can update user details") {
-    val testUser = classUnderTest.getUsers().first { it.loginId == dotenv["YUJADEV_TEST_USER_ID"] }
+    val testUser = testTemerity.getUsers().first { it.loginId == dotenv["YUJADEV_TEST_USER_ID"] }
     Logger.d("Previous platform value for test user last name: ${testUser.lastName}")
     runBlocking {
       val updateObject = UserUpdate()
@@ -89,14 +87,14 @@ class DevUserApiTests : FunSpec({
       val newLastName = testUser.lastName.toLong().inc().toString()
       updateObject.lastName = newLastName
       Logger.d("Attempting to update user last name with value: ${testUser.lastName}")
-      val response = classUnderTest.updateUser(testUser.userId, updateObject)
+      val response = testTemerity.updateUser(testUser.userId, updateObject)
       Logger.d("Received response: $response")
     }
   }
 
   test("1.2.5 - PlatformClient can delete a user") {
     runBlocking {
-      val response = runBlocking { classUnderTest.deleteUser(dotenv["YUJADEV_TEST_USER_TO_DELETE"].toLong()) }
+      val response = runBlocking { testTemerity.deleteUser(dotenv["YUJADEV_TEST_USER_TO_DELETE"].toLong()) }
       Logger.d("response: $response")
       response.status.shouldBe(HttpStatusCode.OK)
     }
@@ -104,10 +102,10 @@ class DevUserApiTests : FunSpec({
 
   test("1.2.6 - PlatformClient can fetch a list of groups that a user is enrolled in") {
     runBlocking {
-      val users = classUnderTest.getUsers()
+      val users = testTemerity.getUsers()
       val userRef = users.find { user -> dotenv["YUJADEV_TEST_USER"] == user.loginId }
       requireNotNull(userRef)
-      val userGroups = classUnderTest.getUserGroups(userRef.userId)
+      val userGroups = testTemerity.getUserGroups(userRef.userId)
       Logger.d("Returned groups for the user ${userRef.loginId}:")
       userGroups.forEach { Logger.d(it.toString()) }
       assert(userGroups.isNotEmpty())
@@ -116,10 +114,10 @@ class DevUserApiTests : FunSpec({
 
   test("1.2.7 - PlatformClient can fetch a list of groups that a user is content owner of") {
     runBlocking {
-      val users = classUnderTest.getUsers()
+      val users = testTemerity.getUsers()
       val userRef = users.find { user -> dotenv["YUJADEV_TEST_USER"] == user.loginId }
       requireNotNull(userRef)
-      val userGroups = classUnderTest.getUserGroupsOwned(userRef.userId)
+      val userGroups = testTemerity.getUserGroupsOwned(userRef.userId)
       Logger.d("Returned owned groups for the user ${userRef.loginId}:")
       userGroups.forEach { Logger.d(it.toString()) }
       assert(userGroups.isNotEmpty())
@@ -136,7 +134,7 @@ class DevUserApiTests : FunSpec({
 
   test("Extra - PlatformClient can retrieve a list of candidate test students to be deleted from the platform") {
     runBlocking {
-      val users = classUnderTest.getUsers()
+      val users = testTemerity.getUsers()
       val testStudents = users.filter { it.loginId.contains("tstudent") }
       testStudents.forEach {
         Logger.d("Test student found: $it\n")
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 4859148f7c0125d0b20fb6825114b712a41d1389..bd3fade34ca3d098653796807c5099f9c0919adb 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.Temerity
-import edu.ucsc.its.temerity.core.temerityInstance
+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.model.EventType.AUTOMATED_SESSION_FAILED_TO_START
@@ -68,10 +67,10 @@ class ProdReportTests : FunSpec(), KoinTest {
     val today = Clock.System.todayIn(TimeZone.currentSystemDefault())
 
     beforeTest {
-      testTemerity = temerityInstance {
-        debugEnabled(true)
-        serviceEndpoint(dotenv["YUJAPROD_API_URL"])
-        serviceToken(dotenv["YUJAPROD_TOKEN"])
+      testTemerity = Temerity {
+        serviceUrl = dotenv["YUJAPROD_API_URL"]
+        serviceToken = dotenv["YUJAPROD_TOKEN"]
+        optDebugEnabled = true
       }
     }
 
@@ -218,11 +217,11 @@ class ProdReportTests : FunSpec(), KoinTest {
       }
     }
 
-    test("Get all audit log entries of type \"New Login\" from the past day") {
+    test("Get all audit log entries of type \"New Login\" from the past 4 days") {
       runBlocking {
         val testEventType = NEW_LOG_IN
         val currentDate = Clock.System.todayIn(TimeZone.currentSystemDefault())
-        val pastDate = currentDate.minus(1, DateTimeUnit.DAY)
+        val pastDate = currentDate.minus(4, DateTimeUnit.DAY)
 
         val returnedAuditLogEntries = testTemerity.getAuditLogEntries(
           startTime = pastDate,
@@ -230,7 +229,7 @@ class ProdReportTests : FunSpec(), KoinTest {
           eventType = testEventType,
           sortOrder = NEW_FIRST,
         )
-        returnedAuditLogEntries.forEach { println(it) }
+        returnedAuditLogEntries.forEach { kermit.d(it.toString()) }
         val df = returnedAuditLogEntries.toDataFrame()
 
         df.writeCSV("${testEventType.name}-Event-Report-${Uuid.random()}-$pastDate-$currentDate-generated-${currentDate()}.csv")
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 7990e05f862ae7e87fe034934faec61498018de0..4d28a1363340bd1222ec662da237847ab2700384 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
@@ -21,8 +21,7 @@ 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.Temerity
-import edu.ucsc.its.temerity.core.temerityInstance
+import edu.ucsc.its.temerity.core.Temerity
 import edu.ucsc.its.temerity.currentDate
 import edu.ucsc.its.temerity.model.EventType.NEW_LOG_IN
 import edu.ucsc.its.temerity.model.NewUser
@@ -49,13 +48,13 @@ class TemerityDevTest : FunSpec({
 
   val dotenv = dotenvVault()
 
-  lateinit var classUnderTest: Temerity
+  lateinit var testTemerity: Temerity
 
   beforeTest {
-    classUnderTest = temerityInstance {
-      debugEnabled(true)
-      serviceEndpoint(dotenv["YUJADEV_API_URL"])
-      serviceToken(dotenv["YUJADEV_TOKEN"])
+    testTemerity = Temerity {
+      serviceUrl = dotenv["YUJADEV_API_URL"]
+      serviceToken = dotenv["YUJADEV_TOKEN"]
+      optDebugEnabled = true
     }
   }
 
@@ -64,7 +63,7 @@ class TemerityDevTest : FunSpec({
       val currentDate = Clock.System.todayIn(TimeZone.currentSystemDefault())
       val pastDate = currentDate.minus(1, DateTimeUnit.MONTH)
 
-      val returnedAuditLogEntries = classUnderTest.getAuditLogEntries(
+      val returnedAuditLogEntries = testTemerity.getAuditLogEntries(
         startTime = pastDate,
         endTime = currentDate,
         eventType = NEW_LOG_IN,
@@ -79,7 +78,7 @@ class TemerityDevTest : FunSpec({
     runBlocking {
       val currentDate = Clock.System.todayIn(TimeZone.currentSystemDefault())
       val pastDate = currentDate.minus(1, DateTimeUnit.MONTH)
-      val logs = classUnderTest.getAuditLogEntries(pastDate, currentDate, sortOrder = NEW_FIRST)
+      val logs = testTemerity.getAuditLogEntries(pastDate, currentDate, sortOrder = NEW_FIRST)
       println("Returned events from the past month:")
       logs.forEach { println(it) }
       assert(logs.isNotEmpty())
@@ -89,7 +88,7 @@ class TemerityDevTest : FunSpec({
     runBlocking {
       val currentDate = Clock.System.todayIn(TimeZone.currentSystemDefault())
       val pastDate = currentDate.minus(1, DateTimeUnit.DAY)
-      val logs = classUnderTest.getAuditLogEntries(pastDate, currentDate, sortOrder = NEW_FIRST)
+      val logs = testTemerity.getAuditLogEntries(pastDate, currentDate, sortOrder = NEW_FIRST)
       Logger.d("Returned events from the past day:")
       logs.forEach { Logger.d(it.toString()) }
       assert(logs.isNotEmpty())
@@ -99,7 +98,7 @@ class TemerityDevTest : FunSpec({
     runBlocking {
       val pastDate = Clock.System.todayIn(TimeZone.currentSystemDefault()).minus(1, DateTimeUnit.DAY)
       val futureDate = pastDate.plus(1, DateTimeUnit.MONTH)
-      val sessions = classUnderTest.getUserSessions("dotenv[YUJADEV_TEST_USER]", pastDate, futureDate)
+      val sessions = testTemerity.getUserSessions("dotenv[YUJADEV_TEST_USER]", pastDate, futureDate)
       Logger.d("Returned sessions for the next week:")
       sessions.forEach { Logger.d(it.toString()) }
       assert(sessions.isNotEmpty())
@@ -116,7 +115,7 @@ class TemerityDevTest : FunSpec({
         loginPassword = "asdadfadf",
         userType = "Student",
       )
-      val response = classUnderTest.createUser(newUser = newUserRef)
+      val response = testTemerity.createUser(newUser = newUserRef)
       Logger.d("response: $response")
       response.getStatusCode().shouldBe(StatusCode.OK)
     }
@@ -127,8 +126,8 @@ class TemerityDevTest : FunSpec({
       val pastDate = Clock.System.todayIn(TimeZone.currentSystemDefault()).minus(1, DateTimeUnit.DAY)
       val futureDate = pastDate.plus(1, DateTimeUnit.MONTH)
       val testDeviceId = dotenv["YUJADEV_TEST_DEVICE_ID"].toLongOrNull()
-      val sessions = classUnderTest.getDeviceSchedule(testDeviceId!!, pastDate, futureDate)
-      val deviceName = classUnderTest.getDevice(testDeviceId).stationName
+      val sessions = testTemerity.getDeviceSchedule(testDeviceId!!, pastDate, futureDate)
+      val deviceName = testTemerity.getDevice(testDeviceId).stationName
       println("Returned sessions for the device code 6489 with name $deviceName:")
       sessions.forEach { println(it) }
       assert(sessions.isNotEmpty())
@@ -148,7 +147,7 @@ class TemerityDevTest : FunSpec({
       val currentDate = Clock.System.todayIn(TimeZone.currentSystemDefault())
       val pastDate = currentDate.minus(1, DateTimeUnit.DAY)
 
-      val returnedAuditLogEntries = classUnderTest.getAuditLogEntries(
+      val returnedAuditLogEntries = testTemerity.getAuditLogEntries(
         startTime = pastDate,
         endTime = currentDate,
         eventType = testEventType,
@@ -166,7 +165,7 @@ class TemerityDevTest : FunSpec({
   test("PlatformClient can get the analytics storage report for a specific group") {
     val testGroup = dotenv["YUJADEV_TEST_GROUP"].toLong()
     runBlocking {
-      val reportBytes = classUnderTest.getStorageAnalyticsReport(testGroup)
+      val reportBytes = testTemerity.getStorageAnalyticsReport(testGroup)
       val outputFile = File("storage-report-group-$testGroup-${currentDate()}-${Uuid.random()}.csv")
       outputFile.outputStream().use { fileOut ->
         fileOut.write(reportBytes)
@@ -186,7 +185,7 @@ class TemerityDevTest : FunSpec({
         loginPassword = "password",
         userType = "Student",
       )
-      val response = classUnderTest.createUser(newUser = newUser)
+      val response = testTemerity.createUser(newUser = newUser)
       Logger.d(response.toString())
     }
   }