diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index d4d5c8b..a32e476 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -94,11 +94,11 @@ jobs:
 
       - name: Make target directories
         if: github.event_name != 'pull_request' && (startsWith(github.ref, 'refs/tags/v') || github.ref == 'refs/heads/main')
-        run: mkdir -p circe/jvm/target testkit/native/target target testkit/js/target .js/target core/.native/target playJson/jvm/target benchmarks/.jvm/target sprayJson/target core/.js/target circe/js/target core/.jvm/target .jvm/target .native/target circe/native/target playJson/js/target testkit/jvm/target project/target
+        run: mkdir -p mongo4cats/.jvm/target circe/jvm/target testkit/native/target target testkit/js/target .js/target core/.native/target playJson/jvm/target benchmarks/.jvm/target sprayJson/target core/.js/target circe/js/target mongo/.jvm/target core/.jvm/target .jvm/target .native/target circe/native/target playJson/js/target testkit/jvm/target project/target
 
       - name: Compress target directories
         if: github.event_name != 'pull_request' && (startsWith(github.ref, 'refs/tags/v') || github.ref == 'refs/heads/main')
-        run: tar cf targets.tar circe/jvm/target testkit/native/target target testkit/js/target .js/target core/.native/target playJson/jvm/target benchmarks/.jvm/target sprayJson/target core/.js/target circe/js/target core/.jvm/target .jvm/target .native/target circe/native/target playJson/js/target testkit/jvm/target project/target
+        run: tar cf targets.tar mongo4cats/.jvm/target circe/jvm/target testkit/native/target target testkit/js/target .js/target core/.native/target playJson/jvm/target benchmarks/.jvm/target sprayJson/target core/.js/target circe/js/target mongo/.jvm/target core/.jvm/target .jvm/target .native/target circe/native/target playJson/js/target testkit/jvm/target project/target
 
       - name: Upload target directories
         if: github.event_name != 'pull_request' && (startsWith(github.ref, 'refs/tags/v') || github.ref == 'refs/heads/main')
diff --git a/build.sbt b/build.sbt
index f363f73..39a2de5 100644
--- a/build.sbt
+++ b/build.sbt
@@ -6,6 +6,8 @@ val scala3 = "3.2.2"
 
 val scalatestVersion = "3.2.15"
 val scalacheckVersion = "1.17.0"
+val weaverVersion = "0.8.2"
+val mongo4catsVersion = "0.6.10"
 
 ThisBuild / scalaVersion := scala213
 ThisBuild / crossScalaVersions := Seq(scala212, scala213, scala3)
@@ -15,7 +17,7 @@ ThisBuild / tlSonatypeUseLegacyHost := true
 
 ThisBuild / tlFatalWarnings := false
 
-ThisBuild / tlBaseVersion := "4.3"
+ThisBuild / tlBaseVersion := "4.4"
 
 ThisBuild / organization := "org.gnieh"
 ThisBuild / organizationName := "Lucas Satabin"
@@ -27,10 +29,17 @@ ThisBuild / developers := List(
 
 lazy val commonSettings = Seq(
   description := "Json diff/patch library",
-  homepage := Some(url("https://github.com/gnieh/diffson"))
+  homepage := Some(url("https://github.com/gnieh/diffson")),
+  libraryDependencies ++= List(
+    "org.scalatest" %%% "scalatest" % scalatestVersion % Test,
+    "org.scalacheck" %%% "scalacheck" % scalacheckVersion % Test,
+    "com.disneystreaming" %%% "weaver-cats" % weaverVersion % Test,
+    "com.disneystreaming" %%% "weaver-scalacheck" % weaverVersion % Test
+  ),
+  testFrameworks += new TestFramework("weaver.framework.CatsEffect")
 )
 
-lazy val diffson = tlCrossRootProject.aggregate(core, sprayJson, circe, playJson, testkit)
+lazy val diffson = tlCrossRootProject.aggregate(core, sprayJson, circe, playJson, mongo, testkit)
 
 lazy val core = crossProject(JSPlatform, JVMPlatform, NativePlatform)
   .crossType(CrossType.Pure)
@@ -41,9 +50,7 @@ lazy val core = crossProject(JSPlatform, JVMPlatform, NativePlatform)
     name := "diffson-core",
     libraryDependencies ++= Seq(
       "org.scala-lang.modules" %%% "scala-collection-compat" % "2.9.0",
-      "org.typelevel" %%% "cats-core" % "2.9.0",
-      "org.scalatest" %%% "scalatest" % scalatestVersion % Test,
-      "org.scalacheck" %%% "scalacheck" % scalacheckVersion % Test
+      "org.typelevel" %%% "cats-core" % "2.9.0"
     ),
     mimaBinaryIssueFilters ++= List(
       ProblemFilters.exclude[DirectMissingMethodProblem](
@@ -55,9 +62,21 @@ lazy val testkit = crossProject(JSPlatform, JVMPlatform, NativePlatform)
   .crossType(CrossType.Full)
   .in(file("testkit"))
   .settings(commonSettings: _*)
-  .settings(name := "diffson-testkit",
-            libraryDependencies ++= Seq("org.scalatest" %%% "scalatest" % scalatestVersion,
-                                        "org.scalacheck" %%% "scalacheck" % scalacheckVersion))
+  .settings(
+    name := "diffson-testkit",
+    libraryDependencies ++= Seq(
+      "org.scalatest" %%% "scalatest" % scalatestVersion,
+      "org.scalacheck" %%% "scalacheck" % scalacheckVersion,
+      "com.disneystreaming" %%% "weaver-cats" % weaverVersion,
+      "com.disneystreaming" %%% "weaver-scalacheck" % weaverVersion
+    )
+  )
+  .jvmSettings(
+    libraryDependencies ++= List(
+      "io.github.kirill5k" %% "mongo4cats-embedded" % mongo4catsVersion,
+      "io.github.kirill5k" %% "mongo4cats-core" % mongo4catsVersion
+    )
+  )
   .dependsOn(core)
 
 lazy val sprayJson = project
@@ -91,6 +110,32 @@ lazy val circe = crossProject(JSPlatform, JVMPlatform, NativePlatform)
   )
   .dependsOn(core, testkit % Test)
 
+lazy val mongo = crossProject(JVMPlatform)
+  .crossType(CrossType.Pure)
+  .in(file("mongo"))
+  .settings(commonSettings)
+  .settings(
+    name := "diffson-mongodb-driver",
+    libraryDependencies ++= List(
+      "org.mongodb" % "mongodb-driver-core" % "4.9.0"
+    ),
+    tlVersionIntroduced := Map("2.13" -> "4.5.0", "3" -> "4.5.0", "2.12" -> "4.5.0")
+  )
+  .dependsOn(core, testkit % Test)
+
+lazy val mongo4cats = crossProject(JVMPlatform)
+  .crossType(CrossType.Pure)
+  .in(file("mongo4cats"))
+  .settings(commonSettings)
+  .settings(
+    name := "diffson-mongo4cats",
+    libraryDependencies ++= List(
+      "io.github.kirill5k" %% "mongo4cats-core" % mongo4catsVersion
+    ),
+    tlVersionIntroduced := Map("2.13" -> "4.5.0", "3" -> "4.5.0", "2.12" -> "4.5.0")
+  )
+  .dependsOn(core, testkit % Test)
+
 lazy val benchmarks = crossProject(JVMPlatform)
   .crossType(CrossType.Pure)
   .in(file("benchmarks"))
diff --git a/core/src/main/scala/diffson/mongoupdate/MongoDiff.scala b/core/src/main/scala/diffson/mongoupdate/MongoDiff.scala
new file mode 100644
index 0000000..45987db
--- /dev/null
+++ b/core/src/main/scala/diffson/mongoupdate/MongoDiff.scala
@@ -0,0 +1,121 @@
+/*
+ * Copyright 2022 Lucas Satabin
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package diffson
+package mongoupdate
+
+import cats.Eval
+import cats.data.Chain
+import cats.syntax.all._
+
+import scala.annotation.tailrec
+
+class MongoDiff[Bson, Update](implicit Bson: Jsony[Bson], Update: Updates[Update, Bson]) extends Diff[Bson, Update] {
+  private type Path = Chain[String]
+
+  override def diff(bson1: Bson, bson2: Bson): Update =
+    diff(bson1, bson2, Chain.empty, Update.empty).value
+
+  private def diff(bson1: Bson, bson2: Bson, path: Path, acc: Update): Eval[Update] =
+    (bson1, bson2) match {
+      case (JsObject(fields1), JsObject(fields2)) =>
+        fieldsDiff(fields1.toList, fields2, path, acc)
+      case (JsArray(arr1), JsArray(arr2)) =>
+        arrayDiff(arr1, arr2, path, acc)
+      case _ if bson1 === bson2 =>
+        Eval.now(acc)
+      case _ =>
+        Eval.now(Update.set(acc, path.mkString_("."), bson2))
+    }
+
+  private def fieldsDiff(fields1: List[(String, Bson)],
+                         fields2: Map[String, Bson],
+                         path: Path,
+                         acc: Update): Eval[Update] =
+    fields1 match {
+      case (fld, value1) :: fields1 =>
+        fields2.get(fld) match {
+          case Some(value2) =>
+            diff(value1, value2, path.append(fld), acc).flatMap(fieldsDiff(fields1, fields2 - fld, path, _))
+          case None =>
+            fieldsDiff(fields1, fields2, path, Update.unset(acc, path.append(fld).mkString_(".")))
+        }
+      case Nil =>
+        Eval.now(fields2.foldLeft(acc) { case (acc, (fld, value)) =>
+          Update.set(acc, path.append(fld).mkString_("."), value)
+        })
+    }
+
+  private def arrayDiff(arr1: Vector[Bson], arr2: Vector[Bson], path: Path, acc: Update): Eval[Update] = {
+    val length1 = arr1.length
+    val length2 = arr2.length
+    if (length1 == length2) {
+      // same number of elements, diff them pairwise
+      (acc, 0).tailRecM { case (acc, idx) =>
+        if (idx >= length1)
+          Eval.now(acc.asRight)
+        else
+          diff(arr1(idx), arr2(idx), path.append(idx.toString()), acc).map((_, idx + 1).asLeft)
+      }
+    } else if (length1 > length2) {
+      // elements were deleted from the array, this is not supported yet, so replace the entire array
+      Eval.now(Update.set(acc, path.mkString_("."), JsArray(arr2)))
+    } else {
+      val nbAdded = length2 - length1
+      // there are some additions, and possibly some modifications
+      // elements can be added as a block only
+
+      // first we commpute the common prefixes and suffixes
+      @tailrec
+      def commonPrefix(idx: Int): Int =
+        if (idx >= length1)
+          length1
+        else if (arr1(idx) === arr2(idx))
+          commonPrefix(idx + 1)
+        else
+          idx
+      val commonPrefixSize = commonPrefix(0)
+      @tailrec
+      def commonSuffix(idx1: Int, idx2: Int): Int =
+        if (idx1 < 0)
+          length1
+        else if (arr1(idx1) === arr2(idx2))
+          commonSuffix(idx1 - 1, idx2 - 1)
+        else
+          length1 - 1 - idx1
+      val commonSuffixSize = commonSuffix(length1 - 1, length2 - 1)
+
+      val update =
+        if (commonPrefixSize == length1)
+          // all elements are appended
+          Update.pushEach(acc, path.mkString_("."), arr2.drop(length1).toList)
+        else if (commonSuffixSize == length1)
+          // all elements are prepended
+          Update.pushEach(acc, path.mkString_("."), 0, arr2.dropRight(length1).toList)
+        else if (commonPrefixSize + commonSuffixSize == nbAdded)
+          // allements are inserted as a block in the middle
+          Update.pushEach(acc,
+                          path.mkString_("."),
+                          commonPrefixSize,
+                          arr2.slice(commonPrefixSize, length2 - commonSuffixSize).toList)
+        else
+          Update.set(acc, path.mkString_("."), JsArray(arr2))
+
+      Eval.now(update)
+    }
+  }
+
+}
diff --git a/core/src/main/scala/diffson/mongoupdate/Updates.scala b/core/src/main/scala/diffson/mongoupdate/Updates.scala
new file mode 100644
index 0000000..51ab3ca
--- /dev/null
+++ b/core/src/main/scala/diffson/mongoupdate/Updates.scala
@@ -0,0 +1,34 @@
+/*
+ * Copyright 2022 Lucas Satabin
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package diffson.mongoupdate
+
+/** Typeclass describing the [[https://www.mongodb.com/docs/manual/reference/operator/update/ Mongo Update operators]]
+  * necessary to generate a diff.
+  */
+trait Updates[Update, Bson] {
+
+  def empty: Update
+
+  def set(base: Update, field: String, value: Bson): Update
+
+  def unset(base: Update, field: String): Update
+
+  def pushEach(base: Update, field: String, idx: Int, values: List[Bson]): Update
+
+  def pushEach(base: Update, field: String, values: List[Bson]): Update
+
+}
diff --git a/core/src/main/scala/diffson/mongoupdate/package.scala b/core/src/main/scala/diffson/mongoupdate/package.scala
new file mode 100644
index 0000000..1f6a610
--- /dev/null
+++ b/core/src/main/scala/diffson/mongoupdate/package.scala
@@ -0,0 +1,24 @@
+/*
+ * Copyright 2022 Lucas Satabin
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package diffson
+
+package object mongoupdate {
+
+  implicit def MongoDiffDiff[Bson: Jsony, Update](implicit updates: Updates[Update, Bson]): Diff[Bson, Update] =
+    new MongoDiff[Bson, Update]
+
+}
diff --git a/mongo/src/main/scala/diffson/bson/package.scala b/mongo/src/main/scala/diffson/bson/package.scala
new file mode 100644
index 0000000..bd2faaf
--- /dev/null
+++ b/mongo/src/main/scala/diffson/bson/package.scala
@@ -0,0 +1,71 @@
+/*
+ * Copyright 2022 Lucas Satabin
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package diffson
+
+import cats.syntax.all._
+import com.mongodb.client.model.{PushOptions, Updates => JUpdates}
+import org.bson._
+import org.bson.conversions.Bson
+
+import scala.jdk.CollectionConverters._
+
+import mongoupdate.Updates
+
+package object bson {
+
+  implicit object BsonJsony extends Jsony[BsonValue] {
+
+    override def eqv(x: BsonValue, y: BsonValue): Boolean =
+      x == y
+
+    override def show(t: BsonValue): String = t.toString()
+
+    override def makeObject(fields: Map[String, BsonValue]): BsonValue =
+      new BsonDocument(fields.toList.map { case (key, value) => new BsonElement(key, value) }.asJava)
+
+    override def fields(json: BsonValue): Option[Map[String, BsonValue]] =
+      json.isDocument().guard[Option].map(_ => json.asDocument().asScala.toMap)
+
+    override def makeArray(values: Vector[BsonValue]): BsonValue =
+      new BsonArray(values.asJava)
+
+    override def array(json: BsonValue): Option[Vector[BsonValue]] =
+      json.isArray().guard[Option].map(_ => json.asArray().asScala.toVector)
+
+    override def Null: BsonValue = BsonNull.VALUE
+
+  }
+
+  implicit object BsonUpdates extends Updates[List[Bson], BsonValue] {
+
+    override def empty: List[Bson] = Nil
+
+    override def set(base: List[Bson], field: String, value: BsonValue): List[Bson] =
+      JUpdates.set(field, value) :: base
+
+    override def unset(base: List[Bson], field: String): List[Bson] =
+      JUpdates.unset(field) :: base
+
+    override def pushEach(base: List[Bson], field: String, idx: Int, values: List[BsonValue]): List[Bson] =
+      JUpdates.pushEach(field, values.asJava, new PushOptions().position(idx)) :: base
+
+    override def pushEach(base: List[Bson], field: String, values: List[BsonValue]): List[Bson] =
+      JUpdates.pushEach(field, values.asJava) :: base
+
+  }
+
+}
diff --git a/mongo/src/test/scala/diffson/mongoupdate/ApplySpec.scala b/mongo/src/test/scala/diffson/mongoupdate/ApplySpec.scala
new file mode 100644
index 0000000..c537002
--- /dev/null
+++ b/mongo/src/test/scala/diffson/mongoupdate/ApplySpec.scala
@@ -0,0 +1,30 @@
+/*
+ * Copyright 2022 Lucas Satabin
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package diffson.bson
+
+import com.mongodb.client.model.Updates
+import diffson.bson.ApplyUpdateSpec
+import org.bson.conversions.Bson
+import org.bson.{BsonDocument, BsonValue}
+
+object ApplySpec extends ApplyUpdateSpec[List[Bson], BsonValue] {
+
+  override def fromBsonDocument(bson: BsonDocument): BsonValue = bson
+
+  override def toUpdate(diff: List[Bson]): Bson = Updates.combine(diff: _*)
+
+}
diff --git a/mongo/src/test/scala/diffson/mongoupdate/MongoMongoDiffSpec.scala b/mongo/src/test/scala/diffson/mongoupdate/MongoMongoDiffSpec.scala
new file mode 100644
index 0000000..ded230c
--- /dev/null
+++ b/mongo/src/test/scala/diffson/mongoupdate/MongoMongoDiffSpec.scala
@@ -0,0 +1,32 @@
+/*
+ * Copyright 2022 Lucas Satabin
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package diffson
+package bson
+
+import org.bson.conversions.Bson
+import org.bson.{BsonInt32, BsonString, BsonValue}
+
+import mongoupdate._
+import test._
+
+object MongoMongoDiffSpec extends MongoDiffSpec[List[Bson], BsonValue] {
+
+  override def int(i: Int): BsonValue = new BsonInt32(i)
+
+  override def string(s: String): BsonValue = new BsonString(s)
+
+}
diff --git a/mongo/src/test/scala/diffson/mongoupdate/test.scala b/mongo/src/test/scala/diffson/mongoupdate/test.scala
new file mode 100644
index 0000000..281ed18
--- /dev/null
+++ b/mongo/src/test/scala/diffson/mongoupdate/test.scala
@@ -0,0 +1,26 @@
+/*
+ * Copyright 2022 Lucas Satabin
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package diffson.mongoupdate
+
+import cats.Eq
+import org.bson.conversions.Bson
+
+object test {
+
+  implicit val BsonEq: Eq[Bson] = Eq.fromUniversalEquals
+
+}
diff --git a/mongo4cats/src/main/scala/diffson/bson4cats/package.scala b/mongo4cats/src/main/scala/diffson/bson4cats/package.scala
new file mode 100644
index 0000000..4d0780f
--- /dev/null
+++ b/mongo4cats/src/main/scala/diffson/bson4cats/package.scala
@@ -0,0 +1,52 @@
+package diffson
+
+import mongo4cats.bson.BsonValue
+import mongo4cats.bson.Document
+import diffson.mongoupdate.Updates
+import mongo4cats.operations.Update
+import mongo4cats.operations
+import com.mongodb.client.model.PushOptions
+
+package object bson4cats {
+
+  implicit object BsonJsony extends Jsony[BsonValue] {
+
+    override def eqv(x: BsonValue, y: BsonValue): Boolean =
+      x == y
+
+    override def show(t: BsonValue): String = t.toString()
+
+    override def makeObject(fields: Map[String, BsonValue]): BsonValue =
+      BsonValue.document(Document(fields))
+
+    override def fields(json: BsonValue): Option[Map[String, BsonValue]] =
+      json.asDocument.map(_.toMap)
+
+    override def makeArray(values: Vector[BsonValue]): BsonValue =
+      BsonValue.array(values)
+
+    override def array(json: BsonValue): Option[Vector[BsonValue]] =
+      json.asList.map(_.toVector)
+
+    override def Null: BsonValue = BsonValue.Null
+
+  }
+
+  implicit object BsonUpdates extends Updates[Update, BsonValue] {
+
+    override def empty: Update = operations.Updates.empty
+
+    override def set(base: Update, field: String, value: BsonValue): Update =
+      base.set(field, value)
+
+    override def unset(base: Update, field: String): Update =
+      base.unset(field)
+
+    override def pushEach(base: Update, field: String, idx: Int, values: List[BsonValue]): Update =
+      base.pushEach(field, values, new PushOptions().position(idx))
+
+    override def pushEach(base: Update, field: String, values: List[BsonValue]): Update =
+      base.pushEach(field, values)
+
+  }
+}
diff --git a/mongo4cats/src/main/scala/mongo4cats/operations/Updates.scala b/mongo4cats/src/main/scala/mongo4cats/operations/Updates.scala
new file mode 100644
index 0000000..0e249e0
--- /dev/null
+++ b/mongo4cats/src/main/scala/mongo4cats/operations/Updates.scala
@@ -0,0 +1,6 @@
+package mongo4cats.operations
+
+// trick to expose the empty updates
+object Updates {
+  val empty: Update = UpdateBuilder(Nil)
+}
diff --git a/mongo4cats/src/test/scala/diffson/bson4cats/ApplySpec.scala b/mongo4cats/src/test/scala/diffson/bson4cats/ApplySpec.scala
new file mode 100644
index 0000000..2849a13
--- /dev/null
+++ b/mongo4cats/src/test/scala/diffson/bson4cats/ApplySpec.scala
@@ -0,0 +1,18 @@
+package mongo4cats.diffson.bson4cats
+
+import diffson.bson.ApplyUpdateSpec
+import diffson.bson4cats._
+import mongo4cats.bson.{BsonValue, Document}
+import mongo4cats.operations.Update
+import org.bson.BsonDocument
+import org.bson.conversions.Bson
+
+object ApplySpec extends ApplyUpdateSpec[Update, BsonValue] {
+
+  override def fromBsonDocument(bson: BsonDocument): BsonValue =
+    BsonValue.document(Document.fromJava(new org.bson.Document(bson)))
+
+  override def toUpdate(diff: Update): Bson =
+    diff.toBson
+
+}
diff --git a/mongo4cats/src/test/scala/diffson/bson4cats/DiffSpec.scala b/mongo4cats/src/test/scala/diffson/bson4cats/DiffSpec.scala
new file mode 100644
index 0000000..dfb87c3
--- /dev/null
+++ b/mongo4cats/src/test/scala/diffson/bson4cats/DiffSpec.scala
@@ -0,0 +1,15 @@
+package diffson.bson4cats
+
+import diffson.mongoupdate.MongoDiffSpec
+import mongo4cats.bson.BsonValue
+import mongo4cats.operations.Update
+
+import test._
+
+object DiffSpec extends MongoDiffSpec[Update, BsonValue] {
+
+  override def int(i: Int): BsonValue = BsonValue.int(i)
+
+  override def string(s: String): BsonValue = BsonValue.string(s)
+
+}
diff --git a/mongo4cats/src/test/scala/diffson/bson4cats/test.scala b/mongo4cats/src/test/scala/diffson/bson4cats/test.scala
new file mode 100644
index 0000000..de9e31e
--- /dev/null
+++ b/mongo4cats/src/test/scala/diffson/bson4cats/test.scala
@@ -0,0 +1,8 @@
+package diffson.bson4cats
+
+import cats.Eq
+import mongo4cats.operations.Update
+
+object test {
+  implicit val updateEq: Eq[Update] = Eq.fromUniversalEquals
+}
diff --git a/testkit/jvm/src/main/scala/diffson/mongoupdate/ApplyUpdateSpec.scala b/testkit/jvm/src/main/scala/diffson/mongoupdate/ApplyUpdateSpec.scala
new file mode 100644
index 0000000..ac762d1
--- /dev/null
+++ b/testkit/jvm/src/main/scala/diffson/mongoupdate/ApplyUpdateSpec.scala
@@ -0,0 +1,105 @@
+/*
+ * Copyright 2022 Lucas Satabin
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package diffson
+package bson
+
+import cats.effect.{IO, Resource}
+import cats.syntax.all._
+import cats.{Eq, Show}
+import com.mongodb.client.model.Filters
+import de.flapdoodle.embed.mongo.distribution.Version
+import mongo4cats.client.MongoClient
+import mongo4cats.embedded.EmbeddedMongo
+import org.bson._
+import org.scalacheck.{Arbitrary, Gen}
+import weaver._
+import weaver.scalacheck._
+
+import scala.jdk.CollectionConverters._
+
+import mongoupdate._
+
+abstract class ApplyUpdateSpec[Update, Bson: Jsony](implicit Update: mongoupdate.Updates[Update, Bson])
+    extends IOSuite
+    with Checkers {
+
+  implicit val eq: Eq[BsonDocument] = Eq.fromUniversalEquals
+
+  type Res = MongoClient[IO]
+
+  override def sharedResource: Resource[IO, Res] =
+    EmbeddedMongo.start(27017, None, None, Version.V6_0_4) >>
+      MongoClient.fromConnectionString[IO]("mongodb://localhost:27017")
+
+  implicit val arbitraryBson: Arbitrary[BsonDocument] = Arbitrary {
+    val genLeaf = Gen.oneOf(
+      Gen.choose(0, 1000).map(new BsonInt32(_)),
+      Gen.alphaNumStr.map(new BsonString(_)),
+      Gen.const(new BsonBoolean(true)),
+      Gen.const(new BsonBoolean(false)),
+      Gen.const(BsonNull.VALUE)
+    )
+
+    def genArray(depth: Int, length: Int): Gen[BsonValue] =
+      for {
+        n <- Gen.choose(length / 3, length / 2)
+        c <- Gen.listOfN(n, sizedBson(depth / 2, length / 2))
+      } yield new BsonArray(c.asJava)
+
+    def genDoc(depth: Int, length: Int): Gen[BsonDocument] =
+      for {
+        n <- Gen.choose(length / 3, length / 2)
+        c <- Gen.listOfN(n, sizedBson(depth / 2, length / 2))
+      } yield new BsonDocument(c.mapWithIndex((v, idx) => new BsonElement(s"elt$idx", v)).asJava)
+
+    def sizedBson(depth: Int, length: Int) =
+      if (depth <= 0) genLeaf
+      else Gen.frequency((1, genLeaf), (2, genArray(depth, length)), (2, genDoc(depth, length)))
+
+    Gen.sized { depth =>
+      Gen.sized { length =>
+        genDoc(depth = depth, length = length)
+      }
+    }
+  }
+
+  implicit val showDoc: Show[BsonDocument] = Show.fromToString
+
+  def fromBsonDocument(bson: BsonDocument): Bson
+
+  def toUpdate(diff: Update): conversions.Bson
+
+  test("apply updates") { client =>
+    ignore("SLOW") >>
+      forall { (bson1: BsonDocument, bson2: BsonDocument) =>
+        val id = new BsonObjectId
+        bson1.put("_id", id)
+        bson2.put("_id", id)
+
+        val diff = fromBsonDocument(bson1).diff(fromBsonDocument(bson2))
+        for {
+          db <- client.getDatabase("testdb")
+          coll <- db.getCollection("docs")
+          doc = mongo4cats.bson.Document.fromJava(new Document(bson1))
+          _ <- coll.insertOne(doc)
+          _ <- coll.updateOne(Filters.eq("_id", id), toUpdate(diff))
+          foundDoc <- coll.find(Filters.eq("_id", id)).first
+        } yield expect.eql(Some(bson2), foundDoc.map(_.toBsonDocument))
+      }
+  }
+
+}
diff --git a/testkit/shared/src/main/scala/diffson/mongoupdate/MongoDiffSpec.scala b/testkit/shared/src/main/scala/diffson/mongoupdate/MongoDiffSpec.scala
new file mode 100644
index 0000000..902ff00
--- /dev/null
+++ b/testkit/shared/src/main/scala/diffson/mongoupdate/MongoDiffSpec.scala
@@ -0,0 +1,139 @@
+/*
+ * Copyright 2022 Lucas Satabin
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package diffson
+package mongoupdate
+
+import cats.Eq
+import weaver._
+
+abstract class MongoDiffSpec[Update: Eq, Bson](implicit Updates: Updates[Update, Bson], Jsony: Jsony[Bson])
+    extends SimpleIOSuite {
+
+  def int(i: Int): Bson
+
+  def string(s: String): Bson
+
+  def doc(value: Bson): Bson =
+    Jsony.makeObject(Map("value" -> value))
+
+  pureTest("append to empty array") {
+    val source = doc(Jsony.makeArray(Vector()))
+    val target = doc(Jsony.makeArray(Vector(string("a"), string("b"), string("c"), string("d"))))
+
+    val d = source.diff(target)
+
+    expect.eql(Updates.pushEach(Updates.empty, "value", List(string("a"), string("b"), string("c"), string("d"))), d)
+  }
+
+  pureTest("append to array") {
+
+    val source = doc(Jsony.makeArray(Vector(string("a"), string("b"))))
+    val target = doc(Jsony.makeArray(Vector(string("a"), string("b"), string("c"), string("d"))))
+
+    val d = source.diff(target)
+
+    expect.eql(Updates.pushEach(Updates.empty, "value", List(string("c"), string("d"))), d)
+  }
+
+  pureTest("prepend to array") {
+
+    val source = doc(Jsony.makeArray(Vector(string("c"), string("d"))))
+    val target = doc(Jsony.makeArray(Vector(string("a"), string("b"), string("c"), string("d"))))
+
+    val d = source.diff(target)
+
+    expect.eql(Updates.pushEach(Updates.empty, "value", 0, List(string("a"), string("b"))), d)
+  }
+
+  pureTest("push in the middle of an array") {
+    val source = doc(Jsony.makeArray(Vector(string("a"), string("b"), string("f"))))
+    val target =
+      doc(Jsony.makeArray(Vector(string("a"), string("b"), string("c"), string("d"), string("e"), string("f"))))
+
+    val d = source.diff(target)
+
+    expect.eql(Updates.pushEach(Updates.empty, "value", 2, List(string("c"), string("d"), string("e"))), d)
+  }
+
+  pureTest("push and modify") {
+    val source = doc(Jsony.makeArray(Vector(string("a"), string("b"), string("f"))))
+    val target =
+      doc(Jsony.makeArray(Vector(string("x"), string("b"), string("c"), string("d"), string("e"), string("f"))))
+
+    val d = source.diff(target)
+
+    expect.eql(Updates.set(
+                 Updates.empty,
+                 "value",
+                 Jsony.makeArray(Vector(string("x"), string("b"), string("c"), string("d"), string("e"), string("f")))),
+               d)
+  }
+
+  pureTest("modify in place") {
+    val source = doc(Jsony.makeArray(Vector(string("a"), string("e"), string("c"), string("f"))))
+    val target =
+      doc(Jsony.makeArray(Vector(string("a"), string("b"), string("c"), string("d"))))
+
+    val d = source.diff(target)
+
+    expect.eql(Updates.set(Updates.set(Updates.empty, "value.1", string("b")), "value.3", string("d")), d)
+  }
+
+  pureTest("delete elements of an array") {
+    val source =
+      doc(Jsony.makeArray(Vector(string("a"), string("b"), string("c"), string("d"), string("e"), string("f"))))
+    val target = doc(Jsony.makeArray(Vector(string("a"), string("b"), string("f"))))
+
+    val d = source.diff(target)
+
+    expect.eql(Updates.set(Updates.empty, "value", Jsony.makeArray(Vector(string("a"), string("b"), string("f")))), d)
+  }
+
+  pureTest("adding fields") {
+    val source = doc(Jsony.makeObject(Map("a" -> int(1), "c" -> int(3))))
+    val target = doc(Jsony.makeObject(Map("a" -> int(1), "b" -> int(2), "c" -> int(3), "z" -> int(26))))
+
+    val d = source.diff(target)
+
+    expect.eql(Updates.set(Updates.set(Updates.empty, "value.b", int(2)), "value.z", int(26)), d)
+  }
+
+  pureTest("deleting fields") {
+    val source = doc(Jsony.makeObject(Map("a" -> int(1), "b" -> int(2), "c" -> int(3), "z" -> int(26))))
+    val target = doc(Jsony.makeObject(Map("a" -> int(1), "c" -> int(3))))
+
+    val d = source.diff(target)
+
+    expect.eql(Updates.unset(Updates.unset(Updates.empty, "value.b"), "value.z"), d)
+  }
+
+  pureTest("mixed field modifications") {
+    val source = doc(
+      Jsony.makeObject(
+        Map("a" -> int(1), "b" -> int(2), "c" -> int(3), "z" -> Jsony.makeObject(Map("value" -> int(26))))))
+    val target = doc(
+      Jsony.makeObject(
+        Map("a" -> int(1), "c" -> int(3), "d" -> int(4), "z" -> Jsony.makeObject(Map("value" -> int(-1))))))
+
+    val d = source.diff(target)
+
+    expect.eql(
+      Updates.set(Updates.set(Updates.unset(Updates.empty, "value.b"), "value.z.value", int(-1)), "value.d", int(4)),
+      d)
+  }
+
+}