diff --git a/Sources/Automerge/BoundTypes/AutomergeText.swift b/Sources/Automerge/BoundTypes/AutomergeText.swift
index d7c4f641..6374230e 100644
--- a/Sources/Automerge/BoundTypes/AutomergeText.swift
+++ b/Sources/Automerge/BoundTypes/AutomergeText.swift
@@ -34,7 +34,7 @@ import Foundation
 ///
 /// > Warning: Although `AutomergeText` conforms to `ObservableObject`, it does not send notifications of content
 /// changes until it has been bound to an Automerge document.
-public final class AutomergeText: Codable {
+public final class AutomergeText: Codable, @unchecked Sendable {
     var doc: Document?
     var objId: ObjId?
     var _hashOfCurrentValue: Int
@@ -43,6 +43,17 @@ public final class AutomergeText: Codable {
     #endif
     var _unboundStorage: String
 
+    #if !os(WASI)
+    fileprivate let queue = DispatchQueue(label: "automergetext-sync-queue", qos: .userInteractive)
+    fileprivate func sync<T>(execute work: () throws -> T) rethrows -> T {
+        try queue.sync(execute: work)
+    }
+    #else
+    fileprivate func sync<T>(execute work: () throws -> T) rethrows -> T {
+        try work()
+    }
+    #endif
+
     // MARK: Initializers and Bind
 
     /// Creates a new, unbound text reference instance.
@@ -111,8 +122,10 @@ public final class AutomergeText: Codable {
     public convenience init(doc: Document, objId: ObjId) throws {
         self.init()
         if doc.objectType(obj: objId) == .Text {
-            self.doc = doc
-            self.objId = objId
+            sync {
+                self.doc = doc
+                self.objId = objId
+            }
         } else {
             throw BindingError.NotText
         }
@@ -133,7 +146,7 @@ public final class AutomergeText: Codable {
     /// Use ``bind(doc:path:)`` to associate this instance with a specific schema location within an Automerge document,
     /// or encode it as part of a larger document model into an Automerge document to store the value.
     public var isBound: Bool {
-        doc != nil && objId != nil
+        sync { doc != nil && objId != nil }
     }
 
     /// Binds a text reference instance info an Automerge document with the schema path you provide.
@@ -155,8 +168,10 @@ public final class AutomergeText: Codable {
                     .InvalidPath("First path element in an Automerge document can't be an index position.")
             }
             let textObjId = try doc.putObject(obj: ObjId.ROOT, key: codingPath[0].stringValue, ty: .Text)
-            self.doc = doc
-            objId = textObjId
+            sync {
+                self.doc = doc
+                objId = textObjId
+            }
         } else {
             guard let lastPathElement = codingPath.last else {
                 throw BindingError.InvalidPath("Unable to request a final path element from path \(path)")
@@ -174,16 +189,20 @@ public final class AutomergeText: Codable {
                         index: UInt64(indexLocation),
                         ty: .Text
                     )
-                    self.doc = doc
-                    objId = textObjId
+                    sync {
+                        self.doc = doc
+                        objId = textObjId
+                    }
                 } else {
                     let textObjId = try doc.putObject(
                         obj: secondToLastPathItemObjId,
                         key: lastPathElement.stringValue,
                         ty: .Text
                     )
-                    self.doc = doc
-                    objId = textObjId
+                    sync {
+                        self.doc = doc
+                        objId = textObjId
+                    }
                 }
             case let .failure(failure):
                 throw failure
@@ -191,7 +210,9 @@ public final class AutomergeText: Codable {
         }
         if !_unboundStorage.isEmpty {
             try updateText(newText: _unboundStorage)
-            _unboundStorage = ""
+            sync {
+                _unboundStorage = ""
+            }
         }
         observeDocForChanges()
     }
@@ -205,14 +226,18 @@ public final class AutomergeText: Codable {
     ///   - path: A string path that represents a `Text` container within the Automerge document.
     public func bind(doc: Document, id: ObjId) throws {
         if doc.objectType(obj: id) == .Text {
-            self.doc = doc
-            objId = id
+            sync {
+                self.doc = doc
+                objId = id
+            }
         } else {
             throw BindingError.NotText
         }
         if !_unboundStorage.isEmpty {
             try updateText(newText: _unboundStorage)
-            _unboundStorage = ""
+            sync {
+                _unboundStorage = ""
+            }
         }
         observeDocForChanges()
     }
@@ -240,7 +265,8 @@ public final class AutomergeText: Codable {
             // a change notification.
             Task {
                 let valueFromDoc = try doc.text(obj: objId)
-                if valueFromDoc.hashValue != self._hashOfCurrentValue {
+                let hashOfCurrentValue = self.sync { self._hashOfCurrentValue }
+                if valueFromDoc.hashValue != hashOfCurrentValue {
                     self.sendObjectWillChange()
                 }
             }
@@ -253,18 +279,22 @@ public final class AutomergeText: Codable {
     /// The string value of the text reference in an Automerge document.
     public var value: String {
         get {
-            guard let doc, let objId else {
-                return _unboundStorage
-            }
-            do {
-                return try doc.text(obj: objId)
-            } catch {
-                fatalError("Error attempting to read text value from objectId \(objId): \(error)")
+            sync {
+                guard let doc, let objId else {
+                    return _unboundStorage
+                }
+                do {
+                    return try doc.text(obj: objId)
+                } catch {
+                    fatalError("Error attempting to read text value from objectId \(objId): \(error)")
+                }
             }
         }
         set {
             guard let objId, doc != nil else {
-                _unboundStorage = newValue
+                sync {
+                    _unboundStorage = newValue
+                }
                 return
             }
             do {
@@ -281,7 +311,9 @@ public final class AutomergeText: Codable {
         }
         let current = try doc.text(obj: objId)
         if current != newText {
-            _hashOfCurrentValue = newText.hashValue
+            sync {
+                _hashOfCurrentValue = newText.hashValue
+            }
             try doc.updateText(obj: objId, value: newText)
             sendObjectWillChange()
         }
@@ -317,8 +349,10 @@ extension AutomergeText: Equatable {
 
 extension AutomergeText: Hashable {
     public func hash(into hasher: inout Hasher) {
-        hasher.combine(objId)
-        hasher.combine(_unboundStorage)
+        sync {
+            hasher.combine(objId)
+            hasher.combine(_unboundStorage)
+        }
     }
 }
 
@@ -352,7 +386,7 @@ public extension AutomergeText {
         Binding(
             get: { () -> String in
                 guard let doc = self.doc, let objId = self.objId else {
-                    return self._unboundStorage
+                    return self.sync { self._unboundStorage }
                 }
                 do {
                     return try doc.text(obj: objId)
@@ -362,7 +396,9 @@ public extension AutomergeText {
             },
             set: { (newValue: String) in
                 guard let objId = self.objId, self.doc != nil else {
-                    self._unboundStorage = newValue
+                    self.sync {
+                        self._unboundStorage = newValue
+                    }
                     return
                 }
                 do {
diff --git a/Sources/Automerge/BoundTypes/Counter.swift b/Sources/Automerge/BoundTypes/Counter.swift
index 9e97dabb..bd81fc1e 100644
--- a/Sources/Automerge/BoundTypes/Counter.swift
+++ b/Sources/Automerge/BoundTypes/Counter.swift
@@ -8,7 +8,7 @@ import Foundation
 ///
 /// As a reference type, `Counter` updates the underlying Automerge document when a value is explicitly
 /// set, or ``increment(by:)`` is called on the instance.
-public final class Counter: Codable {
+public final class Counter: Codable, @unchecked Sendable {
     var doc: Document?
     var objId: ObjId?
     var codingkey: AnyCodingKey?
@@ -18,6 +18,17 @@ public final class Counter: Codable {
     #endif
     var _unboundStorage: Int
 
+    #if !os(WASI)
+    fileprivate let queue = DispatchQueue(label: "automergecounter-sync-queue", qos: .userInteractive)
+    fileprivate func sync<T>(execute work: () throws -> T) rethrows -> T {
+        try queue.sync(execute: work)
+    }
+    #else
+    fileprivate func sync<T>(execute work: () throws -> T) rethrows -> T {
+        try work()
+    }
+    #endif
+
     // MARK: Initializers and Bind
 
     /// Creates a new, unbound counter.
@@ -49,18 +60,24 @@ public final class Counter: Codable {
         self.init()
         // TODO: convert this, akin to Text, to create an instance at the path provided
         if let index = key.intValue {
-            if case .Scalar(.Counter(_)) = try doc.get(obj: objId, index: UInt64(index)) {
-                self.doc = doc
-                self.objId = objId
-                codingkey = AnyCodingKey(key)
+            if case let .Scalar(.Counter(counterValue)) = try doc.get(obj: objId, index: UInt64(index)) {
+                sync {
+                    self.doc = doc
+                    self.objId = objId
+                    codingkey = AnyCodingKey(key)
+                    _hashOfCurrentValue = counterValue.hashValue
+                }
             } else {
                 throw BindingError.NotCounter
             }
         } else {
-            if case .Scalar(.Counter) = try doc.get(obj: objId, key: key.stringValue) {
-                self.doc = doc
-                self.objId = objId
-                codingkey = AnyCodingKey(key)
+            if case let .Scalar(.Counter(counterValue)) = try doc.get(obj: objId, key: key.stringValue) {
+                sync {
+                    self.doc = doc
+                    self.objId = objId
+                    codingkey = AnyCodingKey(key)
+                    _hashOfCurrentValue = counterValue.hashValue
+                }
             } else {
                 throw BindingError.NotCounter
             }
@@ -76,7 +93,9 @@ public final class Counter: Codable {
 
     /// Returns a Boolean value that indicates wether this reference type is actively updating an Automerge document.
     public var isBound: Bool {
-        doc != nil && objId != nil
+        sync {
+            doc != nil && objId != nil
+        }
     }
 
     /// Binds a text reference instance info an Automerge document.
@@ -101,32 +120,42 @@ public final class Counter: Codable {
         }
         if let index = key.intValue {
             if case .Scalar(.Counter) = try doc.get(obj: objId, index: UInt64(index)) {
-                self.doc = doc
-                self.objId = objId
-                codingkey = key
-                if _unboundStorage != 0 {
+                sync {
+                    self.doc = doc
+                    self.objId = objId
+                    codingkey = key
+                }
+                let currentUnboundValue = sync { self._unboundStorage }
+                if currentUnboundValue != 0 {
                     // If the unbound counter has been adjusted, positive or negative, use
                     // that as an increment value on the existing counter to ensure that
                     // all the counter changes are maintained and appended to each other.
                     try doc.increment(obj: objId, index: UInt64(index), by: Int64(_unboundStorage))
                     sendObjectWillChange()
-                    _unboundStorage = 0
+                    sync {
+                        _unboundStorage = 0
+                    }
                 }
             } else {
                 throw BindingError.NotCounter
             }
         } else {
             if case .Scalar(.Counter) = try doc.get(obj: objId, key: key.stringValue) {
-                self.doc = doc
-                self.objId = objId
-                codingkey = key
-                if _unboundStorage != 0 {
+                sync {
+                    self.doc = doc
+                    self.objId = objId
+                    codingkey = key
+                }
+                let currentUnboundValue = sync { self._unboundStorage }
+                if currentUnboundValue != 0 {
                     // If the unbound counter has been adjusted, positive or negative, use
                     // that as an increment value on the existing counter to ensure that
                     // all the counter changes are maintained and appended to each other.
                     try doc.increment(obj: objId, key: key.stringValue, by: Int64(_unboundStorage))
                     sendObjectWillChange()
-                    _unboundStorage = 0
+                    sync {
+                        _unboundStorage = 0
+                    }
                 }
             } else {
                 throw BindingError.NotCounter
@@ -156,9 +185,10 @@ public final class Counter: Codable {
             // This is firing off in a concurrent task explicitly to leave the synchronous
             // context that can happen when a doc is being updated and Combine is triggering
             // a change notification.
+            let _hashOfCurrentValue = sync { self._hashOfCurrentValue }
             Task {
                 let currentValue = self.getCounterValue()
-                if currentValue.hashValue != self._hashOfCurrentValue {
+                if currentValue.hashValue != _hashOfCurrentValue {
                     self.sendObjectWillChange()
                 }
             }
@@ -180,7 +210,9 @@ public final class Counter: Codable {
 
     fileprivate func getCounterValue() -> Int {
         guard let doc, let objId, let codingkey else {
-            return _unboundStorage
+            return sync {
+                _unboundStorage
+            }
         }
         do {
             if let index = codingkey.intValue {
@@ -199,9 +231,9 @@ public final class Counter: Codable {
     }
 
     fileprivate func setCounterValue(_ intValue: Int) {
-        _hashOfCurrentValue = intValue.hashValue
+        _hashOfCurrentValue = sync { intValue.hashValue }
         guard let objId, let doc, let codingkey else {
-            _unboundStorage = intValue
+            sync { _unboundStorage = intValue }
             return
         }
         do {
@@ -231,15 +263,19 @@ public final class Counter: Codable {
     /// - Parameter value: The value to add (or subtract) from the counter.
     public func increment(by value: Int) {
         guard let objId, let doc, let codingkey else {
-            _unboundStorage += value
-            _hashOfCurrentValue = _unboundStorage.hashValue
+            sync {
+                _unboundStorage += value
+                _hashOfCurrentValue = _unboundStorage.hashValue
+            }
             return
         }
         do {
             if let index = codingkey.intValue {
                 if case let .Scalar(.Counter(currentValue)) = try doc.get(obj: objId, index: UInt64(index)) {
                     try doc.increment(obj: objId, index: UInt64(index), by: Int64(value))
-                    _hashOfCurrentValue = (Int(currentValue) + value).hashValue
+                    sync {
+                        _hashOfCurrentValue = (Int(currentValue) + value).hashValue
+                    }
                     sendObjectWillChange()
                 } else {
                     throw BindingError.NotCounter
@@ -247,7 +283,9 @@ public final class Counter: Codable {
             } else {
                 if case let .Scalar(.Counter(currentValue)) = try doc.get(obj: objId, key: codingkey.stringValue) {
                     try doc.increment(obj: objId, key: codingkey.stringValue, by: Int64(value))
-                    _hashOfCurrentValue = (Int(currentValue) + value).hashValue
+                    sync {
+                        _hashOfCurrentValue = (Int(currentValue) + value).hashValue
+                    }
                     sendObjectWillChange()
                 } else {
                     throw BindingError.NotCounter
@@ -299,8 +337,10 @@ extension Counter: Equatable {
 
 extension Counter: Hashable {
     public func hash(into hasher: inout Hasher) {
-        hasher.combine(objId)
-        hasher.combine(value)
+        sync {
+            hasher.combine(objId)
+            hasher.combine(value)
+        }
     }
 }