diff --git a/.scala-steward.conf b/.scala-steward.conf
index 588e5b52..00499248 100644
--- a/.scala-steward.conf
+++ b/.scala-steward.conf
@@ -1,7 +1,3 @@
 updates.ignore = [
   { groupId = "org.apache.poi" },
 ]
-
-updates.pin = [
-  { groupId = "co.fs2", version = "2." }
-]
\ No newline at end of file
diff --git a/modules/analysis/src/main/scala/docspell/analysis/TextAnalyser.scala b/modules/analysis/src/main/scala/docspell/analysis/TextAnalyser.scala
index 7f52fd44..6f0c8dd8 100644
--- a/modules/analysis/src/main/scala/docspell/analysis/TextAnalyser.scala
+++ b/modules/analysis/src/main/scala/docspell/analysis/TextAnalyser.scala
@@ -32,10 +32,7 @@ object TextAnalyser {
       labels ++ dates.map(dl => dl.label.copy(label = dl.date.toString))
   }
 
-  def create[F[_]: Concurrent: Timer: ContextShift](
-      cfg: TextAnalysisConfig,
-      blocker: Blocker
-  ): Resource[F, TextAnalyser[F]] =
+  def create[F[_]: Async](cfg: TextAnalysisConfig): Resource[F, TextAnalyser[F]] =
     Resource
       .eval(Nlp(cfg.nlpConfig))
       .map(stanfordNer =>
@@ -56,7 +53,7 @@ object TextAnalyser {
             } yield Result(spans ++ list, dates)
 
           def classifier: TextClassifier[F] =
-            new StanfordTextClassifier[F](cfg.classifier, blocker)
+            new StanfordTextClassifier[F](cfg.classifier)
 
           private def textLimit(logger: Logger[F], text: String): F[String] =
             if (cfg.maxLength <= 0)
@@ -82,7 +79,7 @@ object TextAnalyser {
 
   /** Provides the nlp pipeline based on the configuration. */
   private object Nlp {
-    def apply[F[_]: Concurrent: Timer](
+    def apply[F[_]: Async](
         cfg: TextAnalysisConfig.NlpConfig
     ): F[Input[F] => F[Vector[NerLabel]]] =
       cfg.mode match {
@@ -104,7 +101,7 @@ object TextAnalyser {
         text: String
     )
 
-    def annotate[F[_]: BracketThrow](
+    def annotate[F[_]: Async](
         cache: PipelineCache[F]
     )(input: Input[F]): F[Vector[NerLabel]] =
       cache
diff --git a/modules/analysis/src/main/scala/docspell/analysis/classifier/StanfordTextClassifier.scala b/modules/analysis/src/main/scala/docspell/analysis/classifier/StanfordTextClassifier.scala
index dc567695..14257b40 100644
--- a/modules/analysis/src/main/scala/docspell/analysis/classifier/StanfordTextClassifier.scala
+++ b/modules/analysis/src/main/scala/docspell/analysis/classifier/StanfordTextClassifier.scala
@@ -2,10 +2,11 @@ package docspell.analysis.classifier
 
 import java.nio.file.Path
 
+import cats.effect.Ref
 import cats.effect._
-import cats.effect.concurrent.Ref
 import cats.implicits._
 import fs2.Stream
+import fs2.io.file.Files
 
 import docspell.analysis.classifier
 import docspell.analysis.classifier.TextClassifier._
@@ -15,10 +16,8 @@ import docspell.common.syntax.FileSyntax._
 
 import edu.stanford.nlp.classify.ColumnDataClassifier
 
-final class StanfordTextClassifier[F[_]: Sync: ContextShift](
-    cfg: TextClassifierConfig,
-    blocker: Blocker
-) extends TextClassifier[F] {
+final class StanfordTextClassifier[F[_]: Async](cfg: TextClassifierConfig)
+    extends TextClassifier[F] {
 
   def trainClassifier[A](
       logger: Logger[F],
@@ -28,7 +27,7 @@ final class StanfordTextClassifier[F[_]: Sync: ContextShift](
       .withTempDir(cfg.workingDir, "trainclassifier")
       .use { dir =>
         for {
-          rawData   <- writeDataFile(blocker, dir, data)
+          rawData   <- writeDataFile(dir, data)
           _         <- logger.debug(s"Learning from ${rawData.count} items.")
           trainData <- splitData(logger, rawData)
           scores    <- cfg.classifierConfigs.traverse(m => train(logger, trainData, m))
@@ -81,8 +80,8 @@ final class StanfordTextClassifier[F[_]: Sync: ContextShift](
       TrainData(in.file.resolveSibling("train.txt"), in.file.resolveSibling("test.txt"))
 
     val fileLines =
-      fs2.io.file
-        .readAll(in.file, blocker, 4096)
+      File
+        .readAll[F](in.file, 4096)
         .through(fs2.text.utf8Decode)
         .through(fs2.text.lines)
 
@@ -95,7 +94,7 @@ final class StanfordTextClassifier[F[_]: Sync: ContextShift](
           .take(nTest)
           .intersperse("\n")
           .through(fs2.text.utf8Encode)
-          .through(fs2.io.file.writeAll(td.test, blocker))
+          .through(Files[F].writeAll(td.test))
           .compile
           .drain
       _ <-
@@ -103,13 +102,13 @@ final class StanfordTextClassifier[F[_]: Sync: ContextShift](
           .drop(nTest)
           .intersperse("\n")
           .through(fs2.text.utf8Encode)
-          .through(fs2.io.file.writeAll(td.train, blocker))
+          .through(Files[F].writeAll(td.train))
           .compile
           .drain
     } yield td
   }
 
-  def writeDataFile(blocker: Blocker, dir: Path, data: Stream[F, Data]): F[RawData] = {
+  def writeDataFile(dir: Path, data: Stream[F, Data]): F[RawData] = {
     val target = dir.resolve("rawdata")
     for {
       counter <- Ref.of[F, Long](0L)
@@ -120,7 +119,7 @@ final class StanfordTextClassifier[F[_]: Sync: ContextShift](
           .evalTap(_ => counter.update(_ + 1))
           .intersperse("\r\n")
           .through(fs2.text.utf8Encode)
-          .through(fs2.io.file.writeAll(target, blocker))
+          .through(Files[F].writeAll(target))
           .compile
           .drain
       lines <- counter.get
diff --git a/modules/analysis/src/main/scala/docspell/analysis/date/DateFind.scala b/modules/analysis/src/main/scala/docspell/analysis/date/DateFind.scala
index c517bc4a..a36a2698 100644
--- a/modules/analysis/src/main/scala/docspell/analysis/date/DateFind.scala
+++ b/modules/analysis/src/main/scala/docspell/analysis/date/DateFind.scala
@@ -19,7 +19,7 @@ object DateFind {
       .splitToken(text, " \t.,\n\r/".toSet)
       .filter(w => lang != Language.Latvian || w.value != "gada")
       .sliding(3)
-      .filter(_.length == 3)
+      .filter(_.size == 3)
       .flatMap(q =>
         Stream.emits(
           SimpleDate
@@ -28,9 +28,9 @@ object DateFind {
               NerDateLabel(
                 sd.toLocalDate,
                 NerLabel(
-                  text.substring(q.head.begin, q(2).end),
+                  text.substring(q.head.get.begin, q(2).end),
                   NerTag.Date,
-                  q.head.begin,
+                  q.head.get.begin,
                   q(2).end
                 )
               )
diff --git a/modules/analysis/src/main/scala/docspell/analysis/nlp/PipelineCache.scala b/modules/analysis/src/main/scala/docspell/analysis/nlp/PipelineCache.scala
index 99657826..0a34b0b0 100644
--- a/modules/analysis/src/main/scala/docspell/analysis/nlp/PipelineCache.scala
+++ b/modules/analysis/src/main/scala/docspell/analysis/nlp/PipelineCache.scala
@@ -2,9 +2,8 @@ package docspell.analysis.nlp
 
 import scala.concurrent.duration.{Duration => _, _}
 
-import cats.Applicative
+import cats.effect.Ref
 import cats.effect._
-import cats.effect.concurrent.Ref
 import cats.implicits._
 
 import docspell.analysis.NlpSettings
@@ -28,7 +27,7 @@ trait PipelineCache[F[_]] {
 object PipelineCache {
   private[this] val logger = getLogger
 
-  def apply[F[_]: Concurrent: Timer](clearInterval: Duration)(
+  def apply[F[_]: Async](clearInterval: Duration)(
       creator: NlpSettings => Annotator[F],
       release: F[Unit]
   ): F[PipelineCache[F]] =
@@ -38,7 +37,7 @@ object PipelineCache {
       _          <- Logger.log4s(logger).info("Creating nlp pipeline cache")
     } yield new Impl[F](data, creator, cacheClear)
 
-  final private class Impl[F[_]: Sync](
+  final private class Impl[F[_]: Async](
       data: Ref[F, Map[String, Entry[Annotator[F]]]],
       creator: NlpSettings => Annotator[F],
       cacheClear: CacheClearing[F]
@@ -97,20 +96,20 @@ object PipelineCache {
   }
 
   object CacheClearing {
-    def none[F[_]: Applicative]: CacheClearing[F] =
+    def none[F[_]]: CacheClearing[F] =
       new CacheClearing[F] {
         def withCache: Resource[F, Unit] =
           Resource.pure[F, Unit](())
       }
 
-    def create[F[_]: Concurrent: Timer, A](
+    def create[F[_]: Async, A](
         data: Ref[F, Map[String, Entry[A]]],
         interval: Duration,
         release: F[Unit]
     ): F[CacheClearing[F]] =
       for {
         counter  <- Ref.of(0L)
-        cleaning <- Ref.of(None: Option[Fiber[F, Unit]])
+        cleaning <- Ref.of(None: Option[Fiber[F, Throwable, Unit]])
         log = Logger.log4s(logger)
         result <-
           if (interval.millis <= 0)
@@ -135,10 +134,10 @@ object PipelineCache {
   final private class CacheClearingImpl[F[_], A](
       data: Ref[F, Map[String, Entry[A]]],
       counter: Ref[F, Long],
-      cleaningFiber: Ref[F, Option[Fiber[F, Unit]]],
+      cleaningFiber: Ref[F, Option[Fiber[F, Throwable, Unit]]],
       clearInterval: FiniteDuration,
       release: F[Unit]
-  )(implicit T: Timer[F], F: Concurrent[F])
+  )(implicit F: Async[F])
       extends CacheClearing[F] {
     private[this] val log = Logger.log4s[F](logger)
 
@@ -157,8 +156,8 @@ object PipelineCache {
         case None        => ().pure[F]
       }
 
-    private def clearAllLater: F[Fiber[F, Unit]] =
-      F.start(T.sleep(clearInterval) *> clearAll)
+    private def clearAllLater: F[Fiber[F, Throwable, Unit]] =
+      F.start(F.sleep(clearInterval) *> clearAll)
 
     private def logDontClear: F[Unit] =
       log.info("Cancel stanford cache clearing, as it has been used in between.")
diff --git a/modules/analysis/src/test/scala/docspell/analysis/classifier/StanfordTextClassifierSuite.scala b/modules/analysis/src/test/scala/docspell/analysis/classifier/StanfordTextClassifierSuite.scala
index 2e483b2a..fe448f3f 100644
--- a/modules/analysis/src/test/scala/docspell/analysis/classifier/StanfordTextClassifierSuite.scala
+++ b/modules/analysis/src/test/scala/docspell/analysis/classifier/StanfordTextClassifierSuite.scala
@@ -2,12 +2,12 @@ package docspell.analysis.classifier
 
 import java.nio.file.Paths
 
-import scala.concurrent.ExecutionContext
-
 import cats.data.Kleisli
 import cats.data.NonEmptyList
 import cats.effect._
+import cats.effect.unsafe.implicits.global
 import fs2.Stream
+import fs2.io.file.Files
 
 import docspell.analysis.classifier.TextClassifier.Data
 import docspell.common._
@@ -17,8 +17,6 @@ import munit._
 class StanfordTextClassifierSuite extends FunSuite {
   val logger = Logger.log4s[IO](org.log4s.getLogger)
 
-  implicit val CS = IO.contextShift(ExecutionContext.global)
-
   test("learn from data") {
     val cfg = TextClassifierConfig(Paths.get("target"), NonEmptyList.of(Map()))
 
@@ -38,34 +36,30 @@ class StanfordTextClassifierSuite extends FunSuite {
         })
         .covary[IO]
 
-    val modelExists =
-      Blocker[IO].use { blocker =>
-        val classifier = new StanfordTextClassifier[IO](cfg, blocker)
-        classifier.trainClassifier[Boolean](logger, data)(
-          Kleisli(result => File.existsNonEmpty[IO](result.model))
-        )
-      }
+    val modelExists = {
+      val classifier = new StanfordTextClassifier[IO](cfg)
+      classifier.trainClassifier[Boolean](logger, data)(
+        Kleisli(result => File.existsNonEmpty[IO](result.model))
+      )
+    }
     assertEquals(modelExists.unsafeRunSync(), true)
   }
 
   test("run classifier") {
-    val cfg = TextClassifierConfig(Paths.get("target"), NonEmptyList.of(Map()))
-    val things = for {
-      dir     <- File.withTempDir[IO](Paths.get("target"), "testcls")
-      blocker <- Blocker[IO]
-    } yield (dir, blocker)
+    val cfg    = TextClassifierConfig(Paths.get("target"), NonEmptyList.of(Map()))
+    val things = File.withTempDir[IO](Paths.get("target"), "testcls")
 
     things
-      .use { case (dir, blocker) =>
-        val classifier = new StanfordTextClassifier[IO](cfg, blocker)
+      .use { dir =>
+        val classifier = new StanfordTextClassifier[IO](cfg)
 
         val modelFile = dir.resolve("test.ser.gz")
         for {
           _ <-
             LenientUri
               .fromJava(getClass.getResource("/test.ser.gz"))
-              .readURL[IO](4096, blocker)
-              .through(fs2.io.file.writeAll(modelFile, blocker))
+              .readURL[IO](4096)
+              .through(Files[IO].writeAll(modelFile))
               .compile
               .drain
           model = ClassifierModel(modelFile)
diff --git a/modules/analysis/src/test/scala/docspell/analysis/nlp/StanfordNerAnnotatorSuite.scala b/modules/analysis/src/test/scala/docspell/analysis/nlp/StanfordNerAnnotatorSuite.scala
index 4938c45b..e3119467 100644
--- a/modules/analysis/src/test/scala/docspell/analysis/nlp/StanfordNerAnnotatorSuite.scala
+++ b/modules/analysis/src/test/scala/docspell/analysis/nlp/StanfordNerAnnotatorSuite.scala
@@ -3,6 +3,7 @@ package docspell.analysis.nlp
 import java.nio.file.Paths
 
 import cats.effect.IO
+import cats.effect.unsafe.implicits.global
 
 import docspell.analysis.Env
 import docspell.common._
diff --git a/modules/backend/src/main/scala/docspell/backend/BackendApp.scala b/modules/backend/src/main/scala/docspell/backend/BackendApp.scala
index 534eb1ca..eddbd161 100644
--- a/modules/backend/src/main/scala/docspell/backend/BackendApp.scala
+++ b/modules/backend/src/main/scala/docspell/backend/BackendApp.scala
@@ -14,8 +14,8 @@ import docspell.store.queue.JobQueue
 import docspell.store.usertask.UserTaskStore
 
 import emil.javamail.{JavaMailEmil, Settings}
+import org.http4s.blaze.client.BlazeClientBuilder
 import org.http4s.client.Client
-import org.http4s.client.blaze.BlazeClientBuilder
 
 trait BackendApp[F[_]] {
 
@@ -43,12 +43,11 @@ trait BackendApp[F[_]] {
 
 object BackendApp {
 
-  def create[F[_]: ConcurrentEffect: ContextShift](
+  def create[F[_]: Async](
       cfg: Config,
       store: Store[F],
       httpClient: Client[F],
-      ftsClient: FtsClient[F],
-      blocker: Blocker
+      ftsClient: FtsClient[F]
   ): Resource[F, BackendApp[F]] =
     for {
       utStore        <- UserTaskStore(store)
@@ -68,7 +67,7 @@ object BackendApp {
       itemSearchImpl <- OItemSearch(store)
       fulltextImpl   <- OFulltext(itemSearchImpl, ftsClient, store, queue, joexImpl)
       javaEmil =
-        JavaMailEmil(blocker, Settings.defaultSettings.copy(debug = cfg.mailDebug))
+        JavaMailEmil(Settings.defaultSettings.copy(debug = cfg.mailDebug))
       mailImpl         <- OMail(store, javaEmil)
       userTaskImpl     <- OUserTask(utStore, queue, joexImpl)
       folderImpl       <- OFolder(store)
@@ -98,16 +97,15 @@ object BackendApp {
       val clientSettings = clientSettingsImpl
     }
 
-  def apply[F[_]: ConcurrentEffect: ContextShift](
+  def apply[F[_]: Async](
       cfg: Config,
       connectEC: ExecutionContext,
-      httpClientEc: ExecutionContext,
-      blocker: Blocker
+      httpClientEc: ExecutionContext
   )(ftsFactory: Client[F] => Resource[F, FtsClient[F]]): Resource[F, BackendApp[F]] =
     for {
-      store      <- Store.create(cfg.jdbc, connectEC, blocker)
+      store      <- Store.create(cfg.jdbc, connectEC)
       httpClient <- BlazeClientBuilder[F](httpClientEc).resource
       ftsClient  <- ftsFactory(httpClient)
-      backend    <- create(cfg, store, httpClient, ftsClient, blocker)
+      backend    <- create(cfg, store, httpClient, ftsClient)
     } yield backend
 }
diff --git a/modules/backend/src/main/scala/docspell/backend/auth/Login.scala b/modules/backend/src/main/scala/docspell/backend/auth/Login.scala
index 050d99d4..94cfa660 100644
--- a/modules/backend/src/main/scala/docspell/backend/auth/Login.scala
+++ b/modules/backend/src/main/scala/docspell/backend/auth/Login.scala
@@ -69,7 +69,7 @@ object Login {
     def invalidTime: Result = InvalidTime
   }
 
-  def apply[F[_]: Effect](store: Store[F]): Resource[F, Login[F]] =
+  def apply[F[_]: Async](store: Store[F]): Resource[F, Login[F]] =
     Resource.pure[F, Login[F]](new Login[F] {
 
       private val logF = Logger.log4s(logger)
diff --git a/modules/backend/src/main/scala/docspell/backend/ops/OClientSettings.scala b/modules/backend/src/main/scala/docspell/backend/ops/OClientSettings.scala
index 81f2aeec..cf467b89 100644
--- a/modules/backend/src/main/scala/docspell/backend/ops/OClientSettings.scala
+++ b/modules/backend/src/main/scala/docspell/backend/ops/OClientSettings.scala
@@ -1,7 +1,7 @@
 package docspell.backend.ops
 
 import cats.data.OptionT
-import cats.effect.{Effect, Resource}
+import cats.effect.{Async, Resource}
 import cats.implicits._
 
 import docspell.common.AccountId
@@ -25,7 +25,7 @@ trait OClientSettings[F[_]] {
 object OClientSettings {
   private[this] val logger = getLogger
 
-  def apply[F[_]: Effect](store: Store[F]): Resource[F, OClientSettings[F]] =
+  def apply[F[_]: Async](store: Store[F]): Resource[F, OClientSettings[F]] =
     Resource.pure[F, OClientSettings[F]](new OClientSettings[F] {
 
       private def getUserId(account: AccountId): OptionT[F, Ident] =
@@ -58,7 +58,7 @@ object OClientSettings {
             store.transact(RClientSettings.upsert(clientId, userId, data))
           )
           _ <- OptionT.liftF(
-            if (n <= 0) Effect[F].raiseError(new Exception("No rows updated!"))
+            if (n <= 0) Async[F].raiseError(new Exception("No rows updated!"))
             else ().pure[F]
           )
         } yield ()).getOrElse(())
diff --git a/modules/backend/src/main/scala/docspell/backend/ops/OCollective.scala b/modules/backend/src/main/scala/docspell/backend/ops/OCollective.scala
index 5579445e..8510703e 100644
--- a/modules/backend/src/main/scala/docspell/backend/ops/OCollective.scala
+++ b/modules/backend/src/main/scala/docspell/backend/ops/OCollective.scala
@@ -1,6 +1,6 @@
 package docspell.backend.ops
 
-import cats.effect.{Effect, Resource}
+import cats.effect.{Async, Resource}
 import cats.implicits._
 import fs2.Stream
 
@@ -126,7 +126,7 @@ object OCollective {
     }
   }
 
-  def apply[F[_]: Effect](
+  def apply[F[_]: Async](
       store: Store[F],
       uts: UserTaskStore[F],
       queue: JobQueue[F],
diff --git a/modules/backend/src/main/scala/docspell/backend/ops/OCustomFields.scala b/modules/backend/src/main/scala/docspell/backend/ops/OCustomFields.scala
index d4f566b5..4d370ee4 100644
--- a/modules/backend/src/main/scala/docspell/backend/ops/OCustomFields.scala
+++ b/modules/backend/src/main/scala/docspell/backend/ops/OCustomFields.scala
@@ -87,7 +87,7 @@ object OCustomFields {
       collective: Ident
   )
 
-  def apply[F[_]: Effect](
+  def apply[F[_]: Async](
       store: Store[F]
   ): Resource[F, OCustomFields[F]] =
     Resource.pure[F, OCustomFields[F]](new OCustomFields[F] {
diff --git a/modules/backend/src/main/scala/docspell/backend/ops/OEquipment.scala b/modules/backend/src/main/scala/docspell/backend/ops/OEquipment.scala
index 9457e6a6..4dcd1a92 100644
--- a/modules/backend/src/main/scala/docspell/backend/ops/OEquipment.scala
+++ b/modules/backend/src/main/scala/docspell/backend/ops/OEquipment.scala
@@ -1,6 +1,6 @@
 package docspell.backend.ops
 
-import cats.effect.{Effect, Resource}
+import cats.effect.{Async, Resource}
 import cats.implicits._
 
 import docspell.common.{AccountId, Ident}
@@ -22,7 +22,7 @@ trait OEquipment[F[_]] {
 
 object OEquipment {
 
-  def apply[F[_]: Effect](store: Store[F]): Resource[F, OEquipment[F]] =
+  def apply[F[_]: Async](store: Store[F]): Resource[F, OEquipment[F]] =
     Resource.pure[F, OEquipment[F]](new OEquipment[F] {
       def findAll(account: AccountId, nameQuery: Option[String]): F[Vector[REquipment]] =
         store.transact(REquipment.findAll(account.collective, nameQuery, _.name))
diff --git a/modules/backend/src/main/scala/docspell/backend/ops/OFolder.scala b/modules/backend/src/main/scala/docspell/backend/ops/OFolder.scala
index 41576378..d5ac1270 100644
--- a/modules/backend/src/main/scala/docspell/backend/ops/OFolder.scala
+++ b/modules/backend/src/main/scala/docspell/backend/ops/OFolder.scala
@@ -55,7 +55,7 @@ object OFolder {
   type FolderDetail = QFolder.FolderDetail
   val FolderDetail = QFolder.FolderDetail
 
-  def apply[F[_]: Effect](store: Store[F]): Resource[F, OFolder[F]] =
+  def apply[F[_]](store: Store[F]): Resource[F, OFolder[F]] =
     Resource.pure[F, OFolder[F]](new OFolder[F] {
       def findAll(
           account: AccountId,
diff --git a/modules/backend/src/main/scala/docspell/backend/ops/OFulltext.scala b/modules/backend/src/main/scala/docspell/backend/ops/OFulltext.scala
index 73b9a015..1a8e1ebd 100644
--- a/modules/backend/src/main/scala/docspell/backend/ops/OFulltext.scala
+++ b/modules/backend/src/main/scala/docspell/backend/ops/OFulltext.scala
@@ -77,7 +77,7 @@ object OFulltext {
   case class FtsItem(item: ListItem, ftsData: FtsData)
   case class FtsItemWithTags(item: ListItemWithTags, ftsData: FtsData)
 
-  def apply[F[_]: Effect](
+  def apply[F[_]: Async](
       itemSearch: OItemSearch[F],
       fts: FtsClient[F],
       store: Store[F],
diff --git a/modules/backend/src/main/scala/docspell/backend/ops/OItem.scala b/modules/backend/src/main/scala/docspell/backend/ops/OItem.scala
index 0cbf8b44..423949f9 100644
--- a/modules/backend/src/main/scala/docspell/backend/ops/OItem.scala
+++ b/modules/backend/src/main/scala/docspell/backend/ops/OItem.scala
@@ -1,7 +1,7 @@
 package docspell.backend.ops
 
 import cats.data.{NonEmptyList, OptionT}
-import cats.effect.{Effect, Resource}
+import cats.effect.{Async, Resource}
 import cats.implicits._
 
 import docspell.backend.JobFactory
@@ -191,7 +191,7 @@ trait OItem[F[_]] {
 
 object OItem {
 
-  def apply[F[_]: Effect](
+  def apply[F[_]: Async](
       store: Store[F],
       fts: FtsClient[F],
       queue: JobQueue[F],
diff --git a/modules/backend/src/main/scala/docspell/backend/ops/OItemSearch.scala b/modules/backend/src/main/scala/docspell/backend/ops/OItemSearch.scala
index a74e451a..756f7f41 100644
--- a/modules/backend/src/main/scala/docspell/backend/ops/OItemSearch.scala
+++ b/modules/backend/src/main/scala/docspell/backend/ops/OItemSearch.scala
@@ -1,7 +1,7 @@
 package docspell.backend.ops
 
 import cats.data.OptionT
-import cats.effect.{Effect, Resource}
+import cats.effect.{Async, Resource}
 import cats.implicits._
 import fs2.Stream
 
@@ -118,7 +118,7 @@ object OItemSearch {
     val fileId = rs.fileId
   }
 
-  def apply[F[_]: Effect](store: Store[F]): Resource[F, OItemSearch[F]] =
+  def apply[F[_]: Async](store: Store[F]): Resource[F, OItemSearch[F]] =
     Resource.pure[F, OItemSearch[F]](new OItemSearch[F] {
 
       def findItem(id: Ident, collective: Ident): F[Option[ItemData]] =
diff --git a/modules/backend/src/main/scala/docspell/backend/ops/OJoex.scala b/modules/backend/src/main/scala/docspell/backend/ops/OJoex.scala
index a954488b..f0c6dd39 100644
--- a/modules/backend/src/main/scala/docspell/backend/ops/OJoex.scala
+++ b/modules/backend/src/main/scala/docspell/backend/ops/OJoex.scala
@@ -36,7 +36,7 @@ object OJoex {
         } yield cancel.success).getOrElse(false)
     })
 
-  def create[F[_]: ConcurrentEffect](
+  def create[F[_]: Async](
       ec: ExecutionContext,
       store: Store[F]
   ): Resource[F, OJoex[F]] =
diff --git a/modules/backend/src/main/scala/docspell/backend/ops/OMail.scala b/modules/backend/src/main/scala/docspell/backend/ops/OMail.scala
index 86663752..d23086e8 100644
--- a/modules/backend/src/main/scala/docspell/backend/ops/OMail.scala
+++ b/modules/backend/src/main/scala/docspell/backend/ops/OMail.scala
@@ -141,7 +141,7 @@ object OMail {
       )
   }
 
-  def apply[F[_]: Effect](store: Store[F], emil: Emil[F]): Resource[F, OMail[F]] =
+  def apply[F[_]: Async](store: Store[F], emil: Emil[F]): Resource[F, OMail[F]] =
     Resource.pure[F, OMail[F]](new OMail[F] {
       def getSmtpSettings(
           accId: AccountId,
diff --git a/modules/backend/src/main/scala/docspell/backend/ops/ONode.scala b/modules/backend/src/main/scala/docspell/backend/ops/ONode.scala
index b81de589..647bd319 100644
--- a/modules/backend/src/main/scala/docspell/backend/ops/ONode.scala
+++ b/modules/backend/src/main/scala/docspell/backend/ops/ONode.scala
@@ -1,6 +1,6 @@
 package docspell.backend.ops
 
-import cats.effect.{Effect, Resource}
+import cats.effect.{Async, Resource}
 import cats.implicits._
 
 import docspell.common.syntax.all._
@@ -20,7 +20,7 @@ trait ONode[F[_]] {
 object ONode {
   private[this] val logger = getLogger
 
-  def apply[F[_]: Effect](store: Store[F]): Resource[F, ONode[F]] =
+  def apply[F[_]: Async](store: Store[F]): Resource[F, ONode[F]] =
     Resource.pure[F, ONode[F]](new ONode[F] {
 
       def register(appId: Ident, nodeType: NodeType, uri: LenientUri): F[Unit] =
diff --git a/modules/backend/src/main/scala/docspell/backend/ops/OOrganization.scala b/modules/backend/src/main/scala/docspell/backend/ops/OOrganization.scala
index eba07e84..53e690c3 100644
--- a/modules/backend/src/main/scala/docspell/backend/ops/OOrganization.scala
+++ b/modules/backend/src/main/scala/docspell/backend/ops/OOrganization.scala
@@ -1,6 +1,6 @@
 package docspell.backend.ops
 
-import cats.effect.{Effect, Resource}
+import cats.effect.{Async, Resource}
 import cats.implicits._
 
 import docspell.backend.ops.OOrganization._
@@ -49,7 +49,7 @@ object OOrganization {
       contacts: Seq[RContact]
   )
 
-  def apply[F[_]: Effect](store: Store[F]): Resource[F, OOrganization[F]] =
+  def apply[F[_]: Async](store: Store[F]): Resource[F, OOrganization[F]] =
     Resource.pure[F, OOrganization[F]](new OOrganization[F] {
 
       def findAllOrg(
diff --git a/modules/backend/src/main/scala/docspell/backend/ops/OSource.scala b/modules/backend/src/main/scala/docspell/backend/ops/OSource.scala
index cd7f3bda..4e07620a 100644
--- a/modules/backend/src/main/scala/docspell/backend/ops/OSource.scala
+++ b/modules/backend/src/main/scala/docspell/backend/ops/OSource.scala
@@ -1,6 +1,6 @@
 package docspell.backend.ops
 
-import cats.effect.{Effect, Resource}
+import cats.effect.{Async, Resource}
 import cats.implicits._
 
 import docspell.common.{AccountId, Ident}
@@ -22,7 +22,7 @@ trait OSource[F[_]] {
 
 object OSource {
 
-  def apply[F[_]: Effect](store: Store[F]): Resource[F, OSource[F]] =
+  def apply[F[_]: Async](store: Store[F]): Resource[F, OSource[F]] =
     Resource.pure[F, OSource[F]](new OSource[F] {
       def findAll(account: AccountId): F[Vector[SourceData]] =
         store
diff --git a/modules/backend/src/main/scala/docspell/backend/ops/OTag.scala b/modules/backend/src/main/scala/docspell/backend/ops/OTag.scala
index a4e0c937..6531714b 100644
--- a/modules/backend/src/main/scala/docspell/backend/ops/OTag.scala
+++ b/modules/backend/src/main/scala/docspell/backend/ops/OTag.scala
@@ -1,6 +1,6 @@
 package docspell.backend.ops
 
-import cats.effect.{Effect, Resource}
+import cats.effect.{Async, Resource}
 import cats.implicits._
 
 import docspell.common.{AccountId, Ident}
@@ -25,7 +25,7 @@ trait OTag[F[_]] {
 
 object OTag {
 
-  def apply[F[_]: Effect](store: Store[F]): Resource[F, OTag[F]] =
+  def apply[F[_]: Async](store: Store[F]): Resource[F, OTag[F]] =
     Resource.pure[F, OTag[F]](new OTag[F] {
       def findAll(account: AccountId, nameQuery: Option[String]): F[Vector[RTag]] =
         store.transact(RTag.findAll(account.collective, nameQuery, _.name))
diff --git a/modules/backend/src/main/scala/docspell/backend/ops/OUserTask.scala b/modules/backend/src/main/scala/docspell/backend/ops/OUserTask.scala
index ca2816c6..0e3ae2f1 100644
--- a/modules/backend/src/main/scala/docspell/backend/ops/OUserTask.scala
+++ b/modules/backend/src/main/scala/docspell/backend/ops/OUserTask.scala
@@ -62,7 +62,7 @@ trait OUserTask[F[_]] {
 
 object OUserTask {
 
-  def apply[F[_]: Effect](
+  def apply[F[_]: Async](
       store: UserTaskStore[F],
       queue: JobQueue[F],
       joex: OJoex[F]
diff --git a/modules/backend/src/main/scala/docspell/backend/signup/OSignup.scala b/modules/backend/src/main/scala/docspell/backend/signup/OSignup.scala
index 0ea599c7..a2f797b2 100644
--- a/modules/backend/src/main/scala/docspell/backend/signup/OSignup.scala
+++ b/modules/backend/src/main/scala/docspell/backend/signup/OSignup.scala
@@ -1,6 +1,6 @@
 package docspell.backend.signup
 
-import cats.effect.{Effect, Resource}
+import cats.effect.{Async, Resource}
 import cats.implicits._
 
 import docspell.backend.PasswordCrypt
@@ -23,7 +23,7 @@ trait OSignup[F[_]] {
 object OSignup {
   private[this] val logger = getLogger
 
-  def apply[F[_]: Effect](store: Store[F]): Resource[F, OSignup[F]] =
+  def apply[F[_]: Async](store: Store[F]): Resource[F, OSignup[F]] =
     Resource.pure[F, OSignup[F]](new OSignup[F] {
 
       def newInvite(cfg: Config)(password: Password): F[NewInviteResult] =
@@ -35,7 +35,7 @@ object OSignup {
               .transact(RInvitation.insertNew)
               .map(ri => NewInviteResult.success(ri.id))
         else
-          Effect[F].pure(NewInviteResult.invitationClosed)
+          Async[F].pure(NewInviteResult.invitationClosed)
 
       def register(cfg: Config)(data: RegisterData): F[SignupResult] =
         cfg.mode match {
diff --git a/modules/common/src/main/scala/docspell/common/File.scala b/modules/common/src/main/scala/docspell/common/File.scala
index 572291c5..002d92d3 100644
--- a/modules/common/src/main/scala/docspell/common/File.scala
+++ b/modules/common/src/main/scala/docspell/common/File.scala
@@ -1,47 +1,48 @@
 package docspell.common
 
 import java.io.IOException
-import java.nio.charset.StandardCharsets
-import java.nio.file._
 import java.nio.file.attribute.BasicFileAttributes
+import java.nio.file.{Files => JFiles, _}
 import java.util.concurrent.atomic.AtomicInteger
 
 import scala.jdk.CollectionConverters._
 
 import cats.effect._
 import cats.implicits._
-import fs2.Stream
+import fs2.io.file.Files
+import fs2.{Chunk, Stream}
 
 import docspell.common.syntax.all._
 
 import io.circe.Decoder
-
+import scodec.bits.ByteVector
+//TODO use io.fs2.files.Files api
 object File {
 
   def mkDir[F[_]: Sync](dir: Path): F[Path] =
-    Sync[F].delay(Files.createDirectories(dir))
+    Sync[F].blocking(JFiles.createDirectories(dir))
 
   def mkTempDir[F[_]: Sync](parent: Path, prefix: String): F[Path] =
-    mkDir(parent).map(p => Files.createTempDirectory(p, prefix))
+    mkDir(parent).map(p => JFiles.createTempDirectory(p, prefix))
 
   def mkTempFile[F[_]: Sync](
       parent: Path,
       prefix: String,
       suffix: Option[String] = None
   ): F[Path] =
-    mkDir(parent).map(p => Files.createTempFile(p, prefix, suffix.orNull))
+    mkDir(parent).map(p => JFiles.createTempFile(p, prefix, suffix.orNull))
 
   def deleteDirectory[F[_]: Sync](dir: Path): F[Int] =
     Sync[F].delay {
       val count = new AtomicInteger(0)
-      Files.walkFileTree(
+      JFiles.walkFileTree(
         dir,
         new SimpleFileVisitor[Path]() {
           override def visitFile(
               file: Path,
               attrs: BasicFileAttributes
           ): FileVisitResult = {
-            Files.deleteIfExists(file)
+            JFiles.deleteIfExists(file)
             count.incrementAndGet()
             FileVisitResult.CONTINUE
           }
@@ -49,7 +50,7 @@ object File {
             Option(e) match {
               case Some(ex) => throw ex
               case None =>
-                Files.deleteIfExists(dir)
+                JFiles.deleteIfExists(dir)
                 FileVisitResult.CONTINUE
             }
         }
@@ -58,47 +59,57 @@ object File {
     }
 
   def exists[F[_]: Sync](file: Path): F[Boolean] =
-    Sync[F].delay(Files.exists(file))
+    Sync[F].delay(JFiles.exists(file))
 
   def size[F[_]: Sync](file: Path): F[Long] =
-    Sync[F].delay(Files.size(file))
+    Sync[F].delay(JFiles.size(file))
 
   def existsNonEmpty[F[_]: Sync](file: Path, minSize: Long = 0): F[Boolean] =
-    Sync[F].delay(Files.exists(file) && Files.size(file) > minSize)
+    Sync[F].delay(JFiles.exists(file) && JFiles.size(file) > minSize)
 
   def deleteFile[F[_]: Sync](file: Path): F[Unit] =
-    Sync[F].delay(Files.deleteIfExists(file)).map(_ => ())
+    Sync[F].delay(JFiles.deleteIfExists(file)).map(_ => ())
 
   def delete[F[_]: Sync](path: Path): F[Int] =
-    if (Files.isDirectory(path)) deleteDirectory(path)
+    if (JFiles.isDirectory(path)) deleteDirectory(path)
     else deleteFile(path).map(_ => 1)
 
   def withTempDir[F[_]: Sync](parent: Path, prefix: String): Resource[F, Path] =
     Resource.make(mkTempDir(parent, prefix))(p => delete(p).map(_ => ()))
 
-  def listFiles[F[_]: Sync](pred: Path => Boolean, dir: Path): F[List[Path]] =
+  def listJFiles[F[_]: Sync](pred: Path => Boolean, dir: Path): F[List[Path]] =
     Sync[F].delay {
       val javaList =
-        Files.list(dir).filter(p => pred(p)).collect(java.util.stream.Collectors.toList())
+        JFiles
+          .list(dir)
+          .filter(p => pred(p))
+          .collect(java.util.stream.Collectors.toList())
       javaList.asScala.toList.sortBy(_.getFileName.toString)
     }
 
-  def readAll[F[_]: Sync: ContextShift](
+  def readAll[F[_]: Files](
       file: Path,
-      blocker: Blocker,
       chunkSize: Int
   ): Stream[F, Byte] =
-    fs2.io.file.readAll(file, blocker, chunkSize)
+    Files[F].readAll(file, chunkSize)
 
-  def readText[F[_]: Sync: ContextShift](file: Path, blocker: Blocker): F[String] =
-    readAll[F](file, blocker, 8192).through(fs2.text.utf8Decode).compile.foldMonoid
+  def readText[F[_]: Files: Concurrent](file: Path): F[String] =
+    readAll[F](file, 8192).through(fs2.text.utf8Decode).compile.foldMonoid
 
-  def writeString[F[_]: Sync](file: Path, content: String): F[Path] =
-    Sync[F].delay(Files.write(file, content.getBytes(StandardCharsets.UTF_8)))
+  def writeString[F[_]: Files: Concurrent](file: Path, content: String): F[Path] =
+    ByteVector.encodeUtf8(content) match {
+      case Right(bv) =>
+        Stream
+          .chunk(Chunk.byteVector(bv))
+          .through(Files[F].writeAll(file))
+          .compile
+          .drain
+          .map(_ => file)
+      case Left(ex) =>
+        Concurrent[F].raiseError(ex)
+    }
 
-  def readJson[F[_]: Sync: ContextShift, A](file: Path, blocker: Blocker)(implicit
-      d: Decoder[A]
-  ): F[A] =
-    readText[F](file, blocker).map(_.parseJsonAs[A]).rethrow
+  def readJson[F[_]: Async, A](file: Path)(implicit d: Decoder[A]): F[A] =
+    readText[F](file).map(_.parseJsonAs[A]).rethrow
 
 }
diff --git a/modules/common/src/main/scala/docspell/common/LenientUri.scala b/modules/common/src/main/scala/docspell/common/LenientUri.scala
index 6b82e001..4193162f 100644
--- a/modules/common/src/main/scala/docspell/common/LenientUri.scala
+++ b/modules/common/src/main/scala/docspell/common/LenientUri.scala
@@ -6,7 +6,7 @@ import java.net.URLEncoder
 
 import cats.data.NonEmptyList
 import cats.effect.Resource
-import cats.effect.{Blocker, ContextShift, Sync}
+import cats.effect._
 import cats.implicits._
 import fs2.Stream
 
@@ -66,20 +66,17 @@ case class LenientUri(
         )
     }
 
-  def readURL[F[_]: Sync: ContextShift](
-      chunkSize: Int,
-      blocker: Blocker
-  ): Stream[F, Byte] =
+  def readURL[F[_]: Sync](chunkSize: Int): Stream[F, Byte] =
     Stream
       .emit(Either.catchNonFatal(new URL(asString)))
       .covary[F]
       .rethrow
       .flatMap(url =>
-        fs2.io.readInputStream(Sync[F].delay(url.openStream()), chunkSize, blocker, true)
+        fs2.io.readInputStream(Sync[F].delay(url.openStream()), chunkSize, true)
       )
 
-  def readText[F[_]: Sync: ContextShift](chunkSize: Int, blocker: Blocker): F[String] =
-    readURL[F](chunkSize, blocker).through(fs2.text.utf8Decode).compile.foldMonoid
+  def readText[F[_]: Sync](chunkSize: Int): F[String] =
+    readURL[F](chunkSize).through(fs2.text.utf8Decode).compile.foldMonoid
 
   def host: Option[String] =
     authority.map(a =>
diff --git a/modules/common/src/main/scala/docspell/common/Pools.scala b/modules/common/src/main/scala/docspell/common/Pools.scala
index c55ec1c5..c51e781f 100644
--- a/modules/common/src/main/scala/docspell/common/Pools.scala
+++ b/modules/common/src/main/scala/docspell/common/Pools.scala
@@ -2,13 +2,10 @@ package docspell.common
 
 import scala.concurrent.ExecutionContext
 
-import cats.effect._
-
 /** Captures thread pools to use in an application.
   */
 case class Pools(
     connectEC: ExecutionContext,
     httpClientEC: ExecutionContext,
-    blocker: Blocker,
     restEC: ExecutionContext
 )
diff --git a/modules/common/src/main/scala/docspell/common/SystemCommand.scala b/modules/common/src/main/scala/docspell/common/SystemCommand.scala
index 92c644ac..ec6bd3f7 100644
--- a/modules/common/src/main/scala/docspell/common/SystemCommand.scala
+++ b/modules/common/src/main/scala/docspell/common/SystemCommand.scala
@@ -7,7 +7,7 @@ import java.util.concurrent.TimeUnit
 
 import scala.jdk.CollectionConverters._
 
-import cats.effect.{Blocker, ContextShift, Sync}
+import cats.effect._
 import cats.implicits._
 import fs2.{Stream, io, text}
 
@@ -34,9 +34,8 @@ object SystemCommand {
 
   final case class Result(rc: Int, stdout: String, stderr: String)
 
-  def exec[F[_]: Sync: ContextShift](
+  def exec[F[_]: Sync](
       cmd: Config,
-      blocker: Blocker,
       logger: Logger[F],
       wd: Option[Path] = None,
       stdin: Stream[F, Byte] = Stream.empty
@@ -44,8 +43,8 @@ object SystemCommand {
     startProcess(cmd, wd, logger, stdin) { proc =>
       Stream.eval {
         for {
-          _    <- writeToProcess(stdin, proc, blocker)
-          term <- Sync[F].delay(proc.waitFor(cmd.timeout.seconds, TimeUnit.SECONDS))
+          _    <- writeToProcess(stdin, proc)
+          term <- Sync[F].blocking(proc.waitFor(cmd.timeout.seconds, TimeUnit.SECONDS))
           _ <-
             if (term)
               logger.debug(s"Command `${cmd.cmdString}` finished: ${proc.exitValue}")
@@ -55,23 +54,22 @@ object SystemCommand {
               )
           _ <- if (!term) timeoutError(proc, cmd) else Sync[F].pure(())
           out <-
-            if (term) inputStreamToString(proc.getInputStream, blocker)
+            if (term) inputStreamToString(proc.getInputStream)
             else Sync[F].pure("")
           err <-
-            if (term) inputStreamToString(proc.getErrorStream, blocker)
+            if (term) inputStreamToString(proc.getErrorStream)
             else Sync[F].pure("")
         } yield Result(proc.exitValue, out, err)
       }
     }
 
-  def execSuccess[F[_]: Sync: ContextShift](
+  def execSuccess[F[_]: Sync](
       cmd: Config,
-      blocker: Blocker,
       logger: Logger[F],
       wd: Option[Path] = None,
       stdin: Stream[F, Byte] = Stream.empty
   ): Stream[F, Result] =
-    exec(cmd, blocker, logger, wd, stdin).flatMap { r =>
+    exec(cmd, logger, wd, stdin).flatMap { r =>
       if (r.rc != 0)
         Stream.raiseError[F](
           new Exception(
@@ -92,7 +90,7 @@ object SystemCommand {
     val log      = logger.debug(s"Running external command: ${cmd.cmdString}")
     val hasStdin = stdin.take(1).compile.last.map(_.isDefined)
     val proc = log *> hasStdin.flatMap(flag =>
-      Sync[F].delay {
+      Sync[F].blocking {
         val pb = new ProcessBuilder(cmd.toCmd.asJava)
           .redirectInput(if (flag) Redirect.PIPE else Redirect.INHERIT)
           .redirectError(Redirect.PIPE)
@@ -109,11 +107,8 @@ object SystemCommand {
       .flatMap(f)
   }
 
-  private def inputStreamToString[F[_]: Sync: ContextShift](
-      in: InputStream,
-      blocker: Blocker
-  ): F[String] =
-    io.readInputStream(Sync[F].pure(in), 16 * 1024, blocker, closeAfterUse = false)
+  private def inputStreamToString[F[_]: Sync](in: InputStream): F[String] =
+    io.readInputStream(Sync[F].pure(in), 16 * 1024, closeAfterUse = false)
       .through(text.utf8Decode)
       .chunks
       .map(_.toVector.mkString)
@@ -122,18 +117,17 @@ object SystemCommand {
       .last
       .map(_.getOrElse(""))
 
-  private def writeToProcess[F[_]: Sync: ContextShift](
+  private def writeToProcess[F[_]: Sync](
       data: Stream[F, Byte],
-      proc: Process,
-      blocker: Blocker
+      proc: Process
   ): F[Unit] =
     data
-      .through(io.writeOutputStream(Sync[F].delay(proc.getOutputStream), blocker))
+      .through(io.writeOutputStream(Sync[F].blocking(proc.getOutputStream)))
       .compile
       .drain
 
   private def timeoutError[F[_]: Sync](proc: Process, cmd: Config): F[Unit] =
-    Sync[F].delay(proc.destroyForcibly()).attempt *> {
+    Sync[F].blocking(proc.destroyForcibly()).attempt *> {
       Sync[F].raiseError(
         new Exception(
           s"Command `${cmd.cmdString}` timed out (${cmd.timeout.formatExact})"
diff --git a/modules/convert/src/main/scala/docspell/convert/Conversion.scala b/modules/convert/src/main/scala/docspell/convert/Conversion.scala
index 589e9db7..ef67e2af 100644
--- a/modules/convert/src/main/scala/docspell/convert/Conversion.scala
+++ b/modules/convert/src/main/scala/docspell/convert/Conversion.scala
@@ -12,6 +12,8 @@ import docspell.convert.extern._
 import docspell.convert.flexmark.Markdown
 import docspell.files.{ImageSize, TikaMimetype}
 
+import scodec.bits.ByteVector
+
 trait Conversion[F[_]] {
 
   def toPDF[A](dataType: DataType, lang: Language, handler: Handler[F, A])(
@@ -22,10 +24,9 @@ trait Conversion[F[_]] {
 
 object Conversion {
 
-  def create[F[_]: Sync: ContextShift](
+  def create[F[_]: Async](
       cfg: ConvertConfig,
       sanitizeHtml: SanitizeHtml,
-      blocker: Blocker,
       logger: Logger[F]
   ): Resource[F, Conversion[F]] =
     Resource.pure[F, Conversion[F]](new Conversion[F] {
@@ -36,12 +37,12 @@ object Conversion {
         TikaMimetype.resolve(dataType, in).flatMap {
           case MimeType.PdfMatch(_) =>
             OcrMyPdf
-              .toPDF(cfg.ocrmypdf, lang, cfg.chunkSize, blocker, logger)(in, handler)
+              .toPDF(cfg.ocrmypdf, lang, cfg.chunkSize, logger)(in, handler)
 
           case MimeType.HtmlMatch(mt) =>
             val cs = mt.charsetOrUtf8
             WkHtmlPdf
-              .toPDF(cfg.wkhtmlpdf, cfg.chunkSize, cs, sanitizeHtml, blocker, logger)(
+              .toPDF(cfg.wkhtmlpdf, cfg.chunkSize, cs, sanitizeHtml, logger)(
                 in,
                 handler
               )
@@ -50,14 +51,15 @@ object Conversion {
             val cs = mt.charsetOrUtf8
             Markdown.toHtml(in, cfg.markdown, cs).flatMap { html =>
               val bytes = Stream
-                .chunk(Chunk.bytes(html.getBytes(StandardCharsets.UTF_8)))
+                .chunk(
+                  Chunk.byteVector(ByteVector.view(html.getBytes(StandardCharsets.UTF_8)))
+                )
                 .covary[F]
               WkHtmlPdf.toPDF(
                 cfg.wkhtmlpdf,
                 cfg.chunkSize,
                 StandardCharsets.UTF_8,
                 sanitizeHtml,
-                blocker,
                 logger
               )(bytes, handler)
             }
@@ -77,7 +79,7 @@ object Conversion {
                       )
                     )
                 else
-                  Tesseract.toPDF(cfg.tesseract, lang, cfg.chunkSize, blocker, logger)(
+                  Tesseract.toPDF(cfg.tesseract, lang, cfg.chunkSize, logger)(
                     in,
                     handler
                   )
@@ -86,14 +88,14 @@ object Conversion {
                 logger.info(
                   s"Cannot read image when determining size for ${mt.asString}. Converting anyways."
                 ) *>
-                  Tesseract.toPDF(cfg.tesseract, lang, cfg.chunkSize, blocker, logger)(
+                  Tesseract.toPDF(cfg.tesseract, lang, cfg.chunkSize, logger)(
                     in,
                     handler
                   )
             }
 
           case Office(_) =>
-            Unoconv.toPDF(cfg.unoconv, cfg.chunkSize, blocker, logger)(in, handler)
+            Unoconv.toPDF(cfg.unoconv, cfg.chunkSize, logger)(in, handler)
 
           case mt =>
             handler.run(ConversionResult.unsupportedFormat(mt))
diff --git a/modules/convert/src/main/scala/docspell/convert/extern/ExternConv.scala b/modules/convert/src/main/scala/docspell/convert/extern/ExternConv.scala
index e96075c2..d690a9f2 100644
--- a/modules/convert/src/main/scala/docspell/convert/extern/ExternConv.scala
+++ b/modules/convert/src/main/scala/docspell/convert/extern/ExternConv.scala
@@ -4,6 +4,7 @@ import java.nio.file.Path
 
 import cats.effect._
 import cats.implicits._
+import fs2.io.file.Files
 import fs2.{Pipe, Stream}
 
 import docspell.common._
@@ -12,12 +13,11 @@ import docspell.convert.ConversionResult.{Handler, successPdf, successPdfTxt}
 
 private[extern] object ExternConv {
 
-  def toPDF[F[_]: Sync: ContextShift, A](
+  def toPDF[F[_]: Async, A](
       name: String,
       cmdCfg: SystemCommand.Config,
       wd: Path,
       useStdin: Boolean,
-      blocker: Blocker,
       logger: Logger[F],
       reader: (Path, SystemCommand.Result) => F[ConversionResult[F]]
   )(in: Stream[F, Byte], handler: Handler[F, A]): F[A] =
@@ -37,13 +37,12 @@ private[extern] object ExternConv {
 
         val createInput: Pipe[F, Byte, Unit] =
           if (useStdin) _ => Stream.emit(())
-          else storeDataToFile(name, blocker, logger, inFile)
+          else storeDataToFile(name, logger, inFile)
 
         in.through(createInput).flatMap { _ =>
           SystemCommand
             .exec[F](
               sysCfg,
-              blocker,
               logger,
               Some(dir),
               if (useStdin) in
@@ -66,8 +65,7 @@ private[extern] object ExternConv {
           handler.run(ConversionResult.failure(ex))
       }
 
-  def readResult[F[_]: Sync: ContextShift](
-      blocker: Blocker,
+  def readResult[F[_]: Async](
       chunkSize: Int,
       logger: Logger[F]
   )(out: Path, result: SystemCommand.Result): F[ConversionResult[F]] =
@@ -77,15 +75,15 @@ private[extern] object ExternConv {
         File.existsNonEmpty[F](outTxt).flatMap {
           case true =>
             successPdfTxt(
-              File.readAll(out, blocker, chunkSize),
-              File.readText(outTxt, blocker)
+              File.readAll(out, chunkSize),
+              File.readText(outTxt)
             ).pure[F]
           case false =>
-            successPdf(File.readAll(out, blocker, chunkSize)).pure[F]
+            successPdf(File.readAll(out, chunkSize)).pure[F]
         }
       case true =>
         logger.warn(s"Command not successful (rc=${result.rc}), but file exists.") *>
-          successPdf(File.readAll(out, blocker, chunkSize)).pure[F]
+          successPdf(File.readAll(out, chunkSize)).pure[F]
 
       case false =>
         ConversionResult
@@ -95,9 +93,8 @@ private[extern] object ExternConv {
           .pure[F]
     }
 
-  def readResultTesseract[F[_]: Sync: ContextShift](
+  def readResultTesseract[F[_]: Async](
       outPrefix: String,
-      blocker: Blocker,
       chunkSize: Int,
       logger: Logger[F]
   )(out: Path, result: SystemCommand.Result): F[ConversionResult[F]] = {
@@ -106,9 +103,9 @@ private[extern] object ExternConv {
       case true =>
         val outTxt = out.resolveSibling(s"$outPrefix.txt")
         File.exists(outTxt).flatMap { txtExists =>
-          val pdfData = File.readAll(out, blocker, chunkSize)
+          val pdfData = File.readAll(out, chunkSize)
           if (result.rc == 0)
-            if (txtExists) successPdfTxt(pdfData, File.readText(outTxt, blocker)).pure[F]
+            if (txtExists) successPdfTxt(pdfData, File.readText(outTxt)).pure[F]
             else successPdf(pdfData).pure[F]
           else
             logger.warn(s"Command not successful (rc=${result.rc}), but file exists.") *>
@@ -124,9 +121,8 @@ private[extern] object ExternConv {
     }
   }
 
-  private def storeDataToFile[F[_]: Sync: ContextShift](
+  private def storeDataToFile[F[_]: Async](
       name: String,
-      blocker: Blocker,
       logger: Logger[F],
       inFile: Path
   ): Pipe[F, Byte, Unit] =
@@ -134,7 +130,7 @@ private[extern] object ExternConv {
       Stream
         .eval(logger.debug(s"Storing input to file ${inFile} for running $name"))
         .drain ++
-        Stream.eval(storeFile(in, inFile, blocker))
+        Stream.eval(storeFile(in, inFile))
 
   private def logResult[F[_]: Sync](
       name: String,
@@ -144,10 +140,9 @@ private[extern] object ExternConv {
     logger.debug(s"$name stdout: ${result.stdout}") *>
       logger.debug(s"$name stderr: ${result.stderr}")
 
-  private def storeFile[F[_]: Sync: ContextShift](
+  private def storeFile[F[_]: Async](
       in: Stream[F, Byte],
-      target: Path,
-      blocker: Blocker
+      target: Path
   ): F[Unit] =
-    in.through(fs2.io.file.writeAll(target, blocker)).compile.drain
+    in.through(Files[F].writeAll(target)).compile.drain
 }
diff --git a/modules/convert/src/main/scala/docspell/convert/extern/OcrMyPdf.scala b/modules/convert/src/main/scala/docspell/convert/extern/OcrMyPdf.scala
index c57170d8..f89b6f95 100644
--- a/modules/convert/src/main/scala/docspell/convert/extern/OcrMyPdf.scala
+++ b/modules/convert/src/main/scala/docspell/convert/extern/OcrMyPdf.scala
@@ -11,23 +11,21 @@ import docspell.convert.ConversionResult.Handler
 
 object OcrMyPdf {
 
-  def toPDF[F[_]: Sync: ContextShift, A](
+  def toPDF[F[_]: Async, A](
       cfg: OcrMyPdfConfig,
       lang: Language,
       chunkSize: Int,
-      blocker: Blocker,
       logger: Logger[F]
   )(in: Stream[F, Byte], handler: Handler[F, A]): F[A] =
     if (cfg.enabled) {
       val reader: (Path, SystemCommand.Result) => F[ConversionResult[F]] =
-        ExternConv.readResult[F](blocker, chunkSize, logger)
+        ExternConv.readResult[F](chunkSize, logger)
 
       ExternConv.toPDF[F, A](
         "ocrmypdf",
         cfg.command.replace(Map("{{lang}}" -> lang.iso3)),
         cfg.workingDir,
         false,
-        blocker,
         logger,
         reader
       )(in, handler)
diff --git a/modules/convert/src/main/scala/docspell/convert/extern/Tesseract.scala b/modules/convert/src/main/scala/docspell/convert/extern/Tesseract.scala
index 90fea777..c7329827 100644
--- a/modules/convert/src/main/scala/docspell/convert/extern/Tesseract.scala
+++ b/modules/convert/src/main/scala/docspell/convert/extern/Tesseract.scala
@@ -11,23 +11,21 @@ import docspell.convert.ConversionResult.Handler
 
 object Tesseract {
 
-  def toPDF[F[_]: Sync: ContextShift, A](
+  def toPDF[F[_]: Async, A](
       cfg: TesseractConfig,
       lang: Language,
       chunkSize: Int,
-      blocker: Blocker,
       logger: Logger[F]
   )(in: Stream[F, Byte], handler: Handler[F, A]): F[A] = {
     val outBase = cfg.command.args.tail.headOption.getOrElse("out")
     val reader: (Path, SystemCommand.Result) => F[ConversionResult[F]] =
-      ExternConv.readResultTesseract[F](outBase, blocker, chunkSize, logger)
+      ExternConv.readResultTesseract[F](outBase, chunkSize, logger)
 
     ExternConv.toPDF[F, A](
       "tesseract",
       cfg.command.replace(Map("{{lang}}" -> lang.iso3)),
       cfg.workingDir,
       false,
-      blocker,
       logger,
       reader
     )(in, handler)
diff --git a/modules/convert/src/main/scala/docspell/convert/extern/Unoconv.scala b/modules/convert/src/main/scala/docspell/convert/extern/Unoconv.scala
index b27609aa..c907126f 100644
--- a/modules/convert/src/main/scala/docspell/convert/extern/Unoconv.scala
+++ b/modules/convert/src/main/scala/docspell/convert/extern/Unoconv.scala
@@ -11,21 +11,19 @@ import docspell.convert.ConversionResult.Handler
 
 object Unoconv {
 
-  def toPDF[F[_]: Sync: ContextShift, A](
+  def toPDF[F[_]: Async, A](
       cfg: UnoconvConfig,
       chunkSize: Int,
-      blocker: Blocker,
       logger: Logger[F]
   )(in: Stream[F, Byte], handler: Handler[F, A]): F[A] = {
     val reader: (Path, SystemCommand.Result) => F[ConversionResult[F]] =
-      ExternConv.readResult[F](blocker, chunkSize, logger)
+      ExternConv.readResult[F](chunkSize, logger)
 
     ExternConv.toPDF[F, A](
       "unoconv",
       cfg.command,
       cfg.workingDir,
       false,
-      blocker,
       logger,
       reader
     )(
diff --git a/modules/convert/src/main/scala/docspell/convert/extern/WkHtmlPdf.scala b/modules/convert/src/main/scala/docspell/convert/extern/WkHtmlPdf.scala
index cf7d0678..f48d3f67 100644
--- a/modules/convert/src/main/scala/docspell/convert/extern/WkHtmlPdf.scala
+++ b/modules/convert/src/main/scala/docspell/convert/extern/WkHtmlPdf.scala
@@ -13,16 +13,15 @@ import docspell.convert.{ConversionResult, SanitizeHtml}
 
 object WkHtmlPdf {
 
-  def toPDF[F[_]: Sync: ContextShift, A](
+  def toPDF[F[_]: Async, A](
       cfg: WkHtmlPdfConfig,
       chunkSize: Int,
       charset: Charset,
       sanitizeHtml: SanitizeHtml,
-      blocker: Blocker,
       logger: Logger[F]
   )(in: Stream[F, Byte], handler: Handler[F, A]): F[A] = {
     val reader: (Path, SystemCommand.Result) => F[ConversionResult[F]] =
-      ExternConv.readResult[F](blocker, chunkSize, logger)
+      ExternConv.readResult[F](chunkSize, logger)
 
     val cmdCfg = cfg.command.replace(Map("{{encoding}}" -> charset.name()))
 
@@ -40,7 +39,7 @@ object WkHtmlPdf {
     )
 
     ExternConv
-      .toPDF[F, A]("wkhtmltopdf", cmdCfg, cfg.workingDir, true, blocker, logger, reader)(
+      .toPDF[F, A]("wkhtmltopdf", cmdCfg, cfg.workingDir, true, logger, reader)(
         inSane,
         handler
       )
diff --git a/modules/convert/src/test/scala/docspell/convert/ConversionTest.scala b/modules/convert/src/test/scala/docspell/convert/ConversionTest.scala
index 8528d25f..908016d2 100644
--- a/modules/convert/src/test/scala/docspell/convert/ConversionTest.scala
+++ b/modules/convert/src/test/scala/docspell/convert/ConversionTest.scala
@@ -4,6 +4,7 @@ import java.nio.file.Paths
 
 import cats.data.Kleisli
 import cats.effect.IO
+import cats.effect.unsafe.implicits.global
 import cats.implicits._
 import fs2.Stream
 
@@ -12,13 +13,11 @@ import docspell.convert.ConversionResult.Handler
 import docspell.convert.extern.OcrMyPdfConfig
 import docspell.convert.extern.{TesseractConfig, UnoconvConfig, WkHtmlPdfConfig}
 import docspell.convert.flexmark.MarkdownConfig
-import docspell.files.{ExampleFiles, TestFiles}
+import docspell.files.ExampleFiles
 
 import munit._
 
 class ConversionTest extends FunSuite with FileChecks {
-  val blocker     = TestFiles.blocker
-  implicit val CS = TestFiles.CS
 
   val logger = Logger.log4s[IO](org.log4s.getLogger)
   val target = Paths.get("target")
@@ -73,7 +72,7 @@ class ConversionTest extends FunSuite with FileChecks {
   )
 
   val conversion =
-    Conversion.create[IO](convertConfig, SanitizeHtml.none, blocker, logger)
+    Conversion.create[IO](convertConfig, SanitizeHtml.none, logger)
 
   val bombs = List(
     ExampleFiles.bombs_20K_gray_jpeg,
@@ -167,7 +166,7 @@ class ConversionTest extends FunSuite with FileChecks {
       .covary[IO]
       .zipWithIndex
       .evalMap({ case (uri, index) =>
-        val load     = uri.readURL[IO](8192, blocker)
+        val load     = uri.readURL[IO](8192)
         val dataType = DataType.filename(uri.path.segments.last)
         logger.info(s"Processing file ${uri.path.asString}") *>
           conv.toPDF(dataType, Language.German, handler(index))(load)
diff --git a/modules/convert/src/test/scala/docspell/convert/FileChecks.scala b/modules/convert/src/test/scala/docspell/convert/FileChecks.scala
index fe340b6c..07c171fc 100644
--- a/modules/convert/src/test/scala/docspell/convert/FileChecks.scala
+++ b/modules/convert/src/test/scala/docspell/convert/FileChecks.scala
@@ -5,6 +5,7 @@ import java.nio.file.{Files, Path}
 
 import cats.data.Kleisli
 import cats.effect.IO
+import cats.effect.unsafe.implicits.global
 import fs2.{Pipe, Stream}
 
 import docspell.common.MimeType
diff --git a/modules/convert/src/test/scala/docspell/convert/extern/ExternConvTest.scala b/modules/convert/src/test/scala/docspell/convert/extern/ExternConvTest.scala
index 305fc2c1..7dbf386e 100644
--- a/modules/convert/src/test/scala/docspell/convert/extern/ExternConvTest.scala
+++ b/modules/convert/src/test/scala/docspell/convert/extern/ExternConvTest.scala
@@ -4,19 +4,18 @@ import java.nio.charset.StandardCharsets
 import java.nio.file.{Path, Paths}
 
 import cats.effect._
+import cats.effect.unsafe.implicits.global
 
 import docspell.common._
 import docspell.convert._
-import docspell.files.{ExampleFiles, TestFiles}
+import docspell.files.ExampleFiles
 
 import munit._
 
 class ExternConvTest extends FunSuite with FileChecks {
-  val blocker     = TestFiles.blocker
-  implicit val CS = TestFiles.CS
-  val utf8        = StandardCharsets.UTF_8
-  val logger      = Logger.log4s[IO](org.log4s.getLogger)
-  val target      = Paths.get("target")
+  val utf8   = StandardCharsets.UTF_8
+  val logger = Logger.log4s[IO](org.log4s.getLogger)
+  val target = Paths.get("target")
 
   test("convert html to pdf") {
     val cfg = SystemCommand.Config(
@@ -32,8 +31,8 @@ class ExternConvTest extends FunSuite with FileChecks {
           val wkCfg = WkHtmlPdfConfig(cfg, target)
           val p =
             WkHtmlPdf
-              .toPDF[IO, Path](wkCfg, 8192, utf8, SanitizeHtml.none, blocker, logger)(
-                ExampleFiles.letter_de_html.readURL[IO](8192, blocker),
+              .toPDF[IO, Path](wkCfg, 8192, utf8, SanitizeHtml.none, logger)(
+                ExampleFiles.letter_de_html.readURL[IO](8192),
                 storePdfHandler(dir.resolve("test.pdf"))
               )
               .unsafeRunSync()
@@ -59,8 +58,8 @@ class ExternConvTest extends FunSuite with FileChecks {
           val ucCfg = UnoconvConfig(cfg, target)
           val p =
             Unoconv
-              .toPDF[IO, Path](ucCfg, 8192, blocker, logger)(
-                ExampleFiles.examples_sample_docx.readURL[IO](8192, blocker),
+              .toPDF[IO, Path](ucCfg, 8192, logger)(
+                ExampleFiles.examples_sample_docx.readURL[IO](8192),
                 storePdfHandler(dir.resolve("test.pdf"))
               )
               .unsafeRunSync()
@@ -85,8 +84,8 @@ class ExternConvTest extends FunSuite with FileChecks {
           val tessCfg = TesseractConfig(cfg, target)
           val (pdf, txt) =
             Tesseract
-              .toPDF[IO, (Path, Path)](tessCfg, Language.German, 8192, blocker, logger)(
-                ExampleFiles.camera_letter_en_jpg.readURL[IO](8192, blocker),
+              .toPDF[IO, (Path, Path)](tessCfg, Language.German, 8192, logger)(
+                ExampleFiles.camera_letter_en_jpg.readURL[IO](8192),
                 storePdfTxtHandler(dir.resolve("test.pdf"), dir.resolve("test.txt"))
               )
               .unsafeRunSync()
diff --git a/modules/extract/src/main/scala/docspell/extract/Extraction.scala b/modules/extract/src/main/scala/docspell/extract/Extraction.scala
index 2507c119..3be2537d 100644
--- a/modules/extract/src/main/scala/docspell/extract/Extraction.scala
+++ b/modules/extract/src/main/scala/docspell/extract/Extraction.scala
@@ -25,8 +25,7 @@ trait Extraction[F[_]] {
 
 object Extraction {
 
-  def create[F[_]: Sync: ContextShift](
-      blocker: Blocker,
+  def create[F[_]: Async](
       logger: Logger[F],
       cfg: ExtractConfig
   ): Extraction[F] =
@@ -39,7 +38,7 @@ object Extraction {
         TikaMimetype.resolve(dataType, data).flatMap {
           case MimeType.PdfMatch(_) =>
             PdfExtract
-              .get(data, blocker, lang, cfg.pdf.minTextLen, cfg.ocr, logger)
+              .get(data, lang, cfg.pdf.minTextLen, cfg.ocr, logger)
               .map(ExtractResult.fromEitherResult)
 
           case PoiType(mt) =>
@@ -59,7 +58,7 @@ object Extraction {
 
           case OcrType(mt) =>
             val doExtract = TextExtract
-              .extractOCR(data, blocker, logger, lang.iso3, cfg.ocr)
+              .extractOCR(data, logger, lang.iso3, cfg.ocr)
               .compile
               .lastOrError
               .map(_.value)
diff --git a/modules/extract/src/main/scala/docspell/extract/PdfExtract.scala b/modules/extract/src/main/scala/docspell/extract/PdfExtract.scala
index 4189c510..52cede85 100644
--- a/modules/extract/src/main/scala/docspell/extract/PdfExtract.scala
+++ b/modules/extract/src/main/scala/docspell/extract/PdfExtract.scala
@@ -17,9 +17,8 @@ object PdfExtract {
       Result(t._1, t._2)
   }
 
-  def get[F[_]: Sync: ContextShift](
+  def get[F[_]: Async](
       in: Stream[F, Byte],
-      blocker: Blocker,
       lang: Language,
       stripMinLen: Int,
       ocrCfg: OcrConfig,
@@ -27,7 +26,7 @@ object PdfExtract {
   ): F[Either[Throwable, Result]] = {
 
     val runOcr =
-      TextExtract.extractOCR(in, blocker, logger, lang.iso3, ocrCfg).compile.lastOrError
+      TextExtract.extractOCR(in, logger, lang.iso3, ocrCfg).compile.lastOrError
 
     def chooseResult(ocrStr: Text, strippedRes: (Text, Option[PdfMetaData])) =
       if (ocrStr.length > strippedRes._1.length)
diff --git a/modules/extract/src/main/scala/docspell/extract/ocr/Ocr.scala b/modules/extract/src/main/scala/docspell/extract/ocr/Ocr.scala
index bc39f94a..6a476697 100644
--- a/modules/extract/src/main/scala/docspell/extract/ocr/Ocr.scala
+++ b/modules/extract/src/main/scala/docspell/extract/ocr/Ocr.scala
@@ -2,7 +2,7 @@ package docspell.extract.ocr
 
 import java.nio.file.Path
 
-import cats.effect.{Blocker, ContextShift, Sync}
+import cats.effect._
 import fs2.Stream
 
 import docspell.common._
@@ -11,16 +11,15 @@ object Ocr {
 
   /** Extract the text of all pages in the given pdf file.
     */
-  def extractPdf[F[_]: Sync: ContextShift](
+  def extractPdf[F[_]: Async](
       pdf: Stream[F, Byte],
-      blocker: Blocker,
       logger: Logger[F],
       lang: String,
       config: OcrConfig
   ): F[Option[String]] =
     File.withTempDir(config.ghostscript.workingDir, "extractpdf").use { wd =>
-      runGhostscript(pdf, config, wd, blocker, logger)
-        .flatMap(tmpImg => runTesseractFile(tmpImg, blocker, logger, lang, config))
+      runGhostscript(pdf, config, wd, logger)
+        .flatMap(tmpImg => runTesseractFile(tmpImg, logger, lang, config))
         .fold1(_ + "\n\n\n" + _)
         .compile
         .last
@@ -28,47 +27,43 @@ object Ocr {
 
   /** Extract the text from the given image file
     */
-  def extractImage[F[_]: Sync: ContextShift](
+  def extractImage[F[_]: Async](
       img: Stream[F, Byte],
-      blocker: Blocker,
       logger: Logger[F],
       lang: String,
       config: OcrConfig
   ): Stream[F, String] =
-    runTesseractStdin(img, blocker, logger, lang, config)
+    runTesseractStdin(img, logger, lang, config)
 
-  def extractPdFFile[F[_]: Sync: ContextShift](
+  def extractPdFFile[F[_]: Async](
       pdf: Path,
-      blocker: Blocker,
       logger: Logger[F],
       lang: String,
       config: OcrConfig
   ): F[Option[String]] =
     File.withTempDir(config.ghostscript.workingDir, "extractpdf").use { wd =>
-      runGhostscriptFile(pdf, config.ghostscript.command, wd, blocker, logger)
-        .flatMap(tif => runTesseractFile(tif, blocker, logger, lang, config))
+      runGhostscriptFile(pdf, config.ghostscript.command, wd, logger)
+        .flatMap(tif => runTesseractFile(tif, logger, lang, config))
         .fold1(_ + "\n\n\n" + _)
         .compile
         .last
     }
 
-  def extractImageFile[F[_]: Sync: ContextShift](
+  def extractImageFile[F[_]: Async](
       img: Path,
-      blocker: Blocker,
       logger: Logger[F],
       lang: String,
       config: OcrConfig
   ): Stream[F, String] =
-    runTesseractFile(img, blocker, logger, lang, config)
+    runTesseractFile(img, logger, lang, config)
 
   /** Run ghostscript to extract all pdf pages into tiff files. The
     * files are stored to a temporary location on disk and returned.
     */
-  private[extract] def runGhostscript[F[_]: Sync: ContextShift](
+  private[extract] def runGhostscript[F[_]: Async](
       pdf: Stream[F, Byte],
       cfg: OcrConfig,
       wd: Path,
-      blocker: Blocker,
       logger: Logger[F]
   ): Stream[F, Path] = {
     val xargs =
@@ -84,19 +79,18 @@ object Ocr {
         )
       )
     SystemCommand
-      .execSuccess(cmd, blocker, logger, wd = Some(wd), stdin = pdf)
-      .evalMap(_ => File.listFiles(pathEndsWith(".tif"), wd))
+      .execSuccess(cmd, logger, wd = Some(wd), stdin = pdf)
+      .evalMap(_ => File.listJFiles(pathEndsWith(".tif"), wd))
       .flatMap(fs => Stream.emits(fs))
   }
 
   /** Run ghostscript to extract all pdf pages into tiff files. The
     * files are stored to a temporary location on disk and returned.
     */
-  private[extract] def runGhostscriptFile[F[_]: Sync: ContextShift](
+  private[extract] def runGhostscriptFile[F[_]: Async](
       pdf: Path,
       ghostscript: SystemCommand.Config,
       wd: Path,
-      blocker: Blocker,
       logger: Logger[F]
   ): Stream[F, Path] = {
     val cmd = ghostscript.replace(
@@ -106,8 +100,8 @@ object Ocr {
       )
     )
     SystemCommand
-      .execSuccess[F](cmd, blocker, logger, wd = Some(wd))
-      .evalMap(_ => File.listFiles(pathEndsWith(".tif"), wd))
+      .execSuccess[F](cmd, logger, wd = Some(wd))
+      .evalMap(_ => File.listJFiles(pathEndsWith(".tif"), wd))
       .flatMap(fs => Stream.emits(fs))
   }
 
@@ -117,11 +111,10 @@ object Ocr {
   /** Run unpaper to optimize the image for ocr. The
     * files are stored to a temporary location on disk and returned.
     */
-  private[extract] def runUnpaperFile[F[_]: Sync: ContextShift](
+  private[extract] def runUnpaperFile[F[_]: Async](
       img: Path,
       unpaper: SystemCommand.Config,
       wd: Path,
-      blocker: Blocker,
       logger: Logger[F]
   ): Stream[F, Path] = {
     val targetFile = img.resolveSibling("u-" + img.getFileName.toString).toAbsolutePath
@@ -132,7 +125,7 @@ object Ocr {
       )
     )
     SystemCommand
-      .execSuccess[F](cmd, blocker, logger, wd = Some(wd))
+      .execSuccess[F](cmd, logger, wd = Some(wd))
       .map(_ => targetFile)
       .handleErrorWith { th =>
         logger
@@ -146,39 +139,36 @@ object Ocr {
   /** Run tesseract on the given image file and return the extracted
     * text.
     */
-  private[extract] def runTesseractFile[F[_]: Sync: ContextShift](
+  private[extract] def runTesseractFile[F[_]: Async](
       img: Path,
-      blocker: Blocker,
       logger: Logger[F],
       lang: String,
       config: OcrConfig
   ): Stream[F, String] =
     // tesseract cannot cope with absolute filenames
     // so use the parent as working dir
-    runUnpaperFile(img, config.unpaper.command, img.getParent, blocker, logger).flatMap {
-      uimg =>
-        val cmd = config.tesseract.command
-          .replace(
-            Map("{{file}}" -> uimg.getFileName.toString, "{{lang}}" -> fixLanguage(lang))
-          )
-        SystemCommand
-          .execSuccess[F](cmd, blocker, logger, wd = Some(uimg.getParent))
-          .map(_.stdout)
+    runUnpaperFile(img, config.unpaper.command, img.getParent, logger).flatMap { uimg =>
+      val cmd = config.tesseract.command
+        .replace(
+          Map("{{file}}" -> uimg.getFileName.toString, "{{lang}}" -> fixLanguage(lang))
+        )
+      SystemCommand
+        .execSuccess[F](cmd, logger, wd = Some(uimg.getParent))
+        .map(_.stdout)
     }
 
   /** Run tesseract on the given image file and return the extracted
     * text.
     */
-  private[extract] def runTesseractStdin[F[_]: Sync: ContextShift](
+  private[extract] def runTesseractStdin[F[_]: Async](
       img: Stream[F, Byte],
-      blocker: Blocker,
       logger: Logger[F],
       lang: String,
       config: OcrConfig
   ): Stream[F, String] = {
     val cmd = config.tesseract.command
       .replace(Map("{{file}}" -> "stdin", "{{lang}}" -> fixLanguage(lang)))
-    SystemCommand.execSuccess(cmd, blocker, logger, stdin = img).map(_.stdout)
+    SystemCommand.execSuccess(cmd, logger, stdin = img).map(_.stdout)
   }
 
   private def fixLanguage(lang: String): String =
diff --git a/modules/extract/src/main/scala/docspell/extract/ocr/TextExtract.scala b/modules/extract/src/main/scala/docspell/extract/ocr/TextExtract.scala
index afc0df7b..0a390473 100644
--- a/modules/extract/src/main/scala/docspell/extract/ocr/TextExtract.scala
+++ b/modules/extract/src/main/scala/docspell/extract/ocr/TextExtract.scala
@@ -1,6 +1,6 @@
 package docspell.extract.ocr
 
-import cats.effect.{Blocker, ContextShift, Sync}
+import cats.effect._
 import fs2.Stream
 
 import docspell.common._
@@ -9,18 +9,16 @@ import docspell.files._
 
 object TextExtract {
 
-  def extract[F[_]: Sync: ContextShift](
+  def extract[F[_]: Async](
       in: Stream[F, Byte],
-      blocker: Blocker,
       logger: Logger[F],
       lang: String,
       config: OcrConfig
   ): Stream[F, Text] =
-    extractOCR(in, blocker, logger, lang, config)
+    extractOCR(in, logger, lang, config)
 
-  def extractOCR[F[_]: Sync: ContextShift](
+  def extractOCR[F[_]: Async](
       in: Stream[F, Byte],
-      blocker: Blocker,
       logger: Logger[F],
       lang: String,
       config: OcrConfig
@@ -29,10 +27,10 @@ object TextExtract {
       .eval(TikaMimetype.detect(in, MimeTypeHint.none))
       .flatMap({
         case MimeType.pdf =>
-          Stream.eval(Ocr.extractPdf(in, blocker, logger, lang, config)).unNoneTerminate
+          Stream.eval(Ocr.extractPdf(in, logger, lang, config)).unNoneTerminate
 
         case mt if mt.primary == "image" =>
-          Ocr.extractImage(in, blocker, logger, lang, config)
+          Ocr.extractImage(in, logger, lang, config)
 
         case mt =>
           raiseError(s"File `$mt` not supported")
diff --git a/modules/extract/src/main/scala/docspell/extract/pdfbox/PdfboxPreview.scala b/modules/extract/src/main/scala/docspell/extract/pdfbox/PdfboxPreview.scala
index 226c6e82..18c4aae1 100644
--- a/modules/extract/src/main/scala/docspell/extract/pdfbox/PdfboxPreview.scala
+++ b/modules/extract/src/main/scala/docspell/extract/pdfbox/PdfboxPreview.scala
@@ -12,6 +12,7 @@ import fs2.Stream
 import org.apache.commons.io.output.ByteArrayOutputStream
 import org.apache.pdfbox.pdmodel.PDDocument
 import org.apache.pdfbox.rendering.PDFRenderer
+import scodec.bits.ByteVector
 
 trait PdfboxPreview[F[_]] {
 
@@ -50,7 +51,7 @@ object PdfboxPreview {
   private def pngStream[F[_]](img: RenderedImage): Stream[F, Byte] = {
     val out = new ByteArrayOutputStream()
     ImageIO.write(img, "PNG", out)
-    Stream.chunk(Chunk.bytes(out.toByteArray()))
+    Stream.chunk(Chunk.byteVector(ByteVector.view(out.toByteArray())))
   }
 
 }
diff --git a/modules/extract/src/test/scala/docspell/extract/ocr/TextExtractionSuite.scala b/modules/extract/src/test/scala/docspell/extract/ocr/TextExtractionSuite.scala
index c074a02d..f6dafa8d 100644
--- a/modules/extract/src/test/scala/docspell/extract/ocr/TextExtractionSuite.scala
+++ b/modules/extract/src/test/scala/docspell/extract/ocr/TextExtractionSuite.scala
@@ -1,6 +1,7 @@
 package docspell.extract.ocr
 
 import cats.effect.IO
+import cats.effect.unsafe.implicits.global
 
 import docspell.common.Logger
 import docspell.files.TestFiles
@@ -14,7 +15,7 @@ class TextExtractionSuite extends FunSuite {
 
   test("extract english pdf".ignore) {
     val text = TextExtract
-      .extract[IO](letterSourceEN, blocker, logger, "eng", OcrConfig.default)
+      .extract[IO](letterSourceEN, logger, "eng", OcrConfig.default)
       .compile
       .lastOrError
       .unsafeRunSync()
@@ -24,7 +25,7 @@ class TextExtractionSuite extends FunSuite {
   test("extract german pdf".ignore) {
     val expect = TestFiles.letterDEText
     val extract = TextExtract
-      .extract[IO](letterSourceDE, blocker, logger, "deu", OcrConfig.default)
+      .extract[IO](letterSourceDE, logger, "deu", OcrConfig.default)
       .compile
       .lastOrError
       .unsafeRunSync()
diff --git a/modules/extract/src/test/scala/docspell/extract/odf/OdfExtractTest.scala b/modules/extract/src/test/scala/docspell/extract/odf/OdfExtractTest.scala
index 7d3a172f..df79d4a1 100644
--- a/modules/extract/src/test/scala/docspell/extract/odf/OdfExtractTest.scala
+++ b/modules/extract/src/test/scala/docspell/extract/odf/OdfExtractTest.scala
@@ -1,14 +1,13 @@
 package docspell.extract.odf
 
 import cats.effect._
+import cats.effect.unsafe.implicits.global
 
-import docspell.files.{ExampleFiles, TestFiles}
+import docspell.files.ExampleFiles
 
 import munit._
 
 class OdfExtractTest extends FunSuite {
-  val blocker     = TestFiles.blocker
-  implicit val CS = TestFiles.CS
 
   val files = List(
     ExampleFiles.examples_sample_odt -> 6372,
@@ -21,7 +20,7 @@ class OdfExtractTest extends FunSuite {
       val str1 = OdfExtract.get(is).fold(throw _, identity)
       assertEquals(str1.length, len)
 
-      val data = file.readURL[IO](8192, blocker)
+      val data = file.readURL[IO](8192)
       val str2 = OdfExtract.get[IO](data).unsafeRunSync().fold(throw _, identity)
       assertEquals(str2, str1)
     }
diff --git a/modules/extract/src/test/scala/docspell/extract/pdfbox/PdfboxExtractTest.scala b/modules/extract/src/test/scala/docspell/extract/pdfbox/PdfboxExtractTest.scala
index fa37ec4a..54b736fb 100644
--- a/modules/extract/src/test/scala/docspell/extract/pdfbox/PdfboxExtractTest.scala
+++ b/modules/extract/src/test/scala/docspell/extract/pdfbox/PdfboxExtractTest.scala
@@ -1,14 +1,13 @@
 package docspell.extract.pdfbox
 
 import cats.effect._
+import cats.effect.unsafe.implicits.global
 
 import docspell.files.{ExampleFiles, TestFiles}
 
 import munit._
 
 class PdfboxExtractTest extends FunSuite {
-  val blocker     = TestFiles.blocker
-  implicit val CS = TestFiles.CS
 
   val textPDFs = List(
     ExampleFiles.letter_de_pdf -> TestFiles.letterDEText,
@@ -27,7 +26,7 @@ class PdfboxExtractTest extends FunSuite {
 
   test("extract text from text PDFs via Stream") {
     textPDFs.foreach { case (file, txt) =>
-      val data     = file.readURL[IO](8192, blocker)
+      val data     = file.readURL[IO](8192)
       val str      = PdfboxExtract.getText(data).unsafeRunSync().fold(throw _, identity)
       val received = removeFormatting(str.value)
       val expect   = removeFormatting(txt)
diff --git a/modules/extract/src/test/scala/docspell/extract/pdfbox/PdfboxPreviewTest.scala b/modules/extract/src/test/scala/docspell/extract/pdfbox/PdfboxPreviewTest.scala
index d1594de6..0389152e 100644
--- a/modules/extract/src/test/scala/docspell/extract/pdfbox/PdfboxPreviewTest.scala
+++ b/modules/extract/src/test/scala/docspell/extract/pdfbox/PdfboxPreviewTest.scala
@@ -3,15 +3,15 @@ package docspell.extract.pdfbox
 import java.nio.file.Path
 
 import cats.effect._
+import cats.effect.unsafe.implicits.global
 import fs2.Stream
+import fs2.io.file.Files
 
-import docspell.files.{ExampleFiles, TestFiles}
+import docspell.files.ExampleFiles
 
 import munit._
 
 class PdfboxPreviewTest extends FunSuite {
-  val blocker     = TestFiles.blocker
-  implicit val CS = TestFiles.CS
 
   val testPDFs = List(
     ExampleFiles.letter_de_pdf     -> "7d98be75b239816d6c751b3f3c56118ebf1a4632c43baf35a68a662f9d595ab8",
@@ -21,7 +21,7 @@ class PdfboxPreviewTest extends FunSuite {
 
   test("extract first page image from PDFs".flaky) {
     testPDFs.foreach { case (file, checksum) =>
-      val data = file.readURL[IO](8192, blocker)
+      val data = file.readURL[IO](8192)
       val sha256out =
         Stream
           .eval(PdfboxPreview[IO](PreviewConfig(48)))
@@ -42,7 +42,7 @@ class PdfboxPreviewTest extends FunSuite {
   def writeToFile(data: Stream[IO, Byte], file: Path): IO[Unit] =
     data
       .through(
-        fs2.io.file.writeAll(file, blocker)
+        Files[IO].writeAll(file)
       )
       .compile
       .drain
diff --git a/modules/extract/src/test/scala/docspell/extract/poi/PoiExtractTest.scala b/modules/extract/src/test/scala/docspell/extract/poi/PoiExtractTest.scala
index 336f54d4..69be2e4d 100644
--- a/modules/extract/src/test/scala/docspell/extract/poi/PoiExtractTest.scala
+++ b/modules/extract/src/test/scala/docspell/extract/poi/PoiExtractTest.scala
@@ -1,15 +1,14 @@
 package docspell.extract.poi
 
 import cats.effect._
+import cats.effect.unsafe.implicits.global
 
 import docspell.common.MimeTypeHint
-import docspell.files.{ExampleFiles, TestFiles}
+import docspell.files.ExampleFiles
 
 import munit._
 
 class PoiExtractTest extends FunSuite {
-  val blocker     = TestFiles.blocker
-  implicit val CS = TestFiles.CS
 
   val officeFiles = List(
     ExampleFiles.examples_sample_doc  -> 6241,
@@ -21,13 +20,13 @@ class PoiExtractTest extends FunSuite {
   test("extract text from ms office files") {
     officeFiles.foreach { case (file, len) =>
       val str1 = PoiExtract
-        .get[IO](file.readURL[IO](8192, blocker), MimeTypeHint.none)
+        .get[IO](file.readURL[IO](8192), MimeTypeHint.none)
         .unsafeRunSync()
         .fold(throw _, identity)
 
       val str2 = PoiExtract
         .get[IO](
-          file.readURL[IO](8192, blocker),
+          file.readURL[IO](8192),
           MimeTypeHint(Some(file.path.segments.last), None)
         )
         .unsafeRunSync()
diff --git a/modules/files/src/main/scala/docspell/files/Zip.scala b/modules/files/src/main/scala/docspell/files/Zip.scala
index 5450cbf7..89d2a8b6 100644
--- a/modules/files/src/main/scala/docspell/files/Zip.scala
+++ b/modules/files/src/main/scala/docspell/files/Zip.scala
@@ -13,28 +13,19 @@ import docspell.common.Glob
 
 object Zip {
 
-  def unzipP[F[_]: ConcurrentEffect: ContextShift](
-      chunkSize: Int,
-      blocker: Blocker,
-      glob: Glob
-  ): Pipe[F, Byte, Binary[F]] =
-    s => unzip[F](chunkSize, blocker, glob)(s)
+  def unzipP[F[_]: Async](chunkSize: Int, glob: Glob): Pipe[F, Byte, Binary[F]] =
+    s => unzip[F](chunkSize, glob)(s)
 
-  def unzip[F[_]: ConcurrentEffect: ContextShift](
-      chunkSize: Int,
-      blocker: Blocker,
-      glob: Glob
-  )(
+  def unzip[F[_]: Async](chunkSize: Int, glob: Glob)(
       data: Stream[F, Byte]
   ): Stream[F, Binary[F]] =
     data
       .through(fs2.io.toInputStream[F])
-      .flatMap(in => unzipJava(in, chunkSize, blocker, glob))
+      .flatMap(in => unzipJava(in, chunkSize, glob))
 
-  def unzipJava[F[_]: Sync: ContextShift](
+  def unzipJava[F[_]: Async](
       in: InputStream,
       chunkSize: Int,
-      blocker: Blocker,
       glob: Glob
   ): Stream[F, Binary[F]] = {
     val zin = new ZipInputStream(in)
@@ -52,7 +43,7 @@ object Zip {
       .map { ze =>
         val name = Paths.get(ze.getName()).getFileName.toString
         val data =
-          fs2.io.readInputStream[F]((zin: InputStream).pure[F], chunkSize, blocker, false)
+          fs2.io.readInputStream[F]((zin: InputStream).pure[F], chunkSize, false)
         Binary(name, data)
       }
   }
diff --git a/modules/files/src/test/scala/docspell/files/ImageSizeTest.scala b/modules/files/src/test/scala/docspell/files/ImageSizeTest.scala
index e82b0ce2..a0b879c3 100644
--- a/modules/files/src/test/scala/docspell/files/ImageSizeTest.scala
+++ b/modules/files/src/test/scala/docspell/files/ImageSizeTest.scala
@@ -1,16 +1,14 @@
 package docspell.files
 
-import scala.concurrent.ExecutionContext
 import scala.util.Using
 
-import cats.effect.{Blocker, IO}
+import cats.effect._
+import cats.effect.unsafe.implicits.global
 import cats.implicits._
 
 import munit._
 
 class ImageSizeTest extends FunSuite {
-  val blocker     = Blocker.liftExecutionContext(ExecutionContext.global)
-  implicit val CS = IO.contextShift(ExecutionContext.global)
 
   //tiff files are not supported on the jdk by default
   //requires an external library
@@ -37,7 +35,7 @@ class ImageSizeTest extends FunSuite {
 
   test("get sizes from stream") {
     files.foreach { case (uri, expect) =>
-      val stream = uri.readURL[IO](8192, blocker)
+      val stream = uri.readURL[IO](8192)
       val dim    = ImageSize.get(stream).unsafeRunSync()
       assertEquals(dim, expect.some)
     }
diff --git a/modules/files/src/test/scala/docspell/files/Playing.scala b/modules/files/src/test/scala/docspell/files/Playing.scala
index ecddf526..c1c56c42 100644
--- a/modules/files/src/test/scala/docspell/files/Playing.scala
+++ b/modules/files/src/test/scala/docspell/files/Playing.scala
@@ -1,19 +1,17 @@
 package docspell.files
 
-import scala.concurrent.ExecutionContext
-
 import cats.effect._
+import cats.effect.unsafe.implicits.global
 
 import docspell.common.MimeTypeHint
 
 object Playing extends IOApp {
-  val blocker = Blocker.liftExecutionContext(ExecutionContext.global)
 
   def run(args: List[String]): IO[ExitCode] =
     IO {
       //val ods = ExampleFiles.examples_sample_ods.readURL[IO](8192, blocker)
       //val odt = ExampleFiles.examples_sample_odt.readURL[IO](8192, blocker)
-      val rtf = ExampleFiles.examples_sample_rtf.readURL[IO](8192, blocker)
+      val rtf = ExampleFiles.examples_sample_rtf.readURL[IO](8192)
 
       val x = for {
         odsm1 <-
diff --git a/modules/files/src/test/scala/docspell/files/TestFiles.scala b/modules/files/src/test/scala/docspell/files/TestFiles.scala
index 283734cf..aa7c413a 100644
--- a/modules/files/src/test/scala/docspell/files/TestFiles.scala
+++ b/modules/files/src/test/scala/docspell/files/TestFiles.scala
@@ -1,29 +1,26 @@
 package docspell.files
 
-import scala.concurrent.ExecutionContext
-
-import cats.effect.{Blocker, IO}
+import cats.effect._
+import cats.effect.unsafe.implicits.global
 import fs2.Stream
 
 object TestFiles {
-  val blocker     = Blocker.liftExecutionContext(ExecutionContext.global)
-  implicit val CS = IO.contextShift(ExecutionContext.global)
 
   val letterSourceDE: Stream[IO, Byte] =
     ExampleFiles.letter_de_pdf
-      .readURL[IO](8 * 1024, blocker)
+      .readURL[IO](8 * 1024)
 
   val letterSourceEN: Stream[IO, Byte] =
     ExampleFiles.letter_en_pdf
-      .readURL[IO](8 * 1024, blocker)
+      .readURL[IO](8 * 1024)
 
   lazy val letterDEText =
     ExampleFiles.letter_de_txt
-      .readText[IO](8 * 1024, blocker)
+      .readText[IO](8 * 1024)
       .unsafeRunSync()
 
   lazy val letterENText =
     ExampleFiles.letter_en_txt
-      .readText[IO](8 * 1024, blocker)
+      .readText[IO](8 * 1024)
       .unsafeRunSync()
 }
diff --git a/modules/files/src/test/scala/docspell/files/ZipTest.scala b/modules/files/src/test/scala/docspell/files/ZipTest.scala
index 8ca1e991..f6557a3a 100644
--- a/modules/files/src/test/scala/docspell/files/ZipTest.scala
+++ b/modules/files/src/test/scala/docspell/files/ZipTest.scala
@@ -1,8 +1,7 @@
 package docspell.files
 
-import scala.concurrent.ExecutionContext
-
 import cats.effect._
+import cats.effect.unsafe.implicits.global
 import cats.implicits._
 
 import docspell.common.Glob
@@ -11,12 +10,9 @@ import munit._
 
 class ZipTest extends FunSuite {
 
-  val blocker     = Blocker.liftExecutionContext(ExecutionContext.global)
-  implicit val CS = IO.contextShift(ExecutionContext.global)
-
   test("unzip") {
-    val zipFile = ExampleFiles.letters_zip.readURL[IO](8192, blocker)
-    val uncomp  = zipFile.through(Zip.unzip(8192, blocker, Glob.all))
+    val zipFile = ExampleFiles.letters_zip.readURL[IO](8192)
+    val uncomp  = zipFile.through(Zip.unzip(8192, Glob.all))
 
     uncomp
       .evalMap { entry =>
diff --git a/modules/fts-solr/src/main/scala/docspell/ftssolr/SolrFtsClient.scala b/modules/fts-solr/src/main/scala/docspell/ftssolr/SolrFtsClient.scala
index b1c7e90d..84383a7b 100644
--- a/modules/fts-solr/src/main/scala/docspell/ftssolr/SolrFtsClient.scala
+++ b/modules/fts-solr/src/main/scala/docspell/ftssolr/SolrFtsClient.scala
@@ -11,7 +11,7 @@ import org.http4s.client.Client
 import org.http4s.client.middleware.Logger
 import org.log4s.getLogger
 
-final class SolrFtsClient[F[_]: Effect](
+final class SolrFtsClient[F[_]: Async](
     solrUpdate: SolrUpdate[F],
     solrSetup: SolrSetup[F],
     solrQuery: SolrQuery[F]
@@ -77,7 +77,7 @@ final class SolrFtsClient[F[_]: Effect](
 object SolrFtsClient {
   private[this] val logger = getLogger
 
-  def apply[F[_]: ConcurrentEffect](
+  def apply[F[_]: Async](
       cfg: SolrConfig,
       httpClient: Client[F]
   ): Resource[F, FtsClient[F]] = {
@@ -91,7 +91,7 @@ object SolrFtsClient {
     )
   }
 
-  private def loggingMiddleware[F[_]: Concurrent](
+  private def loggingMiddleware[F[_]: Async](
       cfg: SolrConfig,
       client: Client[F]
   ): Client[F] =
diff --git a/modules/fts-solr/src/main/scala/docspell/ftssolr/SolrQuery.scala b/modules/fts-solr/src/main/scala/docspell/ftssolr/SolrQuery.scala
index 11c08954..b8b66d9c 100644
--- a/modules/fts-solr/src/main/scala/docspell/ftssolr/SolrQuery.scala
+++ b/modules/fts-solr/src/main/scala/docspell/ftssolr/SolrQuery.scala
@@ -22,7 +22,7 @@ trait SolrQuery[F[_]] {
 }
 
 object SolrQuery {
-  def apply[F[_]: ConcurrentEffect](cfg: SolrConfig, client: Client[F]): SolrQuery[F] = {
+  def apply[F[_]: Async](cfg: SolrConfig, client: Client[F]): SolrQuery[F] = {
     val dsl = new Http4sClientDsl[F] {}
     import dsl._
 
diff --git a/modules/fts-solr/src/main/scala/docspell/ftssolr/SolrSetup.scala b/modules/fts-solr/src/main/scala/docspell/ftssolr/SolrSetup.scala
index 422c964f..3ffef19c 100644
--- a/modules/fts-solr/src/main/scala/docspell/ftssolr/SolrSetup.scala
+++ b/modules/fts-solr/src/main/scala/docspell/ftssolr/SolrSetup.scala
@@ -24,7 +24,7 @@ trait SolrSetup[F[_]] {
 object SolrSetup {
   private val versionDocId = "6d8f09f4-8d7e-4bc9-98b8-7c89223b36dd"
 
-  def apply[F[_]: ConcurrentEffect](cfg: SolrConfig, client: Client[F]): SolrSetup[F] = {
+  def apply[F[_]: Async](cfg: SolrConfig, client: Client[F]): SolrSetup[F] = {
     val dsl = new Http4sClientDsl[F] {}
     import dsl._
 
diff --git a/modules/fts-solr/src/main/scala/docspell/ftssolr/SolrUpdate.scala b/modules/fts-solr/src/main/scala/docspell/ftssolr/SolrUpdate.scala
index 7fa7db41..5c0e43d3 100644
--- a/modules/fts-solr/src/main/scala/docspell/ftssolr/SolrUpdate.scala
+++ b/modules/fts-solr/src/main/scala/docspell/ftssolr/SolrUpdate.scala
@@ -30,7 +30,7 @@ trait SolrUpdate[F[_]] {
 
 object SolrUpdate {
 
-  def apply[F[_]: ConcurrentEffect](cfg: SolrConfig, client: Client[F]): SolrUpdate[F] = {
+  def apply[F[_]: Async](cfg: SolrConfig, client: Client[F]): SolrUpdate[F] = {
     val dsl = new Http4sClientDsl[F] {}
     import dsl._
 
diff --git a/modules/joex/src/main/scala/docspell/joex/JoexAppImpl.scala b/modules/joex/src/main/scala/docspell/joex/JoexAppImpl.scala
index c98d95d5..2197455f 100644
--- a/modules/joex/src/main/scala/docspell/joex/JoexAppImpl.scala
+++ b/modules/joex/src/main/scala/docspell/joex/JoexAppImpl.scala
@@ -30,10 +30,10 @@ import docspell.store.queue._
 import docspell.store.records.RJobLog
 
 import emil.javamail._
+import org.http4s.blaze.client.BlazeClientBuilder
 import org.http4s.client.Client
-import org.http4s.client.blaze.BlazeClientBuilder
 
-final class JoexAppImpl[F[_]: ConcurrentEffect: Timer](
+final class JoexAppImpl[F[_]: Async](
     cfg: Config,
     nodeOps: ONode[F],
     store: Store[F],
@@ -49,8 +49,8 @@ final class JoexAppImpl[F[_]: ConcurrentEffect: Timer](
     val prun = periodicScheduler.start.compile.drain
     for {
       _ <- scheduleBackgroundTasks
-      _ <- ConcurrentEffect[F].start(run)
-      _ <- ConcurrentEffect[F].start(prun)
+      _ <- Async[F].start(run)
+      _ <- Async[F].start(prun)
       _ <- scheduler.periodicAwake
       _ <- periodicScheduler.periodicAwake
       _ <- nodeOps.register(cfg.appId, NodeType.Joex, cfg.baseUrl)
@@ -79,17 +79,16 @@ final class JoexAppImpl[F[_]: ConcurrentEffect: Timer](
 
 object JoexAppImpl {
 
-  def create[F[_]: ConcurrentEffect: ContextShift: Timer](
+  def create[F[_]: Async](
       cfg: Config,
       termSignal: SignallingRef[F, Boolean],
       connectEC: ExecutionContext,
-      clientEC: ExecutionContext,
-      blocker: Blocker
+      clientEC: ExecutionContext
   ): Resource[F, JoexApp[F]] =
     for {
       httpClient <- BlazeClientBuilder[F](clientEC).resource
       client = JoexClient(httpClient)
-      store    <- Store.create(cfg.jdbc, connectEC, blocker)
+      store    <- Store.create(cfg.jdbc, connectEC)
       queue    <- JobQueue(store)
       pstore   <- PeriodicTaskStore.create(store)
       nodeOps  <- ONode(store)
@@ -97,11 +96,11 @@ object JoexAppImpl {
       upload   <- OUpload(store, queue, cfg.files, joex)
       fts      <- createFtsClient(cfg)(httpClient)
       itemOps  <- OItem(store, fts, queue, joex)
-      analyser <- TextAnalyser.create[F](cfg.textAnalysis.textAnalysisConfig, blocker)
-      regexNer <- RegexNerFile(cfg.textAnalysis.regexNerFileConfig, blocker, store)
+      analyser <- TextAnalyser.create[F](cfg.textAnalysis.textAnalysisConfig)
+      regexNer <- RegexNerFile(cfg.textAnalysis.regexNerFileConfig, store)
       javaEmil =
-        JavaMailEmil(blocker, Settings.defaultSettings.copy(debug = cfg.mailDebug))
-      sch <- SchedulerBuilder(cfg.scheduler, blocker, store)
+        JavaMailEmil(Settings.defaultSettings.copy(debug = cfg.mailDebug))
+      sch <- SchedulerBuilder(cfg.scheduler, store)
         .withQueue(queue)
         .withTask(
           JobTask.json(
@@ -207,14 +206,13 @@ object JoexAppImpl {
         sch,
         queue,
         pstore,
-        client,
-        Timer[F]
+        client
       )
       app = new JoexAppImpl(cfg, nodeOps, store, queue, pstore, termSignal, sch, psch)
       appR <- Resource.make(app.init.map(_ => app))(_.shutdown)
     } yield appR
 
-  private def createFtsClient[F[_]: ConcurrentEffect](
+  private def createFtsClient[F[_]: Async](
       cfg: Config
   )(client: Client[F]): Resource[F, FtsClient[F]] =
     if (cfg.fullTextSearch.enabled) SolrFtsClient(cfg.fullTextSearch.solr, client)
diff --git a/modules/joex/src/main/scala/docspell/joex/JoexServer.scala b/modules/joex/src/main/scala/docspell/joex/JoexServer.scala
index 10db220c..e325cf67 100644
--- a/modules/joex/src/main/scala/docspell/joex/JoexServer.scala
+++ b/modules/joex/src/main/scala/docspell/joex/JoexServer.scala
@@ -1,7 +1,7 @@
 package docspell.joex
 
+import cats.effect.Ref
 import cats.effect._
-import cats.effect.concurrent.Ref
 import fs2.Stream
 import fs2.concurrent.SignallingRef
 
@@ -9,9 +9,9 @@ import docspell.common.Pools
 import docspell.joex.routes._
 
 import org.http4s.HttpApp
+import org.http4s.blaze.server.BlazeServerBuilder
 import org.http4s.implicits._
 import org.http4s.server.Router
-import org.http4s.server.blaze.BlazeServerBuilder
 import org.http4s.server.middleware.Logger
 
 object JoexServer {
@@ -22,17 +22,14 @@ object JoexServer {
       exitRef: Ref[F, ExitCode]
   )
 
-  def stream[F[_]: ConcurrentEffect: ContextShift](
-      cfg: Config,
-      pools: Pools
-  )(implicit T: Timer[F]): Stream[F, Nothing] = {
+  def stream[F[_]: Async](cfg: Config, pools: Pools): Stream[F, Nothing] = {
 
     val app = for {
       signal   <- Resource.eval(SignallingRef[F, Boolean](false))
       exitCode <- Resource.eval(Ref[F].of(ExitCode.Success))
       joexApp <-
         JoexAppImpl
-          .create[F](cfg, signal, pools.connectEC, pools.httpClientEC, pools.blocker)
+          .create[F](cfg, signal, pools.connectEC, pools.httpClientEC)
 
       httpApp = Router(
         "/api/info" -> InfoRoutes(cfg),
diff --git a/modules/joex/src/main/scala/docspell/joex/Main.scala b/modules/joex/src/main/scala/docspell/joex/Main.scala
index ae5854ab..a5ccd338 100644
--- a/modules/joex/src/main/scala/docspell/joex/Main.scala
+++ b/modules/joex/src/main/scala/docspell/joex/Main.scala
@@ -57,9 +57,8 @@ object Main extends IOApp {
     val pools = for {
       cec <- connectEC
       bec <- blockingEC
-      blocker = Blocker.liftExecutorService(bec)
       rec <- restserverEC
-    } yield Pools(cec, bec, blocker, rec)
+    } yield Pools(cec, bec, rec)
     pools.use(p =>
       JoexServer
         .stream[IO](cfg, p)
diff --git a/modules/joex/src/main/scala/docspell/joex/analysis/NerFile.scala b/modules/joex/src/main/scala/docspell/joex/analysis/NerFile.scala
index 3939fc26..8075dd5d 100644
--- a/modules/joex/src/main/scala/docspell/joex/analysis/NerFile.scala
+++ b/modules/joex/src/main/scala/docspell/joex/analysis/NerFile.scala
@@ -33,16 +33,15 @@ object NerFile {
   private def jsonFilePath(directory: Path, collective: Ident): Path =
     directory.resolve(s"${collective.id}.json")
 
-  def find[F[_]: Sync: ContextShift](
+  def find[F[_]: Async](
       collective: Ident,
-      directory: Path,
-      blocker: Blocker
+      directory: Path
   ): F[Option[NerFile]] = {
     val file = jsonFilePath(directory, collective)
     File.existsNonEmpty[F](file).flatMap {
       case true =>
         File
-          .readJson[F, NerFile](file, blocker)
+          .readJson[F, NerFile](file)
           .map(_.some)
       case false =>
         (None: Option[NerFile]).pure[F]
diff --git a/modules/joex/src/main/scala/docspell/joex/analysis/RegexNerFile.scala b/modules/joex/src/main/scala/docspell/joex/analysis/RegexNerFile.scala
index 47f9cdb4..e1a3b65d 100644
--- a/modules/joex/src/main/scala/docspell/joex/analysis/RegexNerFile.scala
+++ b/modules/joex/src/main/scala/docspell/joex/analysis/RegexNerFile.scala
@@ -3,7 +3,7 @@ package docspell.joex.analysis
 import java.nio.file.Path
 
 import cats.effect._
-import cats.effect.concurrent.Semaphore
+import cats.effect.std.Semaphore
 import cats.implicits._
 
 import docspell.common._
@@ -31,19 +31,17 @@ object RegexNerFile {
 
   case class Config(maxEntries: Int, directory: Path, minTime: Duration)
 
-  def apply[F[_]: Concurrent: ContextShift](
+  def apply[F[_]: Async](
       cfg: Config,
-      blocker: Blocker,
       store: Store[F]
   ): Resource[F, RegexNerFile[F]] =
     for {
       dir    <- File.withTempDir[F](cfg.directory, "regexner-")
       writer <- Resource.eval(Semaphore(1))
-    } yield new Impl[F](cfg.copy(directory = dir), blocker, store, writer)
+    } yield new Impl[F](cfg.copy(directory = dir), store, writer)
 
-  final private class Impl[F[_]: Concurrent: ContextShift](
+  final private class Impl[F[_]: Async](
       cfg: Config,
-      blocker: Blocker,
       store: Store[F],
       writer: Semaphore[F] //TODO allow parallelism per collective
   ) extends RegexNerFile[F] {
@@ -55,7 +53,7 @@ object RegexNerFile {
     def doMakeFile(collective: Ident): F[Option[Path]] =
       for {
         now      <- Timestamp.current[F]
-        existing <- NerFile.find[F](collective, cfg.directory, blocker)
+        existing <- NerFile.find[F](collective, cfg.directory)
         result <- existing match {
           case Some(nf) =>
             val dur = Duration.between(nf.creation, now)
@@ -105,11 +103,13 @@ object RegexNerFile {
       } yield result
 
     private def updateTimestamp(nf: NerFile, now: Timestamp): F[Unit] =
-      writer.withPermit(for {
-        file <- Sync[F].pure(nf.jsonFilePath(cfg.directory))
-        _    <- File.mkDir(file.getParent)
-        _    <- File.writeString(file, nf.copy(creation = now).asJson.spaces2)
-      } yield ())
+      writer.permit.use(_ =>
+        for {
+          file <- Sync[F].pure(nf.jsonFilePath(cfg.directory))
+          _    <- File.mkDir(file.getParent)
+          _    <- File.writeString(file, nf.copy(creation = now).asJson.spaces2)
+        } yield ()
+      )
 
     private def createFile(
         lastUpdate: Timestamp,
@@ -117,13 +117,17 @@ object RegexNerFile {
         now: Timestamp
     ): F[NerFile] = {
       def update(nf: NerFile, text: String): F[Unit] =
-        writer.withPermit(for {
-          jsonFile <- Sync[F].pure(nf.jsonFilePath(cfg.directory))
-          _        <- logger.fdebug(s"Writing custom NER file for collective '${collective.id}'")
-          _        <- File.mkDir(jsonFile.getParent)
-          _        <- File.writeString(nf.nerFilePath(cfg.directory), text)
-          _        <- File.writeString(jsonFile, nf.asJson.spaces2)
-        } yield ())
+        writer.permit.use(_ =>
+          for {
+            jsonFile <- Sync[F].pure(nf.jsonFilePath(cfg.directory))
+            _ <- logger.fdebug(
+              s"Writing custom NER file for collective '${collective.id}'"
+            )
+            _ <- File.mkDir(jsonFile.getParent)
+            _ <- File.writeString(nf.nerFilePath(cfg.directory), text)
+            _ <- File.writeString(jsonFile, nf.asJson.spaces2)
+          } yield ()
+        )
 
       for {
         _     <- logger.finfo(s"Generating custom NER file for collective '${collective.id}'")
diff --git a/modules/joex/src/main/scala/docspell/joex/fts/Migration.scala b/modules/joex/src/main/scala/docspell/joex/fts/Migration.scala
index ad71c1ad..036c84fd 100644
--- a/modules/joex/src/main/scala/docspell/joex/fts/Migration.scala
+++ b/modules/joex/src/main/scala/docspell/joex/fts/Migration.scala
@@ -28,7 +28,7 @@ object Migration {
   def from[F[_]: Applicative: FlatMap](fm: FtsMigration[F]): Migration[F] =
     Migration(fm.version, fm.engine, fm.description, FtsWork.from(fm.task))
 
-  def apply[F[_]: Effect](
+  def apply[F[_]: Async](
       cfg: Config.FullTextSearch,
       fts: FtsClient[F],
       store: Store[F],
@@ -41,7 +41,7 @@ object Migration {
     }
   }
 
-  def applySingle[F[_]: Effect](ctx: FtsContext[F])(m: Migration[F]): F[Unit] =
+  def applySingle[F[_]: Async](ctx: FtsContext[F])(m: Migration[F]): F[Unit] =
     for {
       _ <- ctx.logger.info(s"Apply ${m.version}/${m.description}")
       _ <- m.task.run(ctx)
diff --git a/modules/joex/src/main/scala/docspell/joex/fts/MigrationTask.scala b/modules/joex/src/main/scala/docspell/joex/fts/MigrationTask.scala
index d8c4e4db..63b27a88 100644
--- a/modules/joex/src/main/scala/docspell/joex/fts/MigrationTask.scala
+++ b/modules/joex/src/main/scala/docspell/joex/fts/MigrationTask.scala
@@ -12,7 +12,7 @@ import docspell.store.records.RJob
 object MigrationTask {
   val taskName = Ident.unsafe("full-text-index")
 
-  def apply[F[_]: ConcurrentEffect](
+  def apply[F[_]: Async](
       cfg: Config.FullTextSearch,
       fts: FtsClient[F]
   ): Task[F, Unit, Unit] =
@@ -46,7 +46,7 @@ object MigrationTask {
       Some(DocspellSystem.migrationTaskTracker)
     )
 
-  def migrationTasks[F[_]: Effect](fts: FtsClient[F]): F[List[Migration[F]]] =
+  def migrationTasks[F[_]: Async](fts: FtsClient[F]): F[List[Migration[F]]] =
     fts.initialize.map(_.map(fm => Migration.from(fm)))
 
 }
diff --git a/modules/joex/src/main/scala/docspell/joex/fts/ReIndexTask.scala b/modules/joex/src/main/scala/docspell/joex/fts/ReIndexTask.scala
index 66751b1b..a04449bd 100644
--- a/modules/joex/src/main/scala/docspell/joex/fts/ReIndexTask.scala
+++ b/modules/joex/src/main/scala/docspell/joex/fts/ReIndexTask.scala
@@ -14,7 +14,7 @@ object ReIndexTask {
   val taskName = ReIndexTaskArgs.taskName
   val tracker  = DocspellSystem.migrationTaskTracker
 
-  def apply[F[_]: ConcurrentEffect](
+  def apply[F[_]: Async](
       cfg: Config.FullTextSearch,
       fts: FtsClient[F]
   ): Task[F, Args, Unit] =
@@ -27,7 +27,7 @@ object ReIndexTask {
   def onCancel[F[_]]: Task[F, Args, Unit] =
     Task.log[F, Args](_.warn("Cancelling full-text re-index task"))
 
-  private def clearData[F[_]: ConcurrentEffect](collective: Option[Ident]): FtsWork[F] =
+  private def clearData[F[_]: Async](collective: Option[Ident]): FtsWork[F] =
     FtsWork.log[F](_.info("Clearing index data")) ++
       (collective match {
         case Some(_) =>
diff --git a/modules/joex/src/main/scala/docspell/joex/hk/CheckNodesTask.scala b/modules/joex/src/main/scala/docspell/joex/hk/CheckNodesTask.scala
index 8380a07d..77909929 100644
--- a/modules/joex/src/main/scala/docspell/joex/hk/CheckNodesTask.scala
+++ b/modules/joex/src/main/scala/docspell/joex/hk/CheckNodesTask.scala
@@ -7,19 +7,20 @@ import docspell.common._
 import docspell.joex.scheduler.{Context, Task}
 import docspell.store.records._
 
+import org.http4s.blaze.client.BlazeClientBuilder
 import org.http4s.client.Client
-import org.http4s.client.blaze.BlazeClientBuilder
 
 object CheckNodesTask {
 
-  def apply[F[_]: ConcurrentEffect](
+  def apply[F[_]: Async](
       cfg: HouseKeepingConfig.CheckNodes
   ): Task[F, Unit, Unit] =
     Task { ctx =>
       if (cfg.enabled)
         for {
           _ <- ctx.logger.info("Check nodes reachability")
-          _ <- BlazeClientBuilder[F](ctx.blocker.blockingContext).resource.use { client =>
+          ec = scala.concurrent.ExecutionContext.global
+          _ <- BlazeClientBuilder[F](ec).resource.use { client =>
             checkNodes(ctx, client)
           }
           _ <- ctx.logger.info(
@@ -32,7 +33,7 @@ object CheckNodesTask {
         ctx.logger.info("CheckNodes task is disabled in the configuration")
     }
 
-  def checkNodes[F[_]: Sync](ctx: Context[F, _], client: Client[F]): F[Unit] =
+  def checkNodes[F[_]: Async](ctx: Context[F, _], client: Client[F]): F[Unit] =
     ctx.store
       .transact(RNode.streamAll)
       .evalMap(node =>
@@ -45,7 +46,7 @@ object CheckNodesTask {
       .compile
       .drain
 
-  def checkNode[F[_]: Sync](logger: Logger[F], client: Client[F])(
+  def checkNode[F[_]: Async](logger: Logger[F], client: Client[F])(
       url: LenientUri
   ): F[Boolean] = {
     val apiVersion = url / "api" / "info" / "version"
diff --git a/modules/joex/src/main/scala/docspell/joex/hk/HouseKeepingTask.scala b/modules/joex/src/main/scala/docspell/joex/hk/HouseKeepingTask.scala
index 1670ea1b..3613a188 100644
--- a/modules/joex/src/main/scala/docspell/joex/hk/HouseKeepingTask.scala
+++ b/modules/joex/src/main/scala/docspell/joex/hk/HouseKeepingTask.scala
@@ -15,7 +15,7 @@ object HouseKeepingTask {
 
   val taskName: Ident = Ident.unsafe("housekeeping")
 
-  def apply[F[_]: ConcurrentEffect](cfg: Config): Task[F, Unit, Unit] =
+  def apply[F[_]: Async](cfg: Config): Task[F, Unit, Unit] =
     Task
       .log[F, Unit](_.info(s"Running house-keeping task now"))
       .flatMap(_ => CleanupInvitesTask(cfg.houseKeeping.cleanupInvites))
diff --git a/modules/joex/src/main/scala/docspell/joex/learn/Classify.scala b/modules/joex/src/main/scala/docspell/joex/learn/Classify.scala
index 4d4c2676..ce8dffc9 100644
--- a/modules/joex/src/main/scala/docspell/joex/learn/Classify.scala
+++ b/modules/joex/src/main/scala/docspell/joex/learn/Classify.scala
@@ -5,6 +5,7 @@ import java.nio.file.Path
 import cats.data.OptionT
 import cats.effect._
 import cats.implicits._
+import fs2.io.file.Files
 
 import docspell.analysis.classifier.{ClassifierModel, TextClassifier}
 import docspell.common._
@@ -15,8 +16,7 @@ import bitpeace.RangeDef
 
 object Classify {
 
-  def apply[F[_]: Sync: ContextShift](
-      blocker: Blocker,
+  def apply[F[_]: Async](
       logger: Logger[F],
       workingDir: Path,
       store: Store[F],
@@ -36,7 +36,7 @@ object Classify {
       cls <- OptionT(File.withTempDir(workingDir, "classify").use { dir =>
         val modelFile = dir.resolve("model.ser.gz")
         modelData
-          .through(fs2.io.file.writeAll(modelFile, blocker))
+          .through(Files[F].writeAll(modelFile))
           .compile
           .drain
           .flatMap(_ => classifier.classify(logger, ClassifierModel(modelFile), text))
diff --git a/modules/joex/src/main/scala/docspell/joex/learn/LearnClassifierTask.scala b/modules/joex/src/main/scala/docspell/joex/learn/LearnClassifierTask.scala
index 5387fdc8..49e4711b 100644
--- a/modules/joex/src/main/scala/docspell/joex/learn/LearnClassifierTask.scala
+++ b/modules/joex/src/main/scala/docspell/joex/learn/LearnClassifierTask.scala
@@ -20,7 +20,7 @@ object LearnClassifierTask {
   def onCancel[F[_]]: Task[F, Args, Unit] =
     Task.log(_.warn("Cancelling learn-classifier task"))
 
-  def apply[F[_]: Sync: ContextShift](
+  def apply[F[_]: Async](
       cfg: Config.TextAnalysis,
       analyser: TextAnalyser[F]
   ): Task[F, Args, Unit] =
@@ -28,7 +28,7 @@ object LearnClassifierTask {
       .flatMap(_ => learnItemEntities(cfg, analyser))
       .flatMap(_ => Task(_ => Sync[F].delay(System.gc())))
 
-  private def learnItemEntities[F[_]: Sync: ContextShift](
+  private def learnItemEntities[F[_]: Async](
       cfg: Config.TextAnalysis,
       analyser: TextAnalyser[F]
   ): Task[F, Args, Unit] =
@@ -45,7 +45,7 @@ object LearnClassifierTask {
       else ().pure[F]
     }
 
-  private def learnTags[F[_]: Sync: ContextShift](
+  private def learnTags[F[_]: Async](
       cfg: Config.TextAnalysis,
       analyser: TextAnalyser[F]
   ): Task[F, Args, Unit] =
diff --git a/modules/joex/src/main/scala/docspell/joex/learn/LearnItemEntities.scala b/modules/joex/src/main/scala/docspell/joex/learn/LearnItemEntities.scala
index f47f1e9c..f442f49f 100644
--- a/modules/joex/src/main/scala/docspell/joex/learn/LearnItemEntities.scala
+++ b/modules/joex/src/main/scala/docspell/joex/learn/LearnItemEntities.scala
@@ -11,7 +11,7 @@ import docspell.common._
 import docspell.joex.scheduler._
 
 object LearnItemEntities {
-  def learnAll[F[_]: Sync: ContextShift, A](
+  def learnAll[F[_]: Async, A](
       analyser: TextAnalyser[F],
       collective: Ident,
       maxItems: Int,
@@ -22,7 +22,7 @@ object LearnItemEntities {
       .flatMap(_ => learnConcPerson(analyser, collective, maxItems, maxTextLen))
       .flatMap(_ => learnConcEquip(analyser, collective, maxItems, maxTextLen))
 
-  def learnCorrOrg[F[_]: Sync: ContextShift, A](
+  def learnCorrOrg[F[_]: Async, A](
       analyser: TextAnalyser[F],
       collective: Ident,
       maxItems: Int,
@@ -33,7 +33,7 @@ object LearnItemEntities {
       ctx => SelectItems.forCorrOrg(ctx.store, collective, maxItems, maxTextLen)
     )
 
-  def learnCorrPerson[F[_]: Sync: ContextShift, A](
+  def learnCorrPerson[F[_]: Async, A](
       analyser: TextAnalyser[F],
       collective: Ident,
       maxItems: Int,
@@ -44,7 +44,7 @@ object LearnItemEntities {
       ctx => SelectItems.forCorrPerson(ctx.store, collective, maxItems, maxTextLen)
     )
 
-  def learnConcPerson[F[_]: Sync: ContextShift, A](
+  def learnConcPerson[F[_]: Async, A](
       analyser: TextAnalyser[F],
       collective: Ident,
       maxItems: Int,
@@ -55,7 +55,7 @@ object LearnItemEntities {
       ctx => SelectItems.forConcPerson(ctx.store, collective, maxItems, maxTextLen)
     )
 
-  def learnConcEquip[F[_]: Sync: ContextShift, A](
+  def learnConcEquip[F[_]: Async, A](
       analyser: TextAnalyser[F],
       collective: Ident,
       maxItems: Int,
@@ -66,7 +66,7 @@ object LearnItemEntities {
       ctx => SelectItems.forConcEquip(ctx.store, collective, maxItems, maxTextLen)
     )
 
-  private def learn[F[_]: Sync: ContextShift, A](
+  private def learn[F[_]: Async, A](
       analyser: TextAnalyser[F],
       collective: Ident
   )(cname: ClassifierName, data: Context[F, _] => Stream[F, Data]): Task[F, A, Unit] =
diff --git a/modules/joex/src/main/scala/docspell/joex/learn/LearnTags.scala b/modules/joex/src/main/scala/docspell/joex/learn/LearnTags.scala
index 234a548f..4a3f211f 100644
--- a/modules/joex/src/main/scala/docspell/joex/learn/LearnTags.scala
+++ b/modules/joex/src/main/scala/docspell/joex/learn/LearnTags.scala
@@ -11,7 +11,7 @@ import docspell.store.records.RClassifierSetting
 
 object LearnTags {
 
-  def learnTagCategory[F[_]: Sync: ContextShift, A](
+  def learnTagCategory[F[_]: Async, A](
       analyser: TextAnalyser[F],
       collective: Ident,
       maxItems: Int,
@@ -33,7 +33,7 @@ object LearnTags {
         )
     }
 
-  def learnAllTagCategories[F[_]: Sync: ContextShift, A](analyser: TextAnalyser[F])(
+  def learnAllTagCategories[F[_]: Async, A](analyser: TextAnalyser[F])(
       collective: Ident,
       maxItems: Int,
       maxTextLen: Int
diff --git a/modules/joex/src/main/scala/docspell/joex/learn/StoreClassifierModel.scala b/modules/joex/src/main/scala/docspell/joex/learn/StoreClassifierModel.scala
index 03d027a1..91251c2e 100644
--- a/modules/joex/src/main/scala/docspell/joex/learn/StoreClassifierModel.scala
+++ b/modules/joex/src/main/scala/docspell/joex/learn/StoreClassifierModel.scala
@@ -2,6 +2,7 @@ package docspell.joex.learn
 
 import cats.effect._
 import cats.implicits._
+import fs2.io.file.Files
 
 import docspell.analysis.classifier.ClassifierModel
 import docspell.common._
@@ -13,18 +14,17 @@ import bitpeace.MimetypeHint
 
 object StoreClassifierModel {
 
-  def handleModel[F[_]: Sync: ContextShift](
+  def handleModel[F[_]: Async](
       ctx: Context[F, _],
       collective: Ident,
       modelName: ClassifierName
   )(
       trainedModel: ClassifierModel
   ): F[Unit] =
-    handleModel(ctx.store, ctx.blocker, ctx.logger)(collective, modelName, trainedModel)
+    handleModel(ctx.store, ctx.logger)(collective, modelName, trainedModel)
 
-  def handleModel[F[_]: Sync: ContextShift](
+  def handleModel[F[_]: Async](
       store: Store[F],
-      blocker: Blocker,
       logger: Logger[F]
   )(
       collective: Ident,
@@ -36,7 +36,7 @@ object StoreClassifierModel {
         RClassifierModel.findByName(collective, modelName.name).map(_.map(_.fileId))
       )
       _ <- logger.debug(s"Storing new trained model for: ${modelName.name}")
-      fileData = fs2.io.file.readAll(trainedModel.model, blocker, 4096)
+      fileData = Files[F].readAll(trainedModel.model, 4096)
       newFile <-
         store.bitpeace.saveNew(fileData, 4096, MimetypeHint.none).compile.lastOrError
       _ <- store.transact(
diff --git a/modules/joex/src/main/scala/docspell/joex/mail/ReadMail.scala b/modules/joex/src/main/scala/docspell/joex/mail/ReadMail.scala
index e1a88844..78179d69 100644
--- a/modules/joex/src/main/scala/docspell/joex/mail/ReadMail.scala
+++ b/modules/joex/src/main/scala/docspell/joex/mail/ReadMail.scala
@@ -15,7 +15,7 @@ import emil.{MimeType => _, _}
 
 object ReadMail {
 
-  def readBytesP[F[_]: ConcurrentEffect](
+  def readBytesP[F[_]: Async](
       logger: Logger[F],
       glob: Glob
   ): Pipe[F, Byte, Binary[F]] =
@@ -26,7 +26,7 @@ object ReadMail {
       Stream.eval(logger.debug(s"Converting e-mail file...")) >>
         s.through(Mail.readBytes[F])
 
-  def mailToEntries[F[_]: ConcurrentEffect](
+  def mailToEntries[F[_]: Async](
       logger: Logger[F],
       glob: Glob
   )(mail: Mail[F]): Stream[F, Binary[F]] = {
diff --git a/modules/joex/src/main/scala/docspell/joex/pdfconv/PdfConvTask.scala b/modules/joex/src/main/scala/docspell/joex/pdfconv/PdfConvTask.scala
index 4eb7cd35..eb8ebe4a 100644
--- a/modules/joex/src/main/scala/docspell/joex/pdfconv/PdfConvTask.scala
+++ b/modules/joex/src/main/scala/docspell/joex/pdfconv/PdfConvTask.scala
@@ -35,7 +35,7 @@ object PdfConvTask {
 
   val taskName = Ident.unsafe("pdf-files-migration")
 
-  def apply[F[_]: Sync: ContextShift](cfg: Config): Task[F, Args, Unit] =
+  def apply[F[_]: Async](cfg: Config): Task[F, Args, Unit] =
     Task { ctx =>
       for {
         _    <- ctx.logger.info(s"Converting pdf file ${ctx.args} using ocrmypdf")
@@ -62,7 +62,7 @@ object PdfConvTask {
     val existsPdf =
       for {
         meta <- ctx.store.transact(RAttachment.findMeta(ctx.args.attachId))
-        res = meta.filter(_.mimetype.matches(Mimetype.`application/pdf`))
+        res = meta.filter(_.mimetype.matches(Mimetype.applicationPdf))
         _ <-
           if (res.isEmpty)
             ctx.logger.info(
@@ -83,7 +83,7 @@ object PdfConvTask {
     else none.pure[F]
   }
 
-  def convert[F[_]: Sync: ContextShift](
+  def convert[F[_]: Async](
       cfg: Config,
       ctx: Context[F, Args],
       in: FileMeta
@@ -118,7 +118,6 @@ object PdfConvTask {
         cfg.convert.ocrmypdf,
         lang,
         in.chunksize,
-        ctx.blocker,
         ctx.logger
       )(data, storeResult)
 
diff --git a/modules/joex/src/main/scala/docspell/joex/process/AttachmentPageCount.scala b/modules/joex/src/main/scala/docspell/joex/process/AttachmentPageCount.scala
index 8082213b..62e22082 100644
--- a/modules/joex/src/main/scala/docspell/joex/process/AttachmentPageCount.scala
+++ b/modules/joex/src/main/scala/docspell/joex/process/AttachmentPageCount.scala
@@ -95,7 +95,7 @@ object AttachmentPageCount {
   def findMime[F[_]: Functor](ctx: Context[F, _])(ra: RAttachment): F[MimeType] =
     OptionT(ctx.store.transact(RFileMeta.findById(ra.fileId)))
       .map(_.mimetype)
-      .getOrElse(Mimetype.`application/octet-stream`)
+      .getOrElse(Mimetype.applicationOctetStream)
       .map(_.toLocal)
 
   def loadFile[F[_]](ctx: Context[F, _])(ra: RAttachment): Stream[F, Byte] =
diff --git a/modules/joex/src/main/scala/docspell/joex/process/AttachmentPreview.scala b/modules/joex/src/main/scala/docspell/joex/process/AttachmentPreview.scala
index f0f26ed2..68c5ec3e 100644
--- a/modules/joex/src/main/scala/docspell/joex/process/AttachmentPreview.scala
+++ b/modules/joex/src/main/scala/docspell/joex/process/AttachmentPreview.scala
@@ -98,7 +98,7 @@ object AttachmentPreview {
   def findMime[F[_]: Functor](ctx: Context[F, _])(ra: RAttachment): F[MimeType] =
     OptionT(ctx.store.transact(RFileMeta.findById(ra.fileId)))
       .map(_.mimetype)
-      .getOrElse(Mimetype.`application/octet-stream`)
+      .getOrElse(Mimetype.applicationOctetStream)
       .map(_.toLocal)
 
   def loadFile[F[_]](ctx: Context[F, _])(ra: RAttachment): Stream[F, Byte] =
diff --git a/modules/joex/src/main/scala/docspell/joex/process/ConvertPdf.scala b/modules/joex/src/main/scala/docspell/joex/process/ConvertPdf.scala
index 84828e19..f1b528ae 100644
--- a/modules/joex/src/main/scala/docspell/joex/process/ConvertPdf.scala
+++ b/modules/joex/src/main/scala/docspell/joex/process/ConvertPdf.scala
@@ -33,7 +33,7 @@ import bitpeace.{Mimetype, MimetypeHint, RangeDef}
   */
 object ConvertPdf {
 
-  def apply[F[_]: Sync: ContextShift](
+  def apply[F[_]: Async](
       cfg: ConvertConfig,
       item: ItemData
   ): Task[F, ProcessItemArgs, ItemData] =
@@ -69,15 +69,15 @@ object ConvertPdf {
   def findMime[F[_]: Functor](ctx: Context[F, _])(ra: RAttachment): F[Mimetype] =
     OptionT(ctx.store.transact(RFileMeta.findById(ra.fileId)))
       .map(_.mimetype)
-      .getOrElse(Mimetype.`application/octet-stream`)
+      .getOrElse(Mimetype.applicationOctetStream)
 
-  def convertSafe[F[_]: Sync: ContextShift](
+  def convertSafe[F[_]: Async](
       cfg: ConvertConfig,
       sanitizeHtml: SanitizeHtml,
       ctx: Context[F, ProcessItemArgs],
       item: ItemData
   )(ra: RAttachment, mime: Mimetype): F[(RAttachment, Option[RAttachmentMeta])] =
-    Conversion.create[F](cfg, sanitizeHtml, ctx.blocker, ctx.logger).use { conv =>
+    Conversion.create[F](cfg, sanitizeHtml, ctx.logger).use { conv =>
       mime.toLocal match {
         case mt =>
           val data = ctx.store.bitpeace
diff --git a/modules/joex/src/main/scala/docspell/joex/process/ExtractArchive.scala b/modules/joex/src/main/scala/docspell/joex/process/ExtractArchive.scala
index 0d33d243..8420023f 100644
--- a/modules/joex/src/main/scala/docspell/joex/process/ExtractArchive.scala
+++ b/modules/joex/src/main/scala/docspell/joex/process/ExtractArchive.scala
@@ -32,12 +32,12 @@ import emil.Mail
   */
 object ExtractArchive {
 
-  def apply[F[_]: ConcurrentEffect: ContextShift](
+  def apply[F[_]: Async](
       item: ItemData
   ): Task[F, ProcessItemArgs, ItemData] =
     multiPass(item, None).map(_._2)
 
-  def multiPass[F[_]: ConcurrentEffect: ContextShift](
+  def multiPass[F[_]: Async](
       item: ItemData,
       archive: Option[RAttachmentArchive]
   ): Task[F, ProcessItemArgs, (Option[RAttachmentArchive], ItemData)] =
@@ -46,7 +46,7 @@ object ExtractArchive {
       else multiPass(t._2, t._1)
     }
 
-  def singlePass[F[_]: ConcurrentEffect: ContextShift](
+  def singlePass[F[_]: Async](
       item: ItemData,
       archive: Option[RAttachmentArchive]
   ): Task[F, ProcessItemArgs, (Option[RAttachmentArchive], ItemData)] =
@@ -85,9 +85,9 @@ object ExtractArchive {
   def findMime[F[_]: Functor](ctx: Context[F, _])(ra: RAttachment): F[Mimetype] =
     OptionT(ctx.store.transact(RFileMeta.findById(ra.fileId)))
       .map(_.mimetype)
-      .getOrElse(Mimetype.`application/octet-stream`)
+      .getOrElse(Mimetype.applicationOctetStream)
 
-  def extractSafe[F[_]: ConcurrentEffect: ContextShift](
+  def extractSafe[F[_]: Async](
       ctx: Context[F, ProcessItemArgs],
       archive: Option[RAttachmentArchive]
   )(ra: RAttachment, pos: Int, mime: Mimetype): F[Extracted] =
@@ -131,7 +131,7 @@ object ExtractArchive {
         } yield extracted.copy(files = extracted.files.filter(_.id != ra.id))
     }
 
-  def extractZip[F[_]: ConcurrentEffect: ContextShift](
+  def extractZip[F[_]: Async](
       ctx: Context[F, ProcessItemArgs],
       archive: Option[RAttachmentArchive]
   )(ra: RAttachment, pos: Int): F[Extracted] = {
@@ -142,7 +142,7 @@ object ExtractArchive {
     val glob = ctx.args.meta.fileFilter.getOrElse(Glob.all)
     ctx.logger.debug(s"Filtering zip entries with '${glob.asString}'") *>
       zipData
-        .through(Zip.unzipP[F](8192, ctx.blocker, glob))
+        .through(Zip.unzipP[F](8192, glob))
         .zipWithIndex
         .flatMap(handleEntry(ctx, ra, pos, archive, None))
         .foldMonoid
@@ -150,7 +150,7 @@ object ExtractArchive {
         .lastOrError
   }
 
-  def extractMail[F[_]: ConcurrentEffect](
+  def extractMail[F[_]: Async](
       ctx: Context[F, ProcessItemArgs],
       archive: Option[RAttachmentArchive]
   )(ra: RAttachment, pos: Int): F[Extracted] = {
diff --git a/modules/joex/src/main/scala/docspell/joex/process/ItemHandler.scala b/modules/joex/src/main/scala/docspell/joex/process/ItemHandler.scala
index 90334bfd..2428dce3 100644
--- a/modules/joex/src/main/scala/docspell/joex/process/ItemHandler.scala
+++ b/modules/joex/src/main/scala/docspell/joex/process/ItemHandler.scala
@@ -28,7 +28,7 @@ object ItemHandler {
       }
     )
 
-  def newItem[F[_]: ConcurrentEffect: ContextShift](
+  def newItem[F[_]: Async](
       cfg: Config,
       itemOps: OItem[F],
       fts: FtsClient[F],
@@ -62,7 +62,7 @@ object ItemHandler {
   def isLastRetry[F[_]: Sync]: Task[F, Args, Boolean] =
     Task(_.isLastRetry)
 
-  def safeProcess[F[_]: ConcurrentEffect: ContextShift](
+  def safeProcess[F[_]: Async](
       cfg: Config,
       itemOps: OItem[F],
       fts: FtsClient[F],
diff --git a/modules/joex/src/main/scala/docspell/joex/process/ProcessItem.scala b/modules/joex/src/main/scala/docspell/joex/process/ProcessItem.scala
index f3fd1862..535c1ba9 100644
--- a/modules/joex/src/main/scala/docspell/joex/process/ProcessItem.scala
+++ b/modules/joex/src/main/scala/docspell/joex/process/ProcessItem.scala
@@ -12,7 +12,7 @@ import docspell.joex.scheduler.Task
 
 object ProcessItem {
 
-  def apply[F[_]: ConcurrentEffect: ContextShift](
+  def apply[F[_]: Async](
       cfg: Config,
       itemOps: OItem[F],
       fts: FtsClient[F],
@@ -27,7 +27,7 @@ object ProcessItem {
       .flatMap(Task.setProgress(99))
       .flatMap(RemoveEmptyItem(itemOps))
 
-  def processAttachments[F[_]: ConcurrentEffect: ContextShift](
+  def processAttachments[F[_]: Async](
       cfg: Config,
       fts: FtsClient[F],
       analyser: TextAnalyser[F],
@@ -35,7 +35,7 @@ object ProcessItem {
   )(item: ItemData): Task[F, ProcessItemArgs, ItemData] =
     processAttachments0[F](cfg, fts, analyser, regexNer, (30, 60, 90))(item)
 
-  def analysisOnly[F[_]: Sync: ContextShift](
+  def analysisOnly[F[_]: Async](
       cfg: Config,
       analyser: TextAnalyser[F],
       regexNer: RegexNerFile[F]
@@ -46,7 +46,7 @@ object ProcessItem {
       .flatMap(CrossCheckProposals[F])
       .flatMap(SaveProposals[F])
 
-  private def processAttachments0[F[_]: ConcurrentEffect: ContextShift](
+  private def processAttachments0[F[_]: Async](
       cfg: Config,
       fts: FtsClient[F],
       analyser: TextAnalyser[F],
diff --git a/modules/joex/src/main/scala/docspell/joex/process/ReProcessItem.scala b/modules/joex/src/main/scala/docspell/joex/process/ReProcessItem.scala
index 2f0188fc..49103c69 100644
--- a/modules/joex/src/main/scala/docspell/joex/process/ReProcessItem.scala
+++ b/modules/joex/src/main/scala/docspell/joex/process/ReProcessItem.scala
@@ -20,7 +20,7 @@ import docspell.store.records.RItem
 object ReProcessItem {
   type Args = ReProcessItemArgs
 
-  def apply[F[_]: ConcurrentEffect: ContextShift](
+  def apply[F[_]: Async](
       cfg: Config,
       fts: FtsClient[F],
       itemOps: OItem[F],
@@ -84,7 +84,7 @@ object ReProcessItem {
       )
     }
 
-  def processFiles[F[_]: ConcurrentEffect: ContextShift](
+  def processFiles[F[_]: Async](
       cfg: Config,
       fts: FtsClient[F],
       itemOps: OItem[F],
@@ -133,7 +133,7 @@ object ReProcessItem {
   def isLastRetry[F[_]: Sync]: Task[F, Args, Boolean] =
     Task(_.isLastRetry)
 
-  def safeProcess[F[_]: ConcurrentEffect: ContextShift](
+  def safeProcess[F[_]: Async](
       cfg: Config,
       fts: FtsClient[F],
       itemOps: OItem[F],
diff --git a/modules/joex/src/main/scala/docspell/joex/process/TextAnalysis.scala b/modules/joex/src/main/scala/docspell/joex/process/TextAnalysis.scala
index 33ec72d6..33c5e545 100644
--- a/modules/joex/src/main/scala/docspell/joex/process/TextAnalysis.scala
+++ b/modules/joex/src/main/scala/docspell/joex/process/TextAnalysis.scala
@@ -19,7 +19,7 @@ import docspell.store.records.{RAttachmentMeta, RClassifierSetting}
 object TextAnalysis {
   type Args = ProcessItemArgs
 
-  def apply[F[_]: Sync: ContextShift](
+  def apply[F[_]: Async](
       cfg: Config.TextAnalysis,
       analyser: TextAnalyser[F],
       nerFile: RegexNerFile[F]
@@ -78,7 +78,7 @@ object TextAnalysis {
     } yield (rm.copy(nerlabels = labels.all.toList), AttachmentDates(rm, labels.dates))
   }
 
-  def predictTags[F[_]: Sync: ContextShift](
+  def predictTags[F[_]: Async](
       ctx: Context[F, Args],
       cfg: Config.TextAnalysis,
       metas: Vector[RAttachmentMeta],
@@ -97,7 +97,7 @@ object TextAnalysis {
     } yield tags.flatten
   }
 
-  def predictItemEntities[F[_]: Sync: ContextShift](
+  def predictItemEntities[F[_]: Async](
       ctx: Context[F, Args],
       cfg: Config.TextAnalysis,
       metas: Vector[RAttachmentMeta],
@@ -128,13 +128,12 @@ object TextAnalysis {
       .map(MetaProposalList.apply)
   }
 
-  private def makeClassify[F[_]: Sync: ContextShift](
+  private def makeClassify[F[_]: Async](
       ctx: Context[F, Args],
       cfg: Config.TextAnalysis,
       classifier: TextClassifier[F]
   )(text: String): ClassifierName => F[Option[String]] =
     Classify[F](
-      ctx.blocker,
       ctx.logger,
       cfg.workingDir,
       ctx.store,
diff --git a/modules/joex/src/main/scala/docspell/joex/process/TextExtraction.scala b/modules/joex/src/main/scala/docspell/joex/process/TextExtraction.scala
index 2dcc4d31..dcb2ba28 100644
--- a/modules/joex/src/main/scala/docspell/joex/process/TextExtraction.scala
+++ b/modules/joex/src/main/scala/docspell/joex/process/TextExtraction.scala
@@ -15,7 +15,7 @@ import bitpeace.{Mimetype, RangeDef}
 
 object TextExtraction {
 
-  def apply[F[_]: ConcurrentEffect: ContextShift](cfg: ExtractConfig, fts: FtsClient[F])(
+  def apply[F[_]: Async](cfg: ExtractConfig, fts: FtsClient[F])(
       item: ItemData
   ): Task[F, ProcessItemArgs, ItemData] =
     Task { ctx =>
@@ -60,7 +60,7 @@ object TextExtraction {
 
   case class Result(am: RAttachmentMeta, td: TextData, tags: List[String] = Nil)
 
-  def extractTextIfEmpty[F[_]: Sync: ContextShift](
+  def extractTextIfEmpty[F[_]: Async](
       ctx: Context[F, ProcessItemArgs],
       cfg: ExtractConfig,
       lang: Language,
@@ -93,7 +93,7 @@ object TextExtraction {
     }
   }
 
-  def extractTextToMeta[F[_]: Sync: ContextShift](
+  def extractTextToMeta[F[_]: Async](
       ctx: Context[F, _],
       cfg: ExtractConfig,
       lang: Language,
@@ -132,13 +132,13 @@ object TextExtraction {
     def findMime: F[Mimetype] =
       OptionT(ctx.store.transact(RFileMeta.findById(fileId)))
         .map(_.mimetype)
-        .getOrElse(Mimetype.`application/octet-stream`)
+        .getOrElse(Mimetype.applicationOctetStream)
 
     findMime
       .flatMap(mt => extr.extractText(data, DataType(mt.toLocal), lang))
   }
 
-  private def extractTextFallback[F[_]: Sync: ContextShift](
+  private def extractTextFallback[F[_]: Async](
       ctx: Context[F, _],
       cfg: ExtractConfig,
       ra: RAttachment,
@@ -149,7 +149,7 @@ object TextExtraction {
         ctx.logger.error(s"Cannot extract text").map(_ => None)
 
       case id :: rest =>
-        val extr = Extraction.create[F](ctx.blocker, ctx.logger, cfg)
+        val extr = Extraction.create[F](ctx.logger, cfg)
 
         extractText[F](ctx, extr, lang)(id)
           .flatMap({
diff --git a/modules/joex/src/main/scala/docspell/joex/routes/JoexRoutes.scala b/modules/joex/src/main/scala/docspell/joex/routes/JoexRoutes.scala
index a5c5c04e..b3a87973 100644
--- a/modules/joex/src/main/scala/docspell/joex/routes/JoexRoutes.scala
+++ b/modules/joex/src/main/scala/docspell/joex/routes/JoexRoutes.scala
@@ -14,7 +14,7 @@ import org.http4s.dsl.Http4sDsl
 
 object JoexRoutes {
 
-  def apply[F[_]: ConcurrentEffect: Timer](app: JoexApp[F]): HttpRoutes[F] = {
+  def apply[F[_]: Async](app: JoexApp[F]): HttpRoutes[F] = {
     val dsl = new Http4sDsl[F] {}
     import dsl._
     HttpRoutes.of[F] {
@@ -34,8 +34,8 @@ object JoexRoutes {
 
       case POST -> Root / "shutdownAndExit" =>
         for {
-          _ <- ConcurrentEffect[F].start(
-            Timer[F].sleep(Duration.seconds(1).toScala) *> app.initShutdown
+          _ <- Async[F].start(
+            Temporal[F].sleep(Duration.seconds(1).toScala) *> app.initShutdown
           )
           resp <- Ok(BasicResult(true, "Shutdown initiated."))
         } yield resp
diff --git a/modules/joex/src/main/scala/docspell/joex/scheduler/Context.scala b/modules/joex/src/main/scala/docspell/joex/scheduler/Context.scala
index fc51759a..6720e7d4 100644
--- a/modules/joex/src/main/scala/docspell/joex/scheduler/Context.scala
+++ b/modules/joex/src/main/scala/docspell/joex/scheduler/Context.scala
@@ -31,45 +31,40 @@ trait Context[F[_], A] { self =>
       last = config.retries == current.getOrElse(0)
     } yield last
 
-  def blocker: Blocker
-
   def map[C](f: A => C)(implicit F: Functor[F]): Context[F, C] =
-    new Context.ContextImpl[F, C](f(args), logger, store, blocker, config, jobId)
+    new Context.ContextImpl[F, C](f(args), logger, store, config, jobId)
 }
 
 object Context {
   private[this] val log = getLogger
 
-  def create[F[_]: Functor, A](
+  def create[F[_]: Async, A](
       jobId: Ident,
       arg: A,
       config: SchedulerConfig,
       log: Logger[F],
-      store: Store[F],
-      blocker: Blocker
+      store: Store[F]
   ): Context[F, A] =
-    new ContextImpl(arg, log, store, blocker, config, jobId)
+    new ContextImpl(arg, log, store, config, jobId)
 
-  def apply[F[_]: Concurrent, A](
+  def apply[F[_]: Async, A](
       job: RJob,
       arg: A,
       config: SchedulerConfig,
       logSink: LogSink[F],
-      blocker: Blocker,
       store: Store[F]
   ): F[Context[F, A]] =
     for {
       _      <- log.ftrace("Creating logger for task run")
       logger <- QueueLogger(job.id, job.info, config.logBufferSize, logSink)
       _      <- log.ftrace("Logger created, instantiating context")
-      ctx = create[F, A](job.id, arg, config, logger, store, blocker)
+      ctx = create[F, A](job.id, arg, config, logger, store)
     } yield ctx
 
   final private class ContextImpl[F[_]: Functor, A](
       val args: A,
       val logger: Logger[F],
       val store: Store[F],
-      val blocker: Blocker,
       val config: SchedulerConfig,
       val jobId: Ident
   ) extends Context[F, A] {
diff --git a/modules/joex/src/main/scala/docspell/joex/scheduler/LogSink.scala b/modules/joex/src/main/scala/docspell/joex/scheduler/LogSink.scala
index ae5c7cdd..db04c3da 100644
--- a/modules/joex/src/main/scala/docspell/joex/scheduler/LogSink.scala
+++ b/modules/joex/src/main/scala/docspell/joex/scheduler/LogSink.scala
@@ -1,8 +1,8 @@
 package docspell.joex.scheduler
 
-import cats.effect.{Concurrent, Sync}
+import cats.effect._
 import cats.implicits._
-import fs2.{Pipe, Stream}
+import fs2.Pipe
 
 import docspell.common._
 import docspell.common.syntax.all._
@@ -45,7 +45,7 @@ object LogSink {
   def printer[F[_]: Sync]: LogSink[F] =
     LogSink(_.evalMap(e => logInternal(e)))
 
-  def db[F[_]: Sync](store: Store[F]): LogSink[F] =
+  def db[F[_]: Async](store: Store[F]): LogSink[F] =
     LogSink(
       _.evalMap(ev =>
         for {
@@ -63,9 +63,6 @@ object LogSink {
       )
     )
 
-  def dbAndLog[F[_]: Concurrent](store: Store[F]): LogSink[F] = {
-    val s: Stream[F, Pipe[F, LogEvent, Unit]] =
-      Stream.emits(Seq(printer[F].receive, db[F](store).receive))
-    LogSink(Pipe.join(s))
-  }
+  def dbAndLog[F[_]: Async](store: Store[F]): LogSink[F] =
+    LogSink(_.broadcastThrough(printer[F].receive, db[F](store).receive))
 }
diff --git a/modules/joex/src/main/scala/docspell/joex/scheduler/PeriodicScheduler.scala b/modules/joex/src/main/scala/docspell/joex/scheduler/PeriodicScheduler.scala
index cbc7ec22..17a0344f 100644
--- a/modules/joex/src/main/scala/docspell/joex/scheduler/PeriodicScheduler.scala
+++ b/modules/joex/src/main/scala/docspell/joex/scheduler/PeriodicScheduler.scala
@@ -24,20 +24,19 @@ trait PeriodicScheduler[F[_]] {
 
   def shutdown: F[Unit]
 
-  def periodicAwake: F[Fiber[F, Unit]]
+  def periodicAwake: F[Fiber[F, Throwable, Unit]]
 
   def notifyChange: F[Unit]
 }
 
 object PeriodicScheduler {
 
-  def create[F[_]: ConcurrentEffect](
+  def create[F[_]: Async](
       cfg: PeriodicSchedulerConfig,
       sch: Scheduler[F],
       queue: JobQueue[F],
       store: PeriodicTaskStore[F],
-      client: JoexClient[F],
-      timer: Timer[F]
+      client: JoexClient[F]
   ): Resource[F, PeriodicScheduler[F]] =
     for {
       waiter <- Resource.eval(SignallingRef(true))
@@ -49,8 +48,7 @@ object PeriodicScheduler {
         store,
         client,
         waiter,
-        state,
-        timer
+        state
       )
       _ <- Resource.eval(psch.init)
     } yield psch
diff --git a/modules/joex/src/main/scala/docspell/joex/scheduler/PeriodicSchedulerImpl.scala b/modules/joex/src/main/scala/docspell/joex/scheduler/PeriodicSchedulerImpl.scala
index c7ae4edd..3b6c0bfc 100644
--- a/modules/joex/src/main/scala/docspell/joex/scheduler/PeriodicSchedulerImpl.scala
+++ b/modules/joex/src/main/scala/docspell/joex/scheduler/PeriodicSchedulerImpl.scala
@@ -12,21 +12,19 @@ import docspell.joexapi.client.JoexClient
 import docspell.store.queue._
 import docspell.store.records.RPeriodicTask
 
-import com.github.eikek.fs2calev._
+import eu.timepit.fs2cron.calev.CalevScheduler
 import org.log4s.getLogger
 
-final class PeriodicSchedulerImpl[F[_]: ConcurrentEffect](
+final class PeriodicSchedulerImpl[F[_]: Async](
     val config: PeriodicSchedulerConfig,
     sch: Scheduler[F],
     queue: JobQueue[F],
     store: PeriodicTaskStore[F],
     client: JoexClient[F],
     waiter: SignallingRef[F, Boolean],
-    state: SignallingRef[F, State[F]],
-    timer: Timer[F]
+    state: SignallingRef[F, State[F]]
 ) extends PeriodicScheduler[F] {
-  private[this] val logger              = getLogger
-  implicit private val _timer: Timer[F] = timer
+  private[this] val logger = getLogger
 
   def start: Stream[F, Nothing] =
     logger.sinfo("Starting periodic scheduler") ++
@@ -35,8 +33,8 @@ final class PeriodicSchedulerImpl[F[_]: ConcurrentEffect](
   def shutdown: F[Unit] =
     state.modify(_.requestShutdown)
 
-  def periodicAwake: F[Fiber[F, Unit]] =
-    ConcurrentEffect[F].start(
+  def periodicAwake: F[Fiber[F, Throwable, Unit]] =
+    Async[F].start(
       Stream
         .awakeEvery[F](config.wakeupPeriod.toScala)
         .evalMap(_ => logger.fdebug("Periodic awake reached") *> notifyChange)
@@ -127,10 +125,11 @@ final class PeriodicSchedulerImpl[F[_]: ConcurrentEffect](
           s"Scheduling next notify for timer ${pj.timer.asString} -> ${pj.timer.nextElapse(now.toUtcDateTime)}"
         )
       ) *>
-      ConcurrentEffect[F]
+      Async[F]
         .start(
-          CalevFs2
-            .sleep[F](pj.timer)
+          CalevScheduler
+            .utc[F]
+            .sleep(pj.timer)
             .evalMap(_ => notifyChange)
             .compile
             .drain
@@ -168,15 +167,15 @@ object PeriodicSchedulerImpl {
 
   case class State[F[_]](
       shutdownRequest: Boolean,
-      scheduledNotify: Option[Fiber[F, Unit]]
+      scheduledNotify: Option[Fiber[F, Throwable, Unit]]
   ) {
     def requestShutdown: (State[F], Unit) =
       (copy(shutdownRequest = true), ())
 
-    def setNotify(fb: Fiber[F, Unit]): (State[F], Unit) =
+    def setNotify(fb: Fiber[F, Throwable, Unit]): (State[F], Unit) =
       (copy(scheduledNotify = Some(fb)), ())
 
-    def clearNotify: (State[F], Option[Fiber[F, Unit]]) =
+    def clearNotify: (State[F], Option[Fiber[F, Throwable, Unit]]) =
       (copy(scheduledNotify = None), scheduledNotify)
 
   }
diff --git a/modules/joex/src/main/scala/docspell/joex/scheduler/QueueLogger.scala b/modules/joex/src/main/scala/docspell/joex/scheduler/QueueLogger.scala
index f9e45264..a4fe2777 100644
--- a/modules/joex/src/main/scala/docspell/joex/scheduler/QueueLogger.scala
+++ b/modules/joex/src/main/scala/docspell/joex/scheduler/QueueLogger.scala
@@ -1,8 +1,9 @@
 package docspell.joex.scheduler
 
-import cats.effect.{Concurrent, Sync}
+import cats.effect._
+import cats.effect.std.Queue
 import cats.implicits._
-import fs2.concurrent.Queue
+import fs2.Stream
 
 import docspell.common._
 
@@ -15,28 +16,28 @@ object QueueLogger {
   ): Logger[F] =
     new Logger[F] {
       def trace(msg: => String): F[Unit] =
-        LogEvent.create[F](jobId, jobInfo, LogLevel.Debug, msg).flatMap(q.enqueue1)
+        LogEvent.create[F](jobId, jobInfo, LogLevel.Debug, msg).flatMap(q.offer)
 
       def debug(msg: => String): F[Unit] =
-        LogEvent.create[F](jobId, jobInfo, LogLevel.Debug, msg).flatMap(q.enqueue1)
+        LogEvent.create[F](jobId, jobInfo, LogLevel.Debug, msg).flatMap(q.offer)
 
       def info(msg: => String): F[Unit] =
-        LogEvent.create[F](jobId, jobInfo, LogLevel.Info, msg).flatMap(q.enqueue1)
+        LogEvent.create[F](jobId, jobInfo, LogLevel.Info, msg).flatMap(q.offer)
 
       def warn(msg: => String): F[Unit] =
-        LogEvent.create[F](jobId, jobInfo, LogLevel.Warn, msg).flatMap(q.enqueue1)
+        LogEvent.create[F](jobId, jobInfo, LogLevel.Warn, msg).flatMap(q.offer)
 
       def error(ex: Throwable)(msg: => String): F[Unit] =
         LogEvent
           .create[F](jobId, jobInfo, LogLevel.Error, msg)
           .map(le => le.copy(ex = Some(ex)))
-          .flatMap(q.enqueue1)
+          .flatMap(q.offer)
 
       def error(msg: => String): F[Unit] =
-        LogEvent.create[F](jobId, jobInfo, LogLevel.Error, msg).flatMap(q.enqueue1)
+        LogEvent.create[F](jobId, jobInfo, LogLevel.Error, msg).flatMap(q.offer)
     }
 
-  def apply[F[_]: Concurrent](
+  def apply[F[_]: Async](
       jobId: Ident,
       jobInfo: String,
       bufferSize: Int,
@@ -45,7 +46,9 @@ object QueueLogger {
     for {
       q <- Queue.circularBuffer[F, LogEvent](bufferSize)
       log = create(jobId, jobInfo, q)
-      _ <- Concurrent[F].start(q.dequeue.through(sink.receive).compile.drain)
+      _ <- Async[F].start(
+        Stream.fromQueueUnterminated(q).through(sink.receive).compile.drain
+      )
     } yield log
 
 }
diff --git a/modules/joex/src/main/scala/docspell/joex/scheduler/Scheduler.scala b/modules/joex/src/main/scala/docspell/joex/scheduler/Scheduler.scala
index 7558425c..9d261cd5 100644
--- a/modules/joex/src/main/scala/docspell/joex/scheduler/Scheduler.scala
+++ b/modules/joex/src/main/scala/docspell/joex/scheduler/Scheduler.scala
@@ -1,6 +1,6 @@
 package docspell.joex.scheduler
 
-import cats.effect.{Fiber, Timer}
+import cats.effect._
 import fs2.Stream
 
 import docspell.common.Ident
@@ -30,5 +30,5 @@ trait Scheduler[F[_]] {
     */
   def shutdown(cancelAll: Boolean): F[Unit]
 
-  def periodicAwake(implicit T: Timer[F]): F[Fiber[F, Unit]]
+  def periodicAwake: F[Fiber[F, Throwable, Unit]]
 }
diff --git a/modules/joex/src/main/scala/docspell/joex/scheduler/SchedulerBuilder.scala b/modules/joex/src/main/scala/docspell/joex/scheduler/SchedulerBuilder.scala
index 1a804b55..6a8ba93c 100644
--- a/modules/joex/src/main/scala/docspell/joex/scheduler/SchedulerBuilder.scala
+++ b/modules/joex/src/main/scala/docspell/joex/scheduler/SchedulerBuilder.scala
@@ -1,18 +1,17 @@
 package docspell.joex.scheduler
 
 import cats.effect._
-import cats.effect.concurrent.Semaphore
+import cats.effect.std.Semaphore
 import cats.implicits._
 import fs2.concurrent.SignallingRef
 
 import docspell.store.Store
 import docspell.store.queue.JobQueue
 
-case class SchedulerBuilder[F[_]: ConcurrentEffect: ContextShift](
+case class SchedulerBuilder[F[_]: Async](
     config: SchedulerConfig,
     tasks: JobTaskRegistry[F],
     store: Store[F],
-    blocker: Blocker,
     queue: Resource[F, JobQueue[F]],
     logSink: LogSink[F]
 ) {
@@ -27,10 +26,7 @@ case class SchedulerBuilder[F[_]: ConcurrentEffect: ContextShift](
     withTaskRegistry(tasks.withTask(task))
 
   def withQueue(queue: Resource[F, JobQueue[F]]): SchedulerBuilder[F] =
-    SchedulerBuilder[F](config, tasks, store, blocker, queue, logSink)
-
-  def withBlocker(blocker: Blocker): SchedulerBuilder[F] =
-    copy(blocker = blocker)
+    SchedulerBuilder[F](config, tasks, store, queue, logSink)
 
   def withLogSink(sink: LogSink[F]): SchedulerBuilder[F] =
     copy(logSink = sink)
@@ -39,19 +35,16 @@ case class SchedulerBuilder[F[_]: ConcurrentEffect: ContextShift](
     copy(queue = Resource.pure[F, JobQueue[F]](queue))
 
   def serve: Resource[F, Scheduler[F]] =
-    resource.evalMap(sch =>
-      ConcurrentEffect[F].start(sch.start.compile.drain).map(_ => sch)
-    )
+    resource.evalMap(sch => Async[F].start(sch.start.compile.drain).map(_ => sch))
 
   def resource: Resource[F, Scheduler[F]] = {
-    val scheduler = for {
+    val scheduler: Resource[F, SchedulerImpl[F]] = for {
       jq     <- queue
       waiter <- Resource.eval(SignallingRef(true))
       state  <- Resource.eval(SignallingRef(SchedulerImpl.emptyState[F]))
       perms  <- Resource.eval(Semaphore(config.poolSize.toLong))
     } yield new SchedulerImpl[F](
       config,
-      blocker,
       jq,
       tasks,
       store,
@@ -68,16 +61,14 @@ case class SchedulerBuilder[F[_]: ConcurrentEffect: ContextShift](
 
 object SchedulerBuilder {
 
-  def apply[F[_]: ConcurrentEffect: ContextShift](
+  def apply[F[_]: Async](
       config: SchedulerConfig,
-      blocker: Blocker,
       store: Store[F]
   ): SchedulerBuilder[F] =
     new SchedulerBuilder[F](
       config,
       JobTaskRegistry.empty[F],
       store,
-      blocker,
       JobQueue(store),
       LogSink.db[F](store)
     )
diff --git a/modules/joex/src/main/scala/docspell/joex/scheduler/SchedulerImpl.scala b/modules/joex/src/main/scala/docspell/joex/scheduler/SchedulerImpl.scala
index 17edf66b..06de35ba 100644
--- a/modules/joex/src/main/scala/docspell/joex/scheduler/SchedulerImpl.scala
+++ b/modules/joex/src/main/scala/docspell/joex/scheduler/SchedulerImpl.scala
@@ -2,7 +2,7 @@ package docspell.joex.scheduler
 
 import cats.data.OptionT
 import cats.effect._
-import cats.effect.concurrent.Semaphore
+import cats.effect.std.Semaphore
 import cats.implicits._
 import fs2.Stream
 import fs2.concurrent.SignallingRef
@@ -17,9 +17,8 @@ import docspell.store.records.RJob
 
 import org.log4s._
 
-final class SchedulerImpl[F[_]: ConcurrentEffect: ContextShift](
+final class SchedulerImpl[F[_]: Async](
     val config: SchedulerConfig,
-    blocker: Blocker,
     queue: JobQueue[F],
     tasks: JobTaskRegistry[F],
     store: Store[F],
@@ -37,8 +36,8 @@ final class SchedulerImpl[F[_]: ConcurrentEffect: ContextShift](
   def init: F[Unit] =
     QJob.runningToWaiting(config.name, store)
 
-  def periodicAwake(implicit T: Timer[F]): F[Fiber[F, Unit]] =
-    ConcurrentEffect[F].start(
+  def periodicAwake: F[Fiber[F, Throwable, Unit]] =
+    Async[F].start(
       Stream
         .awakeEvery[F](config.wakeupPeriod.toScala)
         .evalMap(_ => logger.fdebug("Periodic awake reached") *> notifyChange)
@@ -153,7 +152,7 @@ final class SchedulerImpl[F[_]: ConcurrentEffect: ContextShift](
         for {
           _ <-
             logger.fdebug(s"Creating context for job ${job.info} to run cancellation $t")
-          ctx <- Context[F, String](job, job.args, config, logSink, blocker, store)
+          ctx <- Context[F, String](job, job.args, config, logSink, store)
           _   <- t.onCancel.run(ctx)
           _   <- state.modify(_.markCancelled(job))
           _   <- onFinish(job, JobState.Cancelled)
@@ -177,7 +176,7 @@ final class SchedulerImpl[F[_]: ConcurrentEffect: ContextShift](
       case Right(t) =>
         for {
           _   <- logger.fdebug(s"Creating context for job ${job.info} to run $t")
-          ctx <- Context[F, String](job, job.args, config, logSink, blocker, store)
+          ctx <- Context[F, String](job, job.args, config, logSink, store)
           jot = wrapTask(job, t.task, ctx)
           tok <- forkRun(job, jot.run(ctx), t.onCancel.run(ctx), ctx)
           _   <- state.modify(_.addRunning(job, tok))
@@ -208,9 +207,7 @@ final class SchedulerImpl[F[_]: ConcurrentEffect: ContextShift](
       ctx: Context[F, String]
   ): Task[F, String, Unit] =
     task
-      .mapF(fa =>
-        onStart(job) *> logger.fdebug("Starting task now") *> blocker.blockOn(fa)
-      )
+      .mapF(fa => onStart(job) *> logger.fdebug("Starting task now") *> fa)
       .mapF(_.attempt.flatMap({
         case Right(()) =>
           logger.info(s"Job execution successful: ${job.info}")
@@ -252,11 +249,10 @@ final class SchedulerImpl[F[_]: ConcurrentEffect: ContextShift](
       code: F[Unit],
       onCancel: F[Unit],
       ctx: Context[F, String]
-  ): F[F[Unit]] = {
-    val bfa = blocker.blockOn(code)
+  ): F[F[Unit]] =
     logger.fdebug(s"Forking job ${job.info}") *>
-      ConcurrentEffect[F]
-        .start(bfa)
+      Async[F]
+        .start(code)
         .map(fiber =>
           logger.fdebug(s"Cancelling job ${job.info}") *>
             fiber.cancel *>
@@ -271,11 +267,12 @@ final class SchedulerImpl[F[_]: ConcurrentEffect: ContextShift](
             ctx.logger.warn("Job has been cancelled.") *>
             logger.fdebug(s"Job ${job.info} has been cancelled.")
         )
-  }
 }
 
 object SchedulerImpl {
 
+  type CancelToken[F[_]] = F[Unit]
+
   def emptyState[F[_]]: State[F] =
     State(Map.empty, Set.empty, Map.empty, false)
 
diff --git a/modules/joexapi/src/main/scala/docspell/joexapi/client/JoexClient.scala b/modules/joexapi/src/main/scala/docspell/joexapi/client/JoexClient.scala
index 69e7e87a..a1b479b3 100644
--- a/modules/joexapi/src/main/scala/docspell/joexapi/client/JoexClient.scala
+++ b/modules/joexapi/src/main/scala/docspell/joexapi/client/JoexClient.scala
@@ -9,9 +9,9 @@ import docspell.common.syntax.all._
 import docspell.common.{Ident, LenientUri}
 import docspell.joexapi.model.BasicResult
 
-import org.http4s.circe.CirceEntityDecoder._
+import org.http4s.blaze.client.BlazeClientBuilder
+import org.http4s.circe.CirceEntityDecoder
 import org.http4s.client.Client
-import org.http4s.client.blaze.BlazeClientBuilder
 import org.http4s.{Method, Request, Uri}
 import org.log4s.getLogger
 
@@ -29,8 +29,9 @@ object JoexClient {
 
   private[this] val logger = getLogger
 
-  def apply[F[_]: Sync](client: Client[F]): JoexClient[F] =
-    new JoexClient[F] {
+  def apply[F[_]: Async](client: Client[F]): JoexClient[F] =
+    new JoexClient[F] with CirceEntityDecoder {
+
       def notifyJoex(base: LenientUri): F[BasicResult] = {
         val notifyUrl = base / "api" / "v1" / "notify"
         val req       = Request[F](Method.POST, uri(notifyUrl))
@@ -62,6 +63,6 @@ object JoexClient {
         Uri.unsafeFromString(u.asString)
     }
 
-  def resource[F[_]: ConcurrentEffect](ec: ExecutionContext): Resource[F, JoexClient[F]] =
+  def resource[F[_]: Async](ec: ExecutionContext): Resource[F, JoexClient[F]] =
     BlazeClientBuilder[F](ec).resource.map(apply[F])
 }
diff --git a/modules/restserver/src/main/scala/docspell/restserver/Config.scala b/modules/restserver/src/main/scala/docspell/restserver/Config.scala
index 06b897df..3696610e 100644
--- a/modules/restserver/src/main/scala/docspell/restserver/Config.scala
+++ b/modules/restserver/src/main/scala/docspell/restserver/Config.scala
@@ -1,12 +1,12 @@
 package docspell.restserver
 
-import java.net.InetAddress
-
 import docspell.backend.auth.Login
 import docspell.backend.{Config => BackendConfig}
 import docspell.common._
 import docspell.ftssolr.SolrConfig
 
+import com.comcast.ip4s.IpAddress
+
 case class Config(
     appName: String,
     appId: Ident,
@@ -42,12 +42,14 @@ object Config {
     case class HttpHeader(enabled: Boolean, headerName: String, headerValue: String)
     case class AllowedIps(enabled: Boolean, ips: Set[String]) {
 
-      def containsAddress(inet: InetAddress): Boolean = {
-        val ip           = inet.getHostAddress
+      def containsAddress(inet: IpAddress): Boolean = {
+        val ip           = inet.fold(_.toUriString, _.toUriString) //.getHostAddress
         lazy val ipParts = ip.split('.')
 
         def checkSingle(pattern: String): Boolean =
-          pattern == ip || (inet.isLoopbackAddress && pattern == "127.0.0.1") || (pattern
+          pattern == ip || (ip.contains(
+            "localhost"
+          ) && pattern == "127.0.0.1") || (pattern
             .split('.')
             .zip(ipParts)
             .foldLeft(true) { case (r, (a, b)) =>
diff --git a/modules/restserver/src/main/scala/docspell/restserver/Main.scala b/modules/restserver/src/main/scala/docspell/restserver/Main.scala
index b52f1f27..fcbedf96 100644
--- a/modules/restserver/src/main/scala/docspell/restserver/Main.scala
+++ b/modules/restserver/src/main/scala/docspell/restserver/Main.scala
@@ -52,9 +52,8 @@ object Main extends IOApp {
     val pools = for {
       cec <- connectEC
       bec <- blockingEC
-      blocker = Blocker.liftExecutorService(bec)
       rec <- restserverEC
-    } yield Pools(cec, bec, blocker, rec)
+    } yield Pools(cec, bec, rec)
 
     logger.info(s"\n${banner.render("***>")}")
     if (EnvMode.current.isDev) {
diff --git a/modules/restserver/src/main/scala/docspell/restserver/RestAppImpl.scala b/modules/restserver/src/main/scala/docspell/restserver/RestAppImpl.scala
index a455dd76..34262f99 100644
--- a/modules/restserver/src/main/scala/docspell/restserver/RestAppImpl.scala
+++ b/modules/restserver/src/main/scala/docspell/restserver/RestAppImpl.scala
@@ -24,21 +24,20 @@ final class RestAppImpl[F[_]](val config: Config, val backend: BackendApp[F])
 
 object RestAppImpl {
 
-  def create[F[_]: ConcurrentEffect: ContextShift](
+  def create[F[_]: Async](
       cfg: Config,
       connectEC: ExecutionContext,
-      httpClientEc: ExecutionContext,
-      blocker: Blocker
+      httpClientEc: ExecutionContext
   ): Resource[F, RestApp[F]] =
     for {
-      backend <- BackendApp(cfg.backend, connectEC, httpClientEc, blocker)(
+      backend <- BackendApp(cfg.backend, connectEC, httpClientEc)(
         createFtsClient[F](cfg)
       )
       app = new RestAppImpl[F](cfg, backend)
       appR <- Resource.make(app.init.map(_ => app))(_.shutdown)
     } yield appR
 
-  private def createFtsClient[F[_]: ConcurrentEffect](
+  private def createFtsClient[F[_]: Async](
       cfg: Config
   )(client: Client[F]): Resource[F, FtsClient[F]] =
     if (cfg.fullTextSearch.enabled) SolrFtsClient(cfg.fullTextSearch.solr, client)
diff --git a/modules/restserver/src/main/scala/docspell/restserver/RestServer.scala b/modules/restserver/src/main/scala/docspell/restserver/RestServer.scala
index 7891cb56..a54bc7f3 100644
--- a/modules/restserver/src/main/scala/docspell/restserver/RestServer.scala
+++ b/modules/restserver/src/main/scala/docspell/restserver/RestServer.scala
@@ -11,36 +11,33 @@ import docspell.restserver.routes._
 import docspell.restserver.webapp._
 
 import org.http4s._
+import org.http4s.blaze.server.BlazeServerBuilder
 import org.http4s.dsl.Http4sDsl
 import org.http4s.headers.Location
 import org.http4s.implicits._
 import org.http4s.server.Router
-import org.http4s.server.blaze.BlazeServerBuilder
 import org.http4s.server.middleware.Logger
 
 object RestServer {
 
-  def stream[F[_]: ConcurrentEffect](
-      cfg: Config,
-      pools: Pools
-  )(implicit T: Timer[F], CS: ContextShift[F]): Stream[F, Nothing] = {
+  def stream[F[_]: Async](cfg: Config, pools: Pools): Stream[F, Nothing] = {
 
-    val templates = TemplateRoutes[F](pools.blocker, cfg)
+    val templates = TemplateRoutes[F](cfg)
     val app = for {
       restApp <-
         RestAppImpl
-          .create[F](cfg, pools.connectEC, pools.httpClientEC, pools.blocker)
+          .create[F](cfg, pools.connectEC, pools.httpClientEC)
       httpApp = Router(
         "/api/info"     -> routes.InfoRoutes(),
         "/api/v1/open/" -> openRoutes(cfg, restApp),
         "/api/v1/sec/" -> Authenticate(restApp.backend.login, cfg.auth) { token =>
-          securedRoutes(cfg, pools, restApp, token)
+          securedRoutes(cfg, restApp, token)
         },
         "/api/v1/admin" -> AdminRoutes(cfg.adminEndpoint) {
           adminRoutes(cfg, restApp)
         },
         "/api/doc"    -> templates.doc,
-        "/app/assets" -> EnvMiddleware(WebjarRoutes.appRoutes[F](pools.blocker)),
+        "/app/assets" -> EnvMiddleware(WebjarRoutes.appRoutes[F]),
         "/app"        -> EnvMiddleware(templates.app),
         "/sw.js"      -> EnvMiddleware(templates.serviceWorker),
         "/"           -> redirectTo("/app")
@@ -61,9 +58,8 @@ object RestServer {
       )
   }.drain
 
-  def securedRoutes[F[_]: Effect: ContextShift](
+  def securedRoutes[F[_]: Async](
       cfg: Config,
-      pools: Pools,
       restApp: RestApp[F],
       token: AuthToken
   ): HttpRoutes[F] =
@@ -77,9 +73,9 @@ object RestServer {
       "user"                    -> UserRoutes(restApp.backend, token),
       "collective"              -> CollectiveRoutes(restApp.backend, token),
       "queue"                   -> JobQueueRoutes(restApp.backend, token),
-      "item"                    -> ItemRoutes(cfg, pools.blocker, restApp.backend, token),
+      "item"                    -> ItemRoutes(cfg, restApp.backend, token),
       "items"                   -> ItemMultiRoutes(restApp.backend, token),
-      "attachment"              -> AttachmentRoutes(pools.blocker, restApp.backend, token),
+      "attachment"              -> AttachmentRoutes(restApp.backend, token),
       "attachments"             -> AttachmentMultiRoutes(restApp.backend, token),
       "upload"                  -> UploadRoutes.secured(restApp.backend, cfg, token),
       "checkfile"               -> CheckFileRoutes.secured(restApp.backend, token),
@@ -95,7 +91,7 @@ object RestServer {
       "clientSettings"          -> ClientSettingsRoutes(restApp.backend, token)
     )
 
-  def openRoutes[F[_]: Effect](cfg: Config, restApp: RestApp[F]): HttpRoutes[F] =
+  def openRoutes[F[_]: Async](cfg: Config, restApp: RestApp[F]): HttpRoutes[F] =
     Router(
       "auth"        -> LoginRoutes.login(restApp.backend.login, cfg),
       "signup"      -> RegisterRoutes(restApp.backend, cfg),
@@ -104,14 +100,14 @@ object RestServer {
       "integration" -> IntegrationEndpointRoutes.open(restApp.backend, cfg)
     )
 
-  def adminRoutes[F[_]: Effect](cfg: Config, restApp: RestApp[F]): HttpRoutes[F] =
+  def adminRoutes[F[_]: Async](cfg: Config, restApp: RestApp[F]): HttpRoutes[F] =
     Router(
       "fts"  -> FullTextIndexRoutes.admin(cfg, restApp.backend),
       "user" -> UserRoutes.admin(restApp.backend),
       "info" -> InfoRoutes.admin(cfg)
     )
 
-  def redirectTo[F[_]: Effect](path: String): HttpRoutes[F] = {
+  def redirectTo[F[_]: Async](path: String): HttpRoutes[F] = {
     val dsl = new Http4sDsl[F] {}
     import dsl._
 
@@ -119,7 +115,7 @@ object RestServer {
       Response[F](
         Status.SeeOther,
         body = Stream.empty,
-        headers = Headers.of(Location(Uri(path = path)))
+        headers = Headers(Location(Uri(path = Uri.Path.unsafeFromString(path))))
       ).pure[F]
     }
   }
diff --git a/modules/restserver/src/main/scala/docspell/restserver/auth/CookieData.scala b/modules/restserver/src/main/scala/docspell/restserver/auth/CookieData.scala
index 2b744f27..60528e24 100644
--- a/modules/restserver/src/main/scala/docspell/restserver/auth/CookieData.scala
+++ b/modules/restserver/src/main/scala/docspell/restserver/auth/CookieData.scala
@@ -5,7 +5,7 @@ import docspell.common.AccountId
 import docspell.common.LenientUri
 
 import org.http4s._
-import org.http4s.util._
+import org.typelevel.ci.CIString
 
 case class CookieData(auth: AuthToken) {
   def accountId: AccountId = auth.account
@@ -37,7 +37,7 @@ object CookieData {
 
   def fromCookie[F[_]](req: Request[F]): Either[String, String] =
     for {
-      header <- headers.Cookie.from(req.headers).toRight("Cookie parsing error")
+      header <- req.headers.get[headers.Cookie].toRight("Cookie parsing error")
       cookie <-
         header.values.toList
           .find(_.name == cookieName)
@@ -46,8 +46,8 @@ object CookieData {
 
   def fromHeader[F[_]](req: Request[F]): Either[String, String] =
     req.headers
-      .get(CaseInsensitiveString(headerName))
-      .map(_.value)
+      .get(CIString(headerName))
+      .map(_.head.value)
       .toRight("Couldn't find an authenticator")
 
   def deleteCookie(baseUrl: LenientUri): ResponseCookie =
diff --git a/modules/restserver/src/main/scala/docspell/restserver/auth/RememberCookieData.scala b/modules/restserver/src/main/scala/docspell/restserver/auth/RememberCookieData.scala
index 7aaebaaa..79d03038 100644
--- a/modules/restserver/src/main/scala/docspell/restserver/auth/RememberCookieData.scala
+++ b/modules/restserver/src/main/scala/docspell/restserver/auth/RememberCookieData.scala
@@ -33,7 +33,7 @@ object RememberCookieData {
 
   def fromCookie[F[_]](req: Request[F]): Option[String] =
     for {
-      header <- headers.Cookie.from(req.headers)
+      header <- req.headers.get[headers.Cookie]
       cookie <- header.values.toList.find(_.name == cookieName)
     } yield cookie.content
 
diff --git a/modules/restserver/src/main/scala/docspell/restserver/conv/Conversions.scala b/modules/restserver/src/main/scala/docspell/restserver/conv/Conversions.scala
index 99b01138..e990ff6a 100644
--- a/modules/restserver/src/main/scala/docspell/restserver/conv/Conversions.scala
+++ b/modules/restserver/src/main/scala/docspell/restserver/conv/Conversions.scala
@@ -2,7 +2,7 @@ package docspell.restserver.conv
 
 import java.time.{LocalDate, ZoneId}
 
-import cats.effect.{Effect, Sync}
+import cats.effect.{Async, Sync}
 import cats.implicits._
 import fs2.Stream
 
@@ -294,7 +294,7 @@ trait Conversions {
     JobLogEvent(jl.created, jl.level, jl.message)
 
   // upload
-  def readMultipart[F[_]: Effect](
+  def readMultipart[F[_]: Async](
       mp: Multipart[F],
       sourceName: String,
       logger: Logger,
@@ -347,11 +347,11 @@ trait Conversions {
       .filter(p => p.name.forall(s => !s.equalsIgnoreCase("meta")))
       .map(p =>
         OUpload
-          .File(p.filename, p.headers.get(`Content-Type`).map(fromContentType), p.body)
+          .File(p.filename, p.headers.get[`Content-Type`].map(fromContentType), p.body)
       )
     for {
       metaData <- meta
-      _        <- Effect[F].delay(logger.debug(s"Parsed upload meta data: $metaData"))
+      _        <- Async[F].delay(logger.debug(s"Parsed upload meta data: $metaData"))
       tracker  <- Ident.randomId[F]
     } yield UploadData(metaData._1, metaData._2, files, prio, Some(tracker))
   }
diff --git a/modules/restserver/src/main/scala/docspell/restserver/http4s/BinaryUtil.scala b/modules/restserver/src/main/scala/docspell/restserver/http4s/BinaryUtil.scala
index 152391b8..2f2e1962 100644
--- a/modules/restserver/src/main/scala/docspell/restserver/http4s/BinaryUtil.scala
+++ b/modules/restserver/src/main/scala/docspell/restserver/http4s/BinaryUtil.scala
@@ -13,6 +13,7 @@ import org.http4s.circe.CirceEntityEncoder._
 import org.http4s.dsl.Http4sDsl
 import org.http4s.headers.ETag.EntityTag
 import org.http4s.headers._
+import org.typelevel.ci.CIString
 
 object BinaryUtil {
 
@@ -21,12 +22,15 @@ object BinaryUtil {
   ): F[Response[F]] = {
     import dsl._
 
-    val mt             = MediaType.unsafeParse(data.meta.mimetype.asString)
-    val ctype          = `Content-Type`(mt)
-    val cntLen: Header = `Content-Length`.unsafeFromLong(data.meta.length)
-    val eTag: Header   = ETag(data.meta.checksum)
-    val disp: Header =
-      `Content-Disposition`("inline", Map("filename" -> data.name.getOrElse("")))
+    val mt     = MediaType.unsafeParse(data.meta.mimetype.asString)
+    val ctype  = `Content-Type`(mt)
+    val cntLen = `Content-Length`.unsafeFromLong(data.meta.length)
+    val eTag   = ETag(data.meta.checksum)
+    val disp =
+      `Content-Disposition`(
+        "inline",
+        Map(CIString("filename") -> data.name.getOrElse(""))
+      )
 
     resp.map(r =>
       if (r.status == NotModified) r.withHeaders(ctype, eTag, disp)
@@ -52,13 +56,9 @@ object BinaryUtil {
         false
     }
 
-  def noPreview[F[_]: Sync: ContextShift](
-      blocker: Blocker,
-      req: Option[Request[F]]
-  ): OptionT[F, Response[F]] =
+  def noPreview[F[_]: Async](req: Option[Request[F]]): OptionT[F, Response[F]] =
     StaticFile.fromResource(
       name = "/docspell/restserver/no-preview.svg",
-      blocker = blocker,
       req = req,
       preferGzipped = true,
       classloader = getClass.getClassLoader().some
diff --git a/modules/restserver/src/main/scala/docspell/restserver/http4s/ClientRequestInfo.scala b/modules/restserver/src/main/scala/docspell/restserver/http4s/ClientRequestInfo.scala
index a98926a0..8c6256da 100644
--- a/modules/restserver/src/main/scala/docspell/restserver/http4s/ClientRequestInfo.scala
+++ b/modules/restserver/src/main/scala/docspell/restserver/http4s/ClientRequestInfo.scala
@@ -8,7 +8,7 @@ import docspell.restserver.Config
 
 import org.http4s._
 import org.http4s.headers._
-import org.http4s.util.CaseInsensitiveString
+import org.typelevel.ci.CIString
 
 /** Obtain information about the client by inspecting the request.
   */
@@ -35,23 +35,23 @@ object ClientRequestInfo {
     xForwardedProto(req).orElse(clientConnectionProto(req))
 
   private def host[F[_]](req: Request[F]): Option[String] =
-    req.headers.get(Host).map(_.host)
+    req.headers.get[Host].map(_.host)
 
   private def xForwardedFor[F[_]](req: Request[F]): Option[String] =
     req.headers
-      .get(`X-Forwarded-For`)
+      .get[`X-Forwarded-For`]
       .flatMap(_.values.head)
-      .flatMap(inet => Option(inet.getHostName).orElse(Option(inet.getHostAddress)))
+      .map(ip => ip.fold(_.toUriString, _.toUriString))
 
   private def xForwardedHost[F[_]](req: Request[F]): Option[String] =
     req.headers
-      .get(CaseInsensitiveString("X-Forwarded-Host"))
-      .map(_.value)
+      .get(CIString("X-Forwarded-Host"))
+      .map(_.head.value)
 
   private def xForwardedProto[F[_]](req: Request[F]): Option[String] =
     req.headers
-      .get(CaseInsensitiveString("X-Forwarded-Proto"))
-      .map(_.value)
+      .get(CIString("X-Forwarded-Proto"))
+      .map(_.head.value)
 
   private def clientConnectionProto[F[_]](req: Request[F]): Option[String] =
     req.isSecure.map {
@@ -61,8 +61,8 @@ object ClientRequestInfo {
 
   private def xForwardedPort[F[_]](req: Request[F]): Option[Int] =
     req.headers
-      .get(CaseInsensitiveString("X-Forwarded-Port"))
-      .map(_.value)
+      .get(CIString("X-Forwarded-Port"))
+      .map(_.head.value)
       .flatMap(str => Either.catchNonFatal(str.toInt).toOption)
 
 }
diff --git a/modules/restserver/src/main/scala/docspell/restserver/http4s/NoCacheMiddleware.scala b/modules/restserver/src/main/scala/docspell/restserver/http4s/NoCacheMiddleware.scala
index 58360186..00a450e3 100644
--- a/modules/restserver/src/main/scala/docspell/restserver/http4s/NoCacheMiddleware.scala
+++ b/modules/restserver/src/main/scala/docspell/restserver/http4s/NoCacheMiddleware.scala
@@ -10,7 +10,7 @@ import org.http4s._
 import org.http4s.headers._
 
 object NoCacheMiddleware {
-  private val noCacheHeader: Header =
+  private val noCacheHeader =
     `Cache-Control`(
       NonEmptyList.of(
         CacheDirective.`max-age`(Duration.zero.toScala),
diff --git a/modules/restserver/src/main/scala/docspell/restserver/http4s/ResponseGenerator.scala b/modules/restserver/src/main/scala/docspell/restserver/http4s/ResponseGenerator.scala
index 70fad0b6..f5d23627 100644
--- a/modules/restserver/src/main/scala/docspell/restserver/http4s/ResponseGenerator.scala
+++ b/modules/restserver/src/main/scala/docspell/restserver/http4s/ResponseGenerator.scala
@@ -10,7 +10,7 @@ trait ResponseGenerator[F[_]] {
   self: Http4sDsl[F] =>
 
   implicit final class EitherResponses[A, B](e: Either[A, B]) {
-    def toResponse(headers: Header*)(implicit
+    def toResponse(headers: Header.ToRaw*)(implicit
         F: Applicative[F],
         w0: EntityEncoder[F, A],
         w1: EntityEncoder[F, B]
@@ -23,7 +23,7 @@ trait ResponseGenerator[F[_]] {
 
   implicit final class OptionResponse[A](o: Option[A]) {
     def toResponse(
-        headers: Header*
+        headers: Header.ToRaw*
     )(implicit F: Applicative[F], w0: EntityEncoder[F, A]): F[Response[F]] =
       o.map(a => Ok(a)).getOrElse(NotFound()).map(_.withHeaders(headers: _*))
   }
diff --git a/modules/restserver/src/main/scala/docspell/restserver/http4s/Responses.scala b/modules/restserver/src/main/scala/docspell/restserver/http4s/Responses.scala
index 7a722d44..1ad57b8e 100644
--- a/modules/restserver/src/main/scala/docspell/restserver/http4s/Responses.scala
+++ b/modules/restserver/src/main/scala/docspell/restserver/http4s/Responses.scala
@@ -26,10 +26,10 @@ object Responses {
     )
 
   def forbidden[F[_]]: Response[F] =
-    pureForbidden.copy(body = pureForbidden.body.covary[F])
+    pureForbidden.covary[F].copy(body = pureForbidden.body.covary[F])
 
   def unauthorized[F[_]]: Response[F] =
-    pureUnauthorized.copy(body = pureUnauthorized.body.covary[F])
+    pureUnauthorized.covary[F].copy(body = pureUnauthorized.body.covary[F])
 
   def noCache[F[_]](r: Response[F]): Response[F] =
     r.withHeaders(
diff --git a/modules/restserver/src/main/scala/docspell/restserver/routes/AdminRoutes.scala b/modules/restserver/src/main/scala/docspell/restserver/routes/AdminRoutes.scala
index fe3d1f5d..70050551 100644
--- a/modules/restserver/src/main/scala/docspell/restserver/routes/AdminRoutes.scala
+++ b/modules/restserver/src/main/scala/docspell/restserver/routes/AdminRoutes.scala
@@ -11,12 +11,12 @@ import org.http4s._
 import org.http4s.circe.CirceEntityEncoder._
 import org.http4s.dsl.Http4sDsl
 import org.http4s.server._
-import org.http4s.util.CaseInsensitiveString
+import org.typelevel.ci.CIString
 
 object AdminRoutes {
-  private val adminHeader = CaseInsensitiveString("Docspell-Admin-Secret")
+  private val adminHeader = CIString("Docspell-Admin-Secret")
 
-  def apply[F[_]: Effect](cfg: Config.AdminEndpoint)(
+  def apply[F[_]: Async](cfg: Config.AdminEndpoint)(
       f: HttpRoutes[F]
   ): HttpRoutes[F] = {
     val dsl: Http4sDsl[F] = new Http4sDsl[F] {}
@@ -34,7 +34,7 @@ object AdminRoutes {
     else middleware(AuthedRoutes(authReq => f.run(authReq.req)))
   }
 
-  private def checkSecret[F[_]: Effect](
+  private def checkSecret[F[_]: Async](
       cfg: Config.AdminEndpoint
   ): Kleisli[F, Request[F], Either[String, Unit]] =
     Kleisli(req =>
@@ -46,7 +46,7 @@ object AdminRoutes {
     )
 
   private def extractSecret[F[_]](req: Request[F]): Option[String] =
-    req.headers.get(adminHeader).map(_.value)
+    req.headers.get(adminHeader).map(_.head.value)
 
   private def compareSecret(s1: String)(s2: String): Boolean =
     s1.length > 0 && s1.length == s2.length &&
diff --git a/modules/restserver/src/main/scala/docspell/restserver/routes/AttachmentMultiRoutes.scala b/modules/restserver/src/main/scala/docspell/restserver/routes/AttachmentMultiRoutes.scala
index e2f125fd..0c90436d 100644
--- a/modules/restserver/src/main/scala/docspell/restserver/routes/AttachmentMultiRoutes.scala
+++ b/modules/restserver/src/main/scala/docspell/restserver/routes/AttachmentMultiRoutes.scala
@@ -1,6 +1,6 @@
 package docspell.restserver.routes
 
-import cats.effect.Effect
+import cats.effect.Async
 import cats.implicits._
 
 import docspell.backend.BackendApp
@@ -15,7 +15,7 @@ import org.http4s.dsl.Http4sDsl
 
 object AttachmentMultiRoutes extends MultiIdSupport {
 
-  def apply[F[_]: Effect](
+  def apply[F[_]: Async](
       backend: BackendApp[F],
       user: AuthToken
   ): HttpRoutes[F] = {
diff --git a/modules/restserver/src/main/scala/docspell/restserver/routes/AttachmentRoutes.scala b/modules/restserver/src/main/scala/docspell/restserver/routes/AttachmentRoutes.scala
index 34e09ba3..bb882d69 100644
--- a/modules/restserver/src/main/scala/docspell/restserver/routes/AttachmentRoutes.scala
+++ b/modules/restserver/src/main/scala/docspell/restserver/routes/AttachmentRoutes.scala
@@ -22,8 +22,7 @@ import org.http4s.headers._
 
 object AttachmentRoutes {
 
-  def apply[F[_]: Effect: ContextShift](
-      blocker: Blocker,
+  def apply[F[_]: Async](
       backend: BackendApp[F],
       user: AuthToken
   ): HttpRoutes[F] = {
@@ -51,7 +50,7 @@ object AttachmentRoutes {
       case req @ GET -> Root / Ident(id) =>
         for {
           fileData <- backend.itemSearch.findAttachment(id, user.account.collective)
-          inm     = req.headers.get(`If-None-Match`).flatMap(_.tags)
+          inm     = req.headers.get[`If-None-Match`].flatMap(_.tags)
           matches = BinaryUtil.matchETag(fileData.map(_.meta), inm)
           resp <-
             fileData
@@ -74,7 +73,7 @@ object AttachmentRoutes {
       case req @ GET -> Root / Ident(id) / "original" =>
         for {
           fileData <- backend.itemSearch.findAttachmentSource(id, user.account.collective)
-          inm     = req.headers.get(`If-None-Match`).flatMap(_.tags)
+          inm     = req.headers.get[`If-None-Match`].flatMap(_.tags)
           matches = BinaryUtil.matchETag(fileData.map(_.meta), inm)
           resp <-
             fileData
@@ -99,7 +98,7 @@ object AttachmentRoutes {
         for {
           fileData <-
             backend.itemSearch.findAttachmentArchive(id, user.account.collective)
-          inm     = req.headers.get(`If-None-Match`).flatMap(_.tags)
+          inm     = req.headers.get[`If-None-Match`].flatMap(_.tags)
           matches = BinaryUtil.matchETag(fileData.map(_.meta), inm)
           resp <-
             fileData
@@ -116,7 +115,7 @@ object AttachmentRoutes {
         for {
           fileData <-
             backend.itemSearch.findAttachmentPreview(id, user.account.collective)
-          inm      = req.headers.get(`If-None-Match`).flatMap(_.tags)
+          inm      = req.headers.get[`If-None-Match`].flatMap(_.tags)
           matches  = BinaryUtil.matchETag(fileData.map(_.meta), inm)
           fallback = flag.getOrElse(false)
           resp <-
@@ -126,7 +125,7 @@ object AttachmentRoutes {
                 else makeByteResp(data)
               }
               .getOrElse(
-                if (fallback) BinaryUtil.noPreview(blocker, req.some).getOrElseF(notFound)
+                if (fallback) BinaryUtil.noPreview(req.some).getOrElseF(notFound)
                 else notFound
               )
         } yield resp
@@ -158,7 +157,7 @@ object AttachmentRoutes {
         // it redirects currently to viewerjs
         val attachUrl = s"/api/v1/sec/attachment/${id.id}"
         val path      = s"/app/assets${Webjars.viewerjs}/ViewerJS/index.html#$attachUrl"
-        SeeOther(Location(Uri(path = path)))
+        SeeOther(Location(Uri(path = Uri.Path.unsafeFromString(path))))
 
       case GET -> Root / Ident(id) / "meta" =>
         for {
diff --git a/modules/restserver/src/main/scala/docspell/restserver/routes/Authenticate.scala b/modules/restserver/src/main/scala/docspell/restserver/routes/Authenticate.scala
index 5fe16414..a51d40f9 100644
--- a/modules/restserver/src/main/scala/docspell/restserver/routes/Authenticate.scala
+++ b/modules/restserver/src/main/scala/docspell/restserver/routes/Authenticate.scala
@@ -14,7 +14,7 @@ import org.http4s.server._
 
 object Authenticate {
 
-  def authenticateRequest[F[_]: Effect](
+  def authenticateRequest[F[_]: Async](
       auth: (String, Option[String]) => F[Login.Result]
   )(req: Request[F]): F[Login.Result] =
     CookieData.authenticator(req) match {
@@ -30,7 +30,7 @@ object Authenticate {
         }
     }
 
-  def of[F[_]: Effect](S: Login[F], cfg: Login.Config)(
+  def of[F[_]: Async](S: Login[F], cfg: Login.Config)(
       pf: PartialFunction[AuthedRequest[F, AuthToken], F[Response[F]]]
   ): HttpRoutes[F] = {
     val dsl: Http4sDsl[F] = new Http4sDsl[F] {}
@@ -47,7 +47,7 @@ object Authenticate {
     middleware(AuthedRoutes.of(pf))
   }
 
-  def apply[F[_]: Effect](S: Login[F], cfg: Login.Config)(
+  def apply[F[_]: Async](S: Login[F], cfg: Login.Config)(
       f: AuthToken => HttpRoutes[F]
   ): HttpRoutes[F] = {
     val dsl: Http4sDsl[F] = new Http4sDsl[F] {}
@@ -64,7 +64,7 @@ object Authenticate {
     middleware(AuthedRoutes(authReq => f(authReq.context).run(authReq.req)))
   }
 
-  private def getUser[F[_]: Effect](
+  private def getUser[F[_]: Async](
       auth: (String, Option[String]) => F[Login.Result]
   ): Kleisli[F, Request[F], Either[String, AuthToken]] =
     Kleisli(r => authenticateRequest(auth)(r).map(_.toEither))
diff --git a/modules/restserver/src/main/scala/docspell/restserver/routes/CalEventCheckRoutes.scala b/modules/restserver/src/main/scala/docspell/restserver/routes/CalEventCheckRoutes.scala
index 70a8570e..098eba3a 100644
--- a/modules/restserver/src/main/scala/docspell/restserver/routes/CalEventCheckRoutes.scala
+++ b/modules/restserver/src/main/scala/docspell/restserver/routes/CalEventCheckRoutes.scala
@@ -14,7 +14,7 @@ import org.http4s.dsl.Http4sDsl
 
 object CalEventCheckRoutes {
 
-  def apply[F[_]: Effect](): HttpRoutes[F] = {
+  def apply[F[_]: Async](): HttpRoutes[F] = {
     val dsl = new Http4sDsl[F] {}
     import dsl._
 
diff --git a/modules/restserver/src/main/scala/docspell/restserver/routes/CheckFileRoutes.scala b/modules/restserver/src/main/scala/docspell/restserver/routes/CheckFileRoutes.scala
index 02e735cf..5ed78058 100644
--- a/modules/restserver/src/main/scala/docspell/restserver/routes/CheckFileRoutes.scala
+++ b/modules/restserver/src/main/scala/docspell/restserver/routes/CheckFileRoutes.scala
@@ -16,7 +16,7 @@ import org.http4s.dsl.Http4sDsl
 
 object CheckFileRoutes {
 
-  def secured[F[_]: Effect](backend: BackendApp[F], user: AuthToken): HttpRoutes[F] = {
+  def secured[F[_]: Async](backend: BackendApp[F], user: AuthToken): HttpRoutes[F] = {
     val dsl = new Http4sDsl[F] with ResponseGenerator[F] {}
     import dsl._
 
@@ -30,7 +30,7 @@ object CheckFileRoutes {
     }
   }
 
-  def open[F[_]: Effect](backend: BackendApp[F]): HttpRoutes[F] = {
+  def open[F[_]: Async](backend: BackendApp[F]): HttpRoutes[F] = {
     val dsl = new Http4sDsl[F] with ResponseGenerator[F] {}
     import dsl._
 
diff --git a/modules/restserver/src/main/scala/docspell/restserver/routes/ClientSettingsRoutes.scala b/modules/restserver/src/main/scala/docspell/restserver/routes/ClientSettingsRoutes.scala
index 3672a35b..c3db37bb 100644
--- a/modules/restserver/src/main/scala/docspell/restserver/routes/ClientSettingsRoutes.scala
+++ b/modules/restserver/src/main/scala/docspell/restserver/routes/ClientSettingsRoutes.scala
@@ -16,7 +16,7 @@ import org.http4s.dsl.Http4sDsl
 
 object ClientSettingsRoutes {
 
-  def apply[F[_]: Effect](backend: BackendApp[F], user: AuthToken): HttpRoutes[F] = {
+  def apply[F[_]: Async](backend: BackendApp[F], user: AuthToken): HttpRoutes[F] = {
     val dsl = new Http4sDsl[F] {}
     import dsl._
 
diff --git a/modules/restserver/src/main/scala/docspell/restserver/routes/CollectiveRoutes.scala b/modules/restserver/src/main/scala/docspell/restserver/routes/CollectiveRoutes.scala
index 663ca46b..57eb8292 100644
--- a/modules/restserver/src/main/scala/docspell/restserver/routes/CollectiveRoutes.scala
+++ b/modules/restserver/src/main/scala/docspell/restserver/routes/CollectiveRoutes.scala
@@ -19,7 +19,7 @@ import org.http4s.dsl.Http4sDsl
 
 object CollectiveRoutes {
 
-  def apply[F[_]: Effect](backend: BackendApp[F], user: AuthToken): HttpRoutes[F] = {
+  def apply[F[_]: Async](backend: BackendApp[F], user: AuthToken): HttpRoutes[F] = {
     val dsl = new Http4sDsl[F] with ResponseGenerator[F] {}
     import dsl._
 
diff --git a/modules/restserver/src/main/scala/docspell/restserver/routes/CustomFieldRoutes.scala b/modules/restserver/src/main/scala/docspell/restserver/routes/CustomFieldRoutes.scala
index 0aab3c17..cfcb3bc7 100644
--- a/modules/restserver/src/main/scala/docspell/restserver/routes/CustomFieldRoutes.scala
+++ b/modules/restserver/src/main/scala/docspell/restserver/routes/CustomFieldRoutes.scala
@@ -23,7 +23,7 @@ import org.http4s.dsl.Http4sDsl
 
 object CustomFieldRoutes {
 
-  def apply[F[_]: Effect](backend: BackendApp[F], user: AuthToken): HttpRoutes[F] = {
+  def apply[F[_]: Async](backend: BackendApp[F], user: AuthToken): HttpRoutes[F] = {
     val dsl = new Http4sDsl[F] with ResponseGenerator[F] {}
     import dsl._
 
diff --git a/modules/restserver/src/main/scala/docspell/restserver/routes/EquipmentRoutes.scala b/modules/restserver/src/main/scala/docspell/restserver/routes/EquipmentRoutes.scala
index a8db67ba..bd0e56d4 100644
--- a/modules/restserver/src/main/scala/docspell/restserver/routes/EquipmentRoutes.scala
+++ b/modules/restserver/src/main/scala/docspell/restserver/routes/EquipmentRoutes.scala
@@ -18,7 +18,7 @@ import org.http4s.dsl.Http4sDsl
 
 object EquipmentRoutes {
 
-  def apply[F[_]: Effect](backend: BackendApp[F], user: AuthToken): HttpRoutes[F] = {
+  def apply[F[_]: Async](backend: BackendApp[F], user: AuthToken): HttpRoutes[F] = {
     val dsl = new Http4sDsl[F] {}
     import dsl._
 
diff --git a/modules/restserver/src/main/scala/docspell/restserver/routes/FolderRoutes.scala b/modules/restserver/src/main/scala/docspell/restserver/routes/FolderRoutes.scala
index 0a9305bc..81a9ccb3 100644
--- a/modules/restserver/src/main/scala/docspell/restserver/routes/FolderRoutes.scala
+++ b/modules/restserver/src/main/scala/docspell/restserver/routes/FolderRoutes.scala
@@ -20,7 +20,7 @@ import org.http4s.dsl.Http4sDsl
 
 object FolderRoutes {
 
-  def apply[F[_]: Effect](backend: BackendApp[F], user: AuthToken): HttpRoutes[F] = {
+  def apply[F[_]: Async](backend: BackendApp[F], user: AuthToken): HttpRoutes[F] = {
     val dsl = new Http4sDsl[F] with ResponseGenerator[F] {}
     import dsl._
 
diff --git a/modules/restserver/src/main/scala/docspell/restserver/routes/FullTextIndexRoutes.scala b/modules/restserver/src/main/scala/docspell/restserver/routes/FullTextIndexRoutes.scala
index d6d927c4..1cd41dc0 100644
--- a/modules/restserver/src/main/scala/docspell/restserver/routes/FullTextIndexRoutes.scala
+++ b/modules/restserver/src/main/scala/docspell/restserver/routes/FullTextIndexRoutes.scala
@@ -15,7 +15,7 @@ import org.http4s.dsl.Http4sDsl
 
 object FullTextIndexRoutes {
 
-  def secured[F[_]: Effect](
+  def secured[F[_]: Async](
       cfg: Config,
       backend: BackendApp[F],
       user: AuthToken
@@ -33,7 +33,7 @@ object FullTextIndexRoutes {
       }
     }
 
-  def admin[F[_]: Effect](cfg: Config, backend: BackendApp[F]): HttpRoutes[F] =
+  def admin[F[_]: Async](cfg: Config, backend: BackendApp[F]): HttpRoutes[F] =
     if (!cfg.fullTextSearch.enabled) notFound[F]
     else {
       val dsl = Http4sDsl[F]
@@ -47,6 +47,6 @@ object FullTextIndexRoutes {
       }
     }
 
-  private def notFound[F[_]: Effect]: HttpRoutes[F] =
+  private def notFound[F[_]: Async]: HttpRoutes[F] =
     Responses.notFoundRoute[F]
 }
diff --git a/modules/restserver/src/main/scala/docspell/restserver/routes/IntegrationEndpointRoutes.scala b/modules/restserver/src/main/scala/docspell/restserver/routes/IntegrationEndpointRoutes.scala
index 86b6342d..d2b9dd91 100644
--- a/modules/restserver/src/main/scala/docspell/restserver/routes/IntegrationEndpointRoutes.scala
+++ b/modules/restserver/src/main/scala/docspell/restserver/routes/IntegrationEndpointRoutes.scala
@@ -16,13 +16,13 @@ import org.http4s.circe.CirceEntityEncoder._
 import org.http4s.dsl.Http4sDsl
 import org.http4s.headers.{Authorization, `WWW-Authenticate`}
 import org.http4s.multipart.Multipart
-import org.http4s.util.CaseInsensitiveString
 import org.log4s.getLogger
+import org.typelevel.ci.CIString
 
 object IntegrationEndpointRoutes {
   private[this] val logger = getLogger
 
-  def open[F[_]: Effect](backend: BackendApp[F], cfg: Config): HttpRoutes[F] = {
+  def open[F[_]: Async](backend: BackendApp[F], cfg: Config): HttpRoutes[F] = {
     val dsl = new Http4sDsl[F] {}
     import dsl._
 
@@ -61,12 +61,12 @@ object IntegrationEndpointRoutes {
     }
   }
 
-  def checkEnabled[F[_]: Effect](
+  def checkEnabled[F[_]: Async](
       cfg: Config.IntegrationEndpoint
   ): EitherT[F, Response[F], Unit] =
     EitherT.cond[F](cfg.enabled, (), Response.notFound[F])
 
-  def authRequest[F[_]: Effect](
+  def authRequest[F[_]: Async](
       req: Request[F],
       cfg: Config.IntegrationEndpoint
   ): EitherT[F, Response[F], Unit] = {
@@ -77,7 +77,7 @@ object IntegrationEndpointRoutes {
     service.run(req).toLeft(())
   }
 
-  def lookupCollective[F[_]: Effect](
+  def lookupCollective[F[_]: Async](
       coll: Ident,
       backend: BackendApp[F]
   ): EitherT[F, Response[F], Unit] =
@@ -86,7 +86,7 @@ object IntegrationEndpointRoutes {
       res <- EitherT.cond[F](opt.exists(_.integrationEnabled), (), Response.notFound[F])
     } yield res
 
-  def uploadFile[F[_]: Effect](
+  def uploadFile[F[_]: Async](
       coll: Ident,
       backend: BackendApp[F],
       cfg: Config,
@@ -111,48 +111,48 @@ object IntegrationEndpointRoutes {
   }
 
   object HeaderAuth {
-    def apply[F[_]: Effect](cfg: Config.IntegrationEndpoint.HttpHeader): HttpRoutes[F] =
+    def apply[F[_]: Async](cfg: Config.IntegrationEndpoint.HttpHeader): HttpRoutes[F] =
       if (cfg.enabled) checkHeader(cfg)
       else HttpRoutes.empty[F]
 
-    def checkHeader[F[_]: Effect](
+    def checkHeader[F[_]: Async](
         cfg: Config.IntegrationEndpoint.HttpHeader
     ): HttpRoutes[F] =
       HttpRoutes { req =>
-        val h = req.headers.find(_.name == CaseInsensitiveString(cfg.headerName))
-        if (h.exists(_.value == cfg.headerValue)) OptionT.none[F, Response[F]]
+        val h = req.headers.get(CIString(cfg.headerName))
+        if (h.exists(_.head.value == cfg.headerValue)) OptionT.none[F, Response[F]]
         else OptionT.pure(Responses.forbidden[F])
       }
   }
 
   object SourceIpAuth {
-    def apply[F[_]: Effect](cfg: Config.IntegrationEndpoint.AllowedIps): HttpRoutes[F] =
+    def apply[F[_]: Async](cfg: Config.IntegrationEndpoint.AllowedIps): HttpRoutes[F] =
       if (cfg.enabled) checkIps(cfg)
       else HttpRoutes.empty[F]
 
-    def checkIps[F[_]: Effect](
+    def checkIps[F[_]: Async](
         cfg: Config.IntegrationEndpoint.AllowedIps
     ): HttpRoutes[F] =
       HttpRoutes { req =>
         //The `req.from' take the X-Forwarded-For header into account,
         //which is not desirable here. The `http-header' auth config
         //can be used to authenticate based on headers.
-        val from = req.remote.flatMap(remote => Option(remote.getAddress))
+        val from = req.remote.map(_.host)
         if (from.exists(cfg.containsAddress)) OptionT.none[F, Response[F]]
         else OptionT.pure(Responses.forbidden[F])
       }
   }
 
   object HttpBasicAuth {
-    def apply[F[_]: Effect](cfg: Config.IntegrationEndpoint.HttpBasic): HttpRoutes[F] =
+    def apply[F[_]: Async](cfg: Config.IntegrationEndpoint.HttpBasic): HttpRoutes[F] =
       if (cfg.enabled) checkHttpBasic(cfg)
       else HttpRoutes.empty[F]
 
-    def checkHttpBasic[F[_]: Effect](
+    def checkHttpBasic[F[_]: Async](
         cfg: Config.IntegrationEndpoint.HttpBasic
     ): HttpRoutes[F] =
       HttpRoutes { req =>
-        req.headers.get(Authorization) match {
+        req.headers.get[Authorization] match {
           case Some(auth) =>
             auth.credentials match {
               case BasicCredentials(user, pass)
diff --git a/modules/restserver/src/main/scala/docspell/restserver/routes/ItemMultiRoutes.scala b/modules/restserver/src/main/scala/docspell/restserver/routes/ItemMultiRoutes.scala
index 42a80d9d..55fe5f49 100644
--- a/modules/restserver/src/main/scala/docspell/restserver/routes/ItemMultiRoutes.scala
+++ b/modules/restserver/src/main/scala/docspell/restserver/routes/ItemMultiRoutes.scala
@@ -17,7 +17,7 @@ import org.http4s.dsl.Http4sDsl
 
 object ItemMultiRoutes extends MultiIdSupport {
 
-  def apply[F[_]: Effect](
+  def apply[F[_]: Async](
       backend: BackendApp[F],
       user: AuthToken
   ): HttpRoutes[F] = {
diff --git a/modules/restserver/src/main/scala/docspell/restserver/routes/ItemRoutes.scala b/modules/restserver/src/main/scala/docspell/restserver/routes/ItemRoutes.scala
index 35fe9320..7b2e5cc2 100644
--- a/modules/restserver/src/main/scala/docspell/restserver/routes/ItemRoutes.scala
+++ b/modules/restserver/src/main/scala/docspell/restserver/routes/ItemRoutes.scala
@@ -32,9 +32,8 @@ import org.log4s._
 object ItemRoutes {
   private[this] val logger = getLogger
 
-  def apply[F[_]: Effect: ContextShift](
+  def apply[F[_]: Async](
       cfg: Config,
-      blocker: Blocker,
       backend: BackendApp[F],
       user: AuthToken
   ): HttpRoutes[F] = {
@@ -331,7 +330,7 @@ object ItemRoutes {
           NotFound(BasicResult(false, "Not found"))
         for {
           preview <- backend.itemSearch.findItemPreview(id, user.account.collective)
-          inm      = req.headers.get(`If-None-Match`).flatMap(_.tags)
+          inm      = req.headers.get[`If-None-Match`].flatMap(_.tags)
           matches  = BinaryUtil.matchETag(preview.map(_.meta), inm)
           fallback = flag.getOrElse(false)
           resp <-
@@ -341,7 +340,7 @@ object ItemRoutes {
                 else BinaryUtil.makeByteResp(dsl)(data).map(Responses.noCache)
               }
               .getOrElse(
-                if (fallback) BinaryUtil.noPreview(blocker, req.some).getOrElseF(notFound)
+                if (fallback) BinaryUtil.noPreview(req.some).getOrElseF(notFound)
                 else notFound
               )
         } yield resp
diff --git a/modules/restserver/src/main/scala/docspell/restserver/routes/JobQueueRoutes.scala b/modules/restserver/src/main/scala/docspell/restserver/routes/JobQueueRoutes.scala
index 751da6b6..4dcae88b 100644
--- a/modules/restserver/src/main/scala/docspell/restserver/routes/JobQueueRoutes.scala
+++ b/modules/restserver/src/main/scala/docspell/restserver/routes/JobQueueRoutes.scala
@@ -16,7 +16,7 @@ import org.http4s.dsl.Http4sDsl
 
 object JobQueueRoutes {
 
-  def apply[F[_]: Effect](backend: BackendApp[F], user: AuthToken): HttpRoutes[F] = {
+  def apply[F[_]: Async](backend: BackendApp[F], user: AuthToken): HttpRoutes[F] = {
     val dsl = new Http4sDsl[F] {}
     import dsl._
 
diff --git a/modules/restserver/src/main/scala/docspell/restserver/routes/LoginRoutes.scala b/modules/restserver/src/main/scala/docspell/restserver/routes/LoginRoutes.scala
index 9beaf4ce..6d16ac13 100644
--- a/modules/restserver/src/main/scala/docspell/restserver/routes/LoginRoutes.scala
+++ b/modules/restserver/src/main/scala/docspell/restserver/routes/LoginRoutes.scala
@@ -17,7 +17,7 @@ import org.http4s.dsl.Http4sDsl
 
 object LoginRoutes {
 
-  def login[F[_]: Effect](S: Login[F], cfg: Config): HttpRoutes[F] = {
+  def login[F[_]: Async](S: Login[F], cfg: Config): HttpRoutes[F] = {
     val dsl: Http4sDsl[F] = new Http4sDsl[F] {}
     import dsl._
 
@@ -32,7 +32,7 @@ object LoginRoutes {
     }
   }
 
-  def session[F[_]: Effect](S: Login[F], cfg: Config, token: AuthToken): HttpRoutes[F] = {
+  def session[F[_]: Async](S: Login[F], cfg: Config, token: AuthToken): HttpRoutes[F] = {
     val dsl: Http4sDsl[F] = new Http4sDsl[F] {}
     import dsl._
 
@@ -56,7 +56,7 @@ object LoginRoutes {
   private def getBaseUrl[F[_]](cfg: Config, req: Request[F]): LenientUri =
     ClientRequestInfo.getBaseUrl(cfg, req)
 
-  private def makeResponse[F[_]: Effect](
+  private def makeResponse[F[_]: Async](
       dsl: Http4sDsl[F],
       cfg: Config,
       req: Request[F],
diff --git a/modules/restserver/src/main/scala/docspell/restserver/routes/MailSendRoutes.scala b/modules/restserver/src/main/scala/docspell/restserver/routes/MailSendRoutes.scala
index 1c6a40c7..15a90ea5 100644
--- a/modules/restserver/src/main/scala/docspell/restserver/routes/MailSendRoutes.scala
+++ b/modules/restserver/src/main/scala/docspell/restserver/routes/MailSendRoutes.scala
@@ -19,7 +19,7 @@ import org.http4s.dsl.Http4sDsl
 
 object MailSendRoutes {
 
-  def apply[F[_]: Effect](backend: BackendApp[F], user: AuthToken): HttpRoutes[F] = {
+  def apply[F[_]: Async](backend: BackendApp[F], user: AuthToken): HttpRoutes[F] = {
     val dsl = new Http4sDsl[F] {}
     import dsl._
 
diff --git a/modules/restserver/src/main/scala/docspell/restserver/routes/MailSettingsRoutes.scala b/modules/restserver/src/main/scala/docspell/restserver/routes/MailSettingsRoutes.scala
index f71dea29..112dc186 100644
--- a/modules/restserver/src/main/scala/docspell/restserver/routes/MailSettingsRoutes.scala
+++ b/modules/restserver/src/main/scala/docspell/restserver/routes/MailSettingsRoutes.scala
@@ -22,7 +22,7 @@ import org.http4s.dsl.Http4sDsl
 
 object MailSettingsRoutes {
 
-  def apply[F[_]: Effect](backend: BackendApp[F], user: AuthToken): HttpRoutes[F] = {
+  def apply[F[_]: Async](backend: BackendApp[F], user: AuthToken): HttpRoutes[F] = {
     val dsl = new Http4sDsl[F] {}
     import dsl._
 
diff --git a/modules/restserver/src/main/scala/docspell/restserver/routes/NotifyDueItemsRoutes.scala b/modules/restserver/src/main/scala/docspell/restserver/routes/NotifyDueItemsRoutes.scala
index 720634f1..e68fa868 100644
--- a/modules/restserver/src/main/scala/docspell/restserver/routes/NotifyDueItemsRoutes.scala
+++ b/modules/restserver/src/main/scala/docspell/restserver/routes/NotifyDueItemsRoutes.scala
@@ -20,7 +20,7 @@ import org.http4s.dsl.Http4sDsl
 
 object NotifyDueItemsRoutes {
 
-  def apply[F[_]: Effect](
+  def apply[F[_]: Async](
       cfg: Config,
       backend: BackendApp[F],
       user: AuthToken
diff --git a/modules/restserver/src/main/scala/docspell/restserver/routes/OrganizationRoutes.scala b/modules/restserver/src/main/scala/docspell/restserver/routes/OrganizationRoutes.scala
index 4bed90e4..f4ae63c6 100644
--- a/modules/restserver/src/main/scala/docspell/restserver/routes/OrganizationRoutes.scala
+++ b/modules/restserver/src/main/scala/docspell/restserver/routes/OrganizationRoutes.scala
@@ -18,7 +18,7 @@ import org.http4s.dsl.Http4sDsl
 
 object OrganizationRoutes {
 
-  def apply[F[_]: Effect](backend: BackendApp[F], user: AuthToken): HttpRoutes[F] = {
+  def apply[F[_]: Async](backend: BackendApp[F], user: AuthToken): HttpRoutes[F] = {
     val dsl = new Http4sDsl[F] {}
     import dsl._
 
diff --git a/modules/restserver/src/main/scala/docspell/restserver/routes/PersonRoutes.scala b/modules/restserver/src/main/scala/docspell/restserver/routes/PersonRoutes.scala
index a37ac536..55470780 100644
--- a/modules/restserver/src/main/scala/docspell/restserver/routes/PersonRoutes.scala
+++ b/modules/restserver/src/main/scala/docspell/restserver/routes/PersonRoutes.scala
@@ -21,7 +21,7 @@ import org.log4s._
 object PersonRoutes {
   private[this] val logger = getLogger
 
-  def apply[F[_]: Effect](backend: BackendApp[F], user: AuthToken): HttpRoutes[F] = {
+  def apply[F[_]: Async](backend: BackendApp[F], user: AuthToken): HttpRoutes[F] = {
     val dsl = new Http4sDsl[F] {}
     import dsl._
 
diff --git a/modules/restserver/src/main/scala/docspell/restserver/routes/RegisterRoutes.scala b/modules/restserver/src/main/scala/docspell/restserver/routes/RegisterRoutes.scala
index 1a548cd3..9fab734f 100644
--- a/modules/restserver/src/main/scala/docspell/restserver/routes/RegisterRoutes.scala
+++ b/modules/restserver/src/main/scala/docspell/restserver/routes/RegisterRoutes.scala
@@ -19,7 +19,7 @@ import org.log4s._
 object RegisterRoutes {
   private[this] val logger = getLogger
 
-  def apply[F[_]: Effect](backend: BackendApp[F], cfg: Config): HttpRoutes[F] = {
+  def apply[F[_]: Async](backend: BackendApp[F], cfg: Config): HttpRoutes[F] = {
     val dsl = new Http4sDsl[F] with ResponseGenerator[F] {}
     import dsl._
 
diff --git a/modules/restserver/src/main/scala/docspell/restserver/routes/ScanMailboxRoutes.scala b/modules/restserver/src/main/scala/docspell/restserver/routes/ScanMailboxRoutes.scala
index 3ac87316..a1b83c84 100644
--- a/modules/restserver/src/main/scala/docspell/restserver/routes/ScanMailboxRoutes.scala
+++ b/modules/restserver/src/main/scala/docspell/restserver/routes/ScanMailboxRoutes.scala
@@ -18,7 +18,7 @@ import org.http4s.dsl.Http4sDsl
 
 object ScanMailboxRoutes {
 
-  def apply[F[_]: Effect](
+  def apply[F[_]: Async](
       backend: BackendApp[F],
       user: AuthToken
   ): HttpRoutes[F] = {
diff --git a/modules/restserver/src/main/scala/docspell/restserver/routes/SentMailRoutes.scala b/modules/restserver/src/main/scala/docspell/restserver/routes/SentMailRoutes.scala
index 49884d12..7de6ec9f 100644
--- a/modules/restserver/src/main/scala/docspell/restserver/routes/SentMailRoutes.scala
+++ b/modules/restserver/src/main/scala/docspell/restserver/routes/SentMailRoutes.scala
@@ -17,7 +17,7 @@ import org.http4s.dsl.Http4sDsl
 
 object SentMailRoutes {
 
-  def apply[F[_]: Effect](backend: BackendApp[F], user: AuthToken): HttpRoutes[F] = {
+  def apply[F[_]: Async](backend: BackendApp[F], user: AuthToken): HttpRoutes[F] = {
     val dsl = new Http4sDsl[F] {}
     import dsl._
 
diff --git a/modules/restserver/src/main/scala/docspell/restserver/routes/SourceRoutes.scala b/modules/restserver/src/main/scala/docspell/restserver/routes/SourceRoutes.scala
index fdda7e76..f8e0bd26 100644
--- a/modules/restserver/src/main/scala/docspell/restserver/routes/SourceRoutes.scala
+++ b/modules/restserver/src/main/scala/docspell/restserver/routes/SourceRoutes.scala
@@ -17,7 +17,7 @@ import org.http4s.dsl.Http4sDsl
 
 object SourceRoutes {
 
-  def apply[F[_]: Effect](backend: BackendApp[F], user: AuthToken): HttpRoutes[F] = {
+  def apply[F[_]: Async](backend: BackendApp[F], user: AuthToken): HttpRoutes[F] = {
     val dsl = new Http4sDsl[F] with ResponseGenerator[F] {}
     import dsl._
 
diff --git a/modules/restserver/src/main/scala/docspell/restserver/routes/TagRoutes.scala b/modules/restserver/src/main/scala/docspell/restserver/routes/TagRoutes.scala
index 461389d6..8e92abff 100644
--- a/modules/restserver/src/main/scala/docspell/restserver/routes/TagRoutes.scala
+++ b/modules/restserver/src/main/scala/docspell/restserver/routes/TagRoutes.scala
@@ -17,7 +17,7 @@ import org.http4s.dsl.Http4sDsl
 
 object TagRoutes {
 
-  def apply[F[_]: Effect](backend: BackendApp[F], user: AuthToken): HttpRoutes[F] = {
+  def apply[F[_]: Async](backend: BackendApp[F], user: AuthToken): HttpRoutes[F] = {
     val dsl = new Http4sDsl[F] with ResponseGenerator[F] {}
     import dsl._
 
diff --git a/modules/restserver/src/main/scala/docspell/restserver/routes/UploadRoutes.scala b/modules/restserver/src/main/scala/docspell/restserver/routes/UploadRoutes.scala
index f50c5da9..34194bda 100644
--- a/modules/restserver/src/main/scala/docspell/restserver/routes/UploadRoutes.scala
+++ b/modules/restserver/src/main/scala/docspell/restserver/routes/UploadRoutes.scala
@@ -20,7 +20,7 @@ import org.log4s._
 object UploadRoutes {
   private[this] val logger = getLogger
 
-  def secured[F[_]: Effect](
+  def secured[F[_]: Async](
       backend: BackendApp[F],
       cfg: Config,
       user: AuthToken
@@ -39,7 +39,7 @@ object UploadRoutes {
     }
   }
 
-  def open[F[_]: Effect](backend: BackendApp[F], cfg: Config): HttpRoutes[F] = {
+  def open[F[_]: Async](backend: BackendApp[F], cfg: Config): HttpRoutes[F] = {
     val dsl = new Http4sDsl[F] with ResponseGenerator[F] {}
     import dsl._
 
@@ -62,7 +62,7 @@ object UploadRoutes {
     }
   }
 
-  private def submitFiles[F[_]: Effect](
+  private def submitFiles[F[_]: Async](
       backend: BackendApp[F],
       cfg: Config,
       accOrSrc: Either[Ident, AccountId]
diff --git a/modules/restserver/src/main/scala/docspell/restserver/routes/UserRoutes.scala b/modules/restserver/src/main/scala/docspell/restserver/routes/UserRoutes.scala
index bdf0ac57..5785aa4b 100644
--- a/modules/restserver/src/main/scala/docspell/restserver/routes/UserRoutes.scala
+++ b/modules/restserver/src/main/scala/docspell/restserver/routes/UserRoutes.scala
@@ -17,7 +17,7 @@ import org.http4s.dsl.Http4sDsl
 
 object UserRoutes {
 
-  def apply[F[_]: Effect](backend: BackendApp[F], user: AuthToken): HttpRoutes[F] = {
+  def apply[F[_]: Async](backend: BackendApp[F], user: AuthToken): HttpRoutes[F] = {
     val dsl = new Http4sDsl[F] {}
     import dsl._
 
@@ -63,7 +63,7 @@ object UserRoutes {
     }
   }
 
-  def admin[F[_]: Effect](backend: BackendApp[F]): HttpRoutes[F] = {
+  def admin[F[_]: Async](backend: BackendApp[F]): HttpRoutes[F] = {
     val dsl = new Http4sDsl[F] {}
     import dsl._
 
diff --git a/modules/restserver/src/main/scala/docspell/restserver/webapp/TemplateRoutes.scala b/modules/restserver/src/main/scala/docspell/restserver/webapp/TemplateRoutes.scala
index 3a7aba3e..d20afa4d 100644
--- a/modules/restserver/src/main/scala/docspell/restserver/webapp/TemplateRoutes.scala
+++ b/modules/restserver/src/main/scala/docspell/restserver/webapp/TemplateRoutes.scala
@@ -30,14 +30,12 @@ object TemplateRoutes {
     def serviceWorker: HttpRoutes[F]
   }
 
-  def apply[F[_]: Effect](blocker: Blocker, cfg: Config)(implicit
-      C: ContextShift[F]
-  ): InnerRoutes[F] = {
+  def apply[F[_]: Async](cfg: Config): InnerRoutes[F] = {
     val indexTemplate = memo(
-      loadResource("/index.html").flatMap(loadTemplate(_, blocker))
+      loadResource("/index.html").flatMap(loadTemplate(_))
     )
-    val docTemplate = memo(loadResource("/doc.html").flatMap(loadTemplate(_, blocker)))
-    val swTemplate  = memo(loadResource("/sw.js").flatMap(loadTemplate(_, blocker)))
+    val docTemplate = memo(loadResource("/doc.html").flatMap(loadTemplate(_)))
+    val swTemplate  = memo(loadResource("/sw.js").flatMap(loadTemplate(_)))
 
     val dsl = new Http4sDsl[F] {}
     import dsl._
@@ -84,12 +82,10 @@ object TemplateRoutes {
         r.pure[F]
     }
 
-  def loadUrl[F[_]: Sync](url: URL, blocker: Blocker)(implicit
-      C: ContextShift[F]
-  ): F[String] =
+  def loadUrl[F[_]: Sync](url: URL): F[String] =
     Stream
       .bracket(Sync[F].delay(url.openStream))(in => Sync[F].delay(in.close()))
-      .flatMap(in => fs2.io.readInputStream(in.pure[F], 64 * 1024, blocker, false))
+      .flatMap(in => fs2.io.readInputStream(in.pure[F], 64 * 1024, false))
       .through(text.utf8Decode)
       .compile
       .fold("")(_ + _)
@@ -102,10 +98,8 @@ object TemplateRoutes {
       }
     }
 
-  def loadTemplate[F[_]: Sync](url: URL, blocker: Blocker)(implicit
-      C: ContextShift[F]
-  ): F[Template] =
-    loadUrl[F](url, blocker).flatMap(s => parseTemplate(s)).map { t =>
+  def loadTemplate[F[_]: Sync](url: URL): F[Template] =
+    loadUrl[F](url).flatMap(s => parseTemplate(s)).map { t =>
       logger.info(s"Compiled template $url")
       t
     }
diff --git a/modules/restserver/src/main/scala/docspell/restserver/webapp/WebjarRoutes.scala b/modules/restserver/src/main/scala/docspell/restserver/webapp/WebjarRoutes.scala
index e1c02ea3..27ba87d8 100644
--- a/modules/restserver/src/main/scala/docspell/restserver/webapp/WebjarRoutes.scala
+++ b/modules/restserver/src/main/scala/docspell/restserver/webapp/WebjarRoutes.scala
@@ -27,19 +27,18 @@ object WebjarRoutes {
     ".xml"
   )
 
-  def appRoutes[F[_]: Effect](
-      blocker: Blocker
-  )(implicit CS: ContextShift[F]): HttpRoutes[F] =
+  def appRoutes[F[_]: Async]: HttpRoutes[F] =
     Kleisli {
       case req if req.method == Method.GET =>
-        val p = req.pathInfo
-        if (p.contains("..") || !suffixes.exists(p.endsWith(_)))
+        val p             = req.pathInfo.renderString
+        val last          = req.pathInfo.segments.lastOption.map(_.encoded).getOrElse("")
+        val containsColon = req.pathInfo.segments.exists(_.encoded.contains(".."))
+        if (containsColon || !suffixes.exists(last.endsWith(_)))
           OptionT.pure(Response.notFound[F])
         else
           StaticFile
             .fromResource(
               s"/META-INF/resources/webjars$p",
-              blocker,
               Some(req),
               EnvMode.current.isProd
             )
diff --git a/modules/store/src/main/scala/docspell/store/Store.scala b/modules/store/src/main/scala/docspell/store/Store.scala
index d20c78ef..8988a96f 100644
--- a/modules/store/src/main/scala/docspell/store/Store.scala
+++ b/modules/store/src/main/scala/docspell/store/Store.scala
@@ -24,10 +24,9 @@ trait Store[F[_]] {
 
 object Store {
 
-  def create[F[_]: Effect: ContextShift](
+  def create[F[_]: Async](
       jdbc: JdbcConfig,
-      connectEC: ExecutionContext,
-      blocker: Blocker
+      connectEC: ExecutionContext
   ): Resource[F, Store[F]] = {
 
     val hxa = HikariTransactor.newHikariTransactor[F](
@@ -35,8 +34,7 @@ object Store {
       jdbc.url.asString,
       jdbc.user,
       jdbc.password,
-      connectEC,
-      blocker
+      connectEC
     )
 
     for {
diff --git a/modules/store/src/main/scala/docspell/store/impl/StoreImpl.scala b/modules/store/src/main/scala/docspell/store/impl/StoreImpl.scala
index a0707844..580113a7 100644
--- a/modules/store/src/main/scala/docspell/store/impl/StoreImpl.scala
+++ b/modules/store/src/main/scala/docspell/store/impl/StoreImpl.scala
@@ -1,6 +1,6 @@
 package docspell.store.impl
 
-import cats.effect.Effect
+import cats.effect.Async
 import cats.implicits._
 
 import docspell.common.Ident
@@ -11,8 +11,7 @@ import bitpeace.{Bitpeace, BitpeaceConfig, TikaMimetypeDetect}
 import doobie._
 import doobie.implicits._
 
-final class StoreImpl[F[_]: Effect](jdbc: JdbcConfig, xa: Transactor[F])
-    extends Store[F] {
+final class StoreImpl[F[_]: Async](jdbc: JdbcConfig, xa: Transactor[F]) extends Store[F] {
   val bitpeaceCfg =
     BitpeaceConfig(
       "filemeta",
diff --git a/modules/store/src/main/scala/docspell/store/queries/QItem.scala b/modules/store/src/main/scala/docspell/store/queries/QItem.scala
index f9c0af38..8b5c6e86 100644
--- a/modules/store/src/main/scala/docspell/store/queries/QItem.scala
+++ b/modules/store/src/main/scala/docspell/store/queries/QItem.scala
@@ -3,8 +3,8 @@ package docspell.store.queries
 import java.time.LocalDate
 
 import cats.data.{NonEmptyList => Nel}
+import cats.effect.Ref
 import cats.effect.Sync
-import cats.effect.concurrent.Ref
 import cats.implicits._
 import fs2.Stream
 
diff --git a/modules/store/src/main/scala/docspell/store/queries/QJob.scala b/modules/store/src/main/scala/docspell/store/queries/QJob.scala
index 41132ee6..7e95d4f1 100644
--- a/modules/store/src/main/scala/docspell/store/queries/QJob.scala
+++ b/modules/store/src/main/scala/docspell/store/queries/QJob.scala
@@ -19,7 +19,7 @@ import org.log4s._
 object QJob {
   private[this] val logger = getLogger
 
-  def takeNextJob[F[_]: Effect](
+  def takeNextJob[F[_]: Async](
       store: Store[F]
   )(
       priority: Ident => F[Priority],
@@ -49,7 +49,7 @@ object QJob {
       .last
       .map(_.flatten)
 
-  private def takeNextJob1[F[_]: Effect](store: Store[F])(
+  private def takeNextJob1[F[_]: Async](store: Store[F])(
       priority: Ident => F[Priority],
       worker: Ident,
       retryPause: Duration,
@@ -147,37 +147,37 @@ object QJob {
     sql.build.query[RJob].option
   }
 
-  def setCancelled[F[_]: Effect](id: Ident, store: Store[F]): F[Unit] =
+  def setCancelled[F[_]: Async](id: Ident, store: Store[F]): F[Unit] =
     for {
       now <- Timestamp.current[F]
       _   <- store.transact(RJob.setCancelled(id, now))
     } yield ()
 
-  def setFailed[F[_]: Effect](id: Ident, store: Store[F]): F[Unit] =
+  def setFailed[F[_]: Async](id: Ident, store: Store[F]): F[Unit] =
     for {
       now <- Timestamp.current[F]
       _   <- store.transact(RJob.setFailed(id, now))
     } yield ()
 
-  def setSuccess[F[_]: Effect](id: Ident, store: Store[F]): F[Unit] =
+  def setSuccess[F[_]: Async](id: Ident, store: Store[F]): F[Unit] =
     for {
       now <- Timestamp.current[F]
       _   <- store.transact(RJob.setSuccess(id, now))
     } yield ()
 
-  def setStuck[F[_]: Effect](id: Ident, store: Store[F]): F[Unit] =
+  def setStuck[F[_]: Async](id: Ident, store: Store[F]): F[Unit] =
     for {
       now <- Timestamp.current[F]
       _   <- store.transact(RJob.setStuck(id, now))
     } yield ()
 
-  def setRunning[F[_]: Effect](id: Ident, workerId: Ident, store: Store[F]): F[Unit] =
+  def setRunning[F[_]: Async](id: Ident, workerId: Ident, store: Store[F]): F[Unit] =
     for {
       now <- Timestamp.current[F]
       _   <- store.transact(RJob.setRunning(id, workerId, now))
     } yield ()
 
-  def setFinalState[F[_]: Effect](id: Ident, state: JobState, store: Store[F]): F[Unit] =
+  def setFinalState[F[_]: Async](id: Ident, state: JobState, store: Store[F]): F[Unit] =
     state match {
       case JobState.Success =>
         setSuccess(id, store)
@@ -191,10 +191,10 @@ object QJob {
         logger.ferror[F](s"Invalid final state: $state.")
     }
 
-  def exceedsRetries[F[_]: Effect](id: Ident, max: Int, store: Store[F]): F[Boolean] =
+  def exceedsRetries[F[_]: Async](id: Ident, max: Int, store: Store[F]): F[Boolean] =
     store.transact(RJob.getRetries(id)).map(n => n.forall(_ >= max))
 
-  def runningToWaiting[F[_]: Effect](workerId: Ident, store: Store[F]): F[Unit] =
+  def runningToWaiting[F[_]: Async](workerId: Ident, store: Store[F]): F[Unit] =
     store.transact(RJob.setRunningToWaiting(workerId)).map(_ => ())
 
   def findAll[F[_]](ids: Seq[Ident], store: Store[F]): F[Vector[RJob]] =
diff --git a/modules/store/src/main/scala/docspell/store/queue/JobQueue.scala b/modules/store/src/main/scala/docspell/store/queue/JobQueue.scala
index 127a45e1..40147571 100644
--- a/modules/store/src/main/scala/docspell/store/queue/JobQueue.scala
+++ b/modules/store/src/main/scala/docspell/store/queue/JobQueue.scala
@@ -1,6 +1,6 @@
 package docspell.store.queue
 
-import cats.effect.{Effect, Resource}
+import cats.effect._
 import cats.implicits._
 
 import docspell.common._
@@ -40,7 +40,7 @@ trait JobQueue[F[_]] {
 object JobQueue {
   private[this] val logger = getLogger
 
-  def apply[F[_]: Effect](store: Store[F]): Resource[F, JobQueue[F]] =
+  def apply[F[_]: Async](store: Store[F]): Resource[F, JobQueue[F]] =
     Resource.pure[F, JobQueue[F]](new JobQueue[F] {
 
       def nextJob(
@@ -56,7 +56,7 @@ object JobQueue {
           .transact(RJob.insert(job))
           .flatMap { n =>
             if (n != 1)
-              Effect[F]
+              Async[F]
                 .raiseError(new Exception(s"Inserting job failed. Update count: $n"))
             else ().pure[F]
           }
diff --git a/modules/store/src/main/scala/docspell/store/queue/PeriodicTaskStore.scala b/modules/store/src/main/scala/docspell/store/queue/PeriodicTaskStore.scala
index dfbdb2d5..297b3c15 100644
--- a/modules/store/src/main/scala/docspell/store/queue/PeriodicTaskStore.scala
+++ b/modules/store/src/main/scala/docspell/store/queue/PeriodicTaskStore.scala
@@ -9,7 +9,6 @@ import docspell.store.queries.QPeriodicTask
 import docspell.store.records._
 import docspell.store.{AddResult, Store}
 
-import com.github.eikek.fs2calev._
 import org.log4s.getLogger
 
 trait PeriodicTaskStore[F[_]] {
@@ -83,13 +82,7 @@ object PeriodicTaskStore {
       def unmark(job: RPeriodicTask): F[Unit] =
         for {
           now <- Timestamp.current[F]
-          nextRun <-
-            CalevFs2
-              .nextElapses[F](now.atUTC)(job.timer)
-              .take(1)
-              .compile
-              .last
-              .map(_.map(Timestamp.from))
+          nextRun = job.timer.nextElapse(now.atUTC).map(Timestamp.from)
           _ <- store.transact(QPeriodicTask.unsetWorker(job.id, nextRun))
         } yield ()
 
diff --git a/modules/store/src/main/scala/docspell/store/records/SourceData.scala b/modules/store/src/main/scala/docspell/store/records/SourceData.scala
index ba8da051..2cccd63d 100644
--- a/modules/store/src/main/scala/docspell/store/records/SourceData.scala
+++ b/modules/store/src/main/scala/docspell/store/records/SourceData.scala
@@ -1,6 +1,6 @@
 package docspell.store.records
 
-import cats.effect.concurrent.Ref
+import cats.effect.Ref
 import cats.implicits._
 import fs2.Stream
 
diff --git a/modules/store/src/main/scala/docspell/store/usertask/UserTaskStore.scala b/modules/store/src/main/scala/docspell/store/usertask/UserTaskStore.scala
index bdcd4ad7..0854cdaf 100644
--- a/modules/store/src/main/scala/docspell/store/usertask/UserTaskStore.scala
+++ b/modules/store/src/main/scala/docspell/store/usertask/UserTaskStore.scala
@@ -95,7 +95,7 @@ trait UserTaskStore[F[_]] {
 
 object UserTaskStore {
 
-  def apply[F[_]: Effect](store: Store[F]): Resource[F, UserTaskStore[F]] =
+  def apply[F[_]: Async](store: Store[F]): Resource[F, UserTaskStore[F]] =
     Resource.pure[F, UserTaskStore[F]](new UserTaskStore[F] {
 
       def getAll(account: AccountId): Stream[F, UserTask[String]] =
@@ -126,7 +126,7 @@ object UserTaskStore {
           case AddResult.EntityExists(_) =>
             store.transact(QUserTask.update(account, ut.encode))
           case AddResult.Failure(ex) =>
-            Effect[F].raiseError(ex)
+            Async[F].raiseError(ex)
         }
       }
 
@@ -145,7 +145,7 @@ object UserTaskStore {
             .flatMap {
               case Nil       => (None: Option[UserTask[String]]).pure[F]
               case ut :: Nil => ut.some.pure[F]
-              case _         => Effect[F].raiseError(new Exception("More than one result found"))
+              case _         => Async[F].raiseError(new Exception("More than one result found"))
             }
         )
 
@@ -155,7 +155,7 @@ object UserTaskStore {
         getOneByNameRaw(account, name)
           .semiflatMap(_.decode match {
             case Right(ua) => ua.pure[F]
-            case Left(err) => Effect[F].raiseError(new Exception(err))
+            case Left(err) => Async[F].raiseError(new Exception(err))
           })
 
       def updateOneTask[A](account: AccountId, ut: UserTask[A])(implicit
diff --git a/modules/store/src/test/scala/docspell/store/StoreFixture.scala b/modules/store/src/test/scala/docspell/store/StoreFixture.scala
index acd59963..9460433a 100644
--- a/modules/store/src/test/scala/docspell/store/StoreFixture.scala
+++ b/modules/store/src/test/scala/docspell/store/StoreFixture.scala
@@ -1,8 +1,7 @@
 package docspell.store
 
-import scala.concurrent.ExecutionContext
-
 import cats.effect._
+import cats.effect.unsafe.implicits.global
 
 import docspell.common.LenientUri
 import docspell.store.impl.StoreImpl
@@ -26,8 +25,6 @@ trait StoreFixture {
 }
 
 object StoreFixture {
-  implicit def contextShift: ContextShift[IO] =
-    IO.contextShift(ExecutionContext.global)
 
   def memoryDB(dbname: String): JdbcConfig =
     JdbcConfig(
@@ -53,10 +50,9 @@ object StoreFixture {
     val makePool = Resource.make(IO(jdbcConnPool))(cp => IO(cp.dispose()))
 
     for {
-      ec      <- ExecutionContexts.cachedThreadPool[IO]
-      blocker <- Blocker[IO]
-      pool    <- makePool
-      xa = Transactor.fromDataSource[IO].apply(pool, ec, blocker)
+      ec   <- ExecutionContexts.cachedThreadPool[IO]
+      pool <- makePool
+      xa = Transactor.fromDataSource[IO].apply(pool, ec)
     } yield xa
   }
 
diff --git a/project/Dependencies.scala b/project/Dependencies.scala
index 4d2ef760..0e369f30 100644
--- a/project/Dependencies.scala
+++ b/project/Dependencies.scala
@@ -7,18 +7,19 @@ object Dependencies {
 
   val BcryptVersion           = "0.4"
   val BetterMonadicForVersion = "0.3.1"
-  val BitpeaceVersion         = "0.8.0"
+  val BitpeaceVersion         = "0.9.0-M1"
   val CalevVersion            = "0.5.3"
   val CatsParseVersion        = "0.3.4"
   val CirceVersion            = "0.14.1"
   val ClipboardJsVersion      = "2.0.6"
-  val DoobieVersion           = "0.13.4"
-  val EmilVersion             = "0.9.2"
+  val DoobieVersion           = "1.0.0-M5"
+  val EmilVersion             = "0.10.0-M1"
   val FlexmarkVersion         = "0.62.2"
   val FlywayVersion           = "7.10.0"
-  val Fs2Version              = "2.5.6"
+  val Fs2Version              = "3.0.4"
+  val Fs2CronVersion          = "0.7.1"
   val H2Version               = "1.4.200"
-  val Http4sVersion           = "0.21.24"
+  val Http4sVersion           = "0.23.0-RC1"
   val Icu4jVersion            = "69.1"
   val JsoupVersion            = "1.13.1"
   val KindProjectorVersion    = "0.10.3"
@@ -27,7 +28,6 @@ object Dependencies {
   val Log4sVersion            = "1.10.0"
   val LogbackVersion          = "1.2.3"
   val MariaDbVersion          = "2.7.3"
-  val MiniTestVersion         = "2.9.3"
   val MUnitVersion            = "0.7.26"
   val OrganizeImportsVersion  = "0.5.0"
   val PdfboxVersion           = "2.0.24"
@@ -66,7 +66,7 @@ object Dependencies {
     "com.github.eikek" %% "calev-core" % CalevVersion
   )
   val calevFs2 = Seq(
-    "com.github.eikek" %% "calev-fs2" % CalevVersion
+    "eu.timepit" %% "fs2-cron-calev" % Fs2CronVersion
   )
   val calevCirce = Seq(
     "com.github.eikek" %% "calev-circe" % CalevVersion
@@ -264,13 +264,6 @@ object Dependencies {
     "com.github.eikek" %% "yamusca-core" % YamuscaVersion
   )
 
-  val miniTest = Seq(
-    // https://github.com/monix/minitest
-    // Apache 2.0
-    "io.monix" %% "minitest"      % MiniTestVersion,
-    "io.monix" %% "minitest-laws" % MiniTestVersion
-  ).map(_ % Test)
-
   val munit = Seq(
     "org.scalameta" %% "munit"            % MUnitVersion,
     "org.scalameta" %% "munit-scalacheck" % MUnitVersion