diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index ba4f4a4b7894a65da5d08fefc3473e0823ca8f03..e77bc987fd3842bc495a43069a874e67346fd36e 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -20,7 +20,6 @@ stages:
   - test
   - build
   - tag
-  - version_check
   - doc-update
   - version_check
 
diff --git a/go.mod b/go.mod
index 5b856648089fdb19d8553ffcde893199ddba7de1..aa9e2404c8d26c12cda1e5d690bfb08728bf56fd 100644
--- a/go.mod
+++ b/go.mod
@@ -25,38 +25,24 @@ require (
 	github.com/desertbit/timer v0.0.0-20180107155436-c41aec40b27f // indirect
 	github.com/elliotchance/orderedmap v1.4.0 // indirect
 	github.com/forPelevin/gomoji v1.1.8 // indirect
-	github.com/fsnotify/fsnotify v1.5.4 // indirect
 	github.com/gobwas/ws v1.1.0 // indirect
 	github.com/golang-collections/collections v0.0.0-20130729185459-604e922904d3 // indirect
 	github.com/golang/protobuf v1.5.2 // indirect
 	github.com/gorilla/websocket v1.5.0 // indirect
-	github.com/hashicorp/hcl v1.0.0 // indirect
 	github.com/improbable-eng/grpc-web v0.15.0 // indirect
-	github.com/inconshreveable/mousetrap v1.0.0 // indirect
 	github.com/json-iterator/go v1.1.12 // indirect
 	github.com/klauspost/compress v1.11.7 // indirect
 	github.com/klauspost/cpuid/v2 v2.1.0 // indirect
-	github.com/magiconair/properties v1.8.6 // indirect
 	github.com/mattn/go-isatty v0.0.14 // indirect
 	github.com/mitchellh/go-homedir v1.1.0 // indirect
-	github.com/mitchellh/mapstructure v1.5.0 // indirect
 	github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 // indirect
 	github.com/oasisprotocol/curve25519-voi v0.0.0-20221003100820-41fad3beba17 // indirect
 	github.com/oasisprotocol/deoxysii v0.0.0-20220228165953-2091330c22b7 // indirect
-	github.com/pelletier/go-toml v1.9.5 // indirect
-	github.com/pelletier/go-toml/v2 v2.0.2 // indirect
-	github.com/pkg/profile v1.6.0 // indirect
 	github.com/rivo/uniseg v0.4.3 // indirect
 	github.com/rs/cors v1.8.2 // indirect
 	github.com/sethvargo/go-diceware v0.3.0 // indirect
 	github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e // indirect
 	github.com/soheilhy/cmux v0.1.5 // indirect
-	github.com/spf13/afero v1.9.2 // indirect
-	github.com/spf13/cast v1.5.0 // indirect
-	github.com/spf13/cobra v1.5.0 // indirect
-	github.com/spf13/pflag v1.0.5 // indirect
-	github.com/spf13/viper v1.12.0 // indirect
-	github.com/subosito/gotenv v1.4.0 // indirect
 	github.com/ttacon/builder v0.0.0-20170518171403-c099f663e1c2 // indirect
 	github.com/ttacon/libphonenumber v1.2.1 // indirect
 	github.com/tyler-smith/go-bip39 v1.1.0 // indirect
@@ -77,9 +63,6 @@ require (
 	google.golang.org/genproto v0.0.0-20220822174746-9e6da59bd2fc // indirect
 	google.golang.org/grpc v1.49.0 // indirect
 	google.golang.org/protobuf v1.28.1 // indirect
-	gopkg.in/ini.v1 v1.66.6 // indirect
-	gopkg.in/yaml.v2 v2.4.0 // indirect
-	gopkg.in/yaml.v3 v3.0.1 // indirect
 	nhooyr.io/websocket v1.8.7 // indirect
 	src.agwa.name/tlshacks v0.0.0-20220518131152-d2c6f4e2b780 // indirect
 )
diff --git a/indexedDb/channels/implementation.go b/indexedDb/channels/implementation.go
index e4074b69089879aa2de81e56a990dac805fc75af..ee099fbfc1d8b55c9b575ce56ad3e0a8973685dd 100644
--- a/indexedDb/channels/implementation.go
+++ b/indexedDb/channels/implementation.go
@@ -40,6 +40,8 @@ type wasmModel struct {
 	db                *idb.Database
 	cipher            cryptoChannel.Cipher
 	receivedMessageCB MessageReceivedCallback
+	deletedMessageCB  DeletedMessageCallback
+	mutedUserCB       MutedUserCallback
 	updateMux         sync.Mutex
 }
 
@@ -489,8 +491,22 @@ func (w *wasmModel) GetMessage(
 // DeleteMessage removes a message with the given messageID from storage.
 func (w *wasmModel) DeleteMessage(messageID message.ID) error {
 	msgId := js.ValueOf(base64.StdEncoding.EncodeToString(messageID.Bytes()))
-	return indexedDb.DeleteIndex(w.db, messageStoreName,
+
+	err := indexedDb.DeleteIndex(w.db, messageStoreName,
 		messageStoreMessageIndex, pkeyName, msgId)
+	if err != nil {
+		return err
+	}
+
+	go w.deletedMessageCB(messageID)
+
+	return nil
+}
+
+// MuteUser is called whenever a user is muted or unmuted.
+func (w *wasmModel) MuteUser(
+	channelID *id.ID, pubKey ed25519.PublicKey, unmute bool) {
+	go w.mutedUserCB(channelID, pubKey, unmute)
 }
 
 // msgIDLookup gets the UUID of the Message with the given messageID.
@@ -511,5 +527,4 @@ func (w *wasmModel) msgIDLookup(messageID message.ID) (*Message, error) {
 		return nil, err
 	}
 	return resultMsg, nil
-
 }
diff --git a/indexedDb/channels/implementation_test.go b/indexedDb/channels/implementation_test.go
index 79a02de774ae701b6b579d356690f931f7a53457..19b0f313dd4bc11325150838007dd689b873a031 100644
--- a/indexedDb/channels/implementation_test.go
+++ b/indexedDb/channels/implementation_test.go
@@ -10,6 +10,7 @@
 package channels
 
 import (
+	"crypto/ed25519"
 	"encoding/json"
 	"fmt"
 	"github.com/hack-pad/go-indexeddb/idb"
@@ -36,12 +37,14 @@ func TestMain(m *testing.M) {
 	os.Exit(m.Run())
 }
 
-func dummyCallback(uint64, *id.ID, bool) {}
+func dummyReceivedMessageCB(uint64, *id.ID, bool)      {}
+func dummyDeletedMessageCB(message.ID)                 {}
+func dummyMutedUserCB(*id.ID, ed25519.PublicKey, bool) {}
 
 // Happy path, insert message and look it up
 func TestWasmModel_msgIDLookup(t *testing.T) {
 	cipher, err := cryptoChannel.NewCipher(
-		[]byte("testpass"), []byte("testsalt"), 128, csprng.NewSystemRNG())
+		[]byte("testPass"), []byte("testSalt"), 128, csprng.NewSystemRNG())
 	if err != nil {
 		t.Fatalf("Failed to create cipher")
 	}
@@ -56,9 +59,10 @@ func TestWasmModel_msgIDLookup(t *testing.T) {
 			testString := "TestWasmModel_msgIDLookup" + cs
 			testMsgId := message.DeriveChannelMessageID(&id.ID{1}, 0, []byte(testString))
 
-			eventModel, err := newWASMModel(testString, c, dummyCallback)
-			if err != nil {
-				t.Fatalf("%+v", err)
+			eventModel, err2 := newWASMModel(testString, c,
+				dummyReceivedMessageCB, dummyDeletedMessageCB, dummyMutedUserCB)
+			if err2 != nil {
+				t.Fatalf("%+v", err2)
 			}
 
 			testMsg := buildMessage([]byte(testString), testMsgId.Bytes(), nil,
@@ -69,9 +73,9 @@ func TestWasmModel_msgIDLookup(t *testing.T) {
 				t.Fatalf("%+v", err)
 			}
 
-			msg, err := eventModel.msgIDLookup(testMsgId)
-			if err != nil {
-				t.Fatalf("%+v", err)
+			msg, err2 := eventModel.msgIDLookup(testMsgId)
+			if err2 != nil {
+				t.Fatalf("%+v", err2)
 			}
 			if msg.ID == 0 {
 				t.Fatalf("Expected to get a UUID!")
@@ -85,7 +89,8 @@ func TestWasmModel_DeleteMessage(t *testing.T) {
 	storage.GetLocalStorage().Clear()
 	testString := "TestWasmModel_DeleteMessage"
 	testMsgId := message.DeriveChannelMessageID(&id.ID{1}, 0, []byte(testString))
-	eventModel, err := newWASMModel(testString, nil, dummyCallback)
+	eventModel, err := newWASMModel(testString, nil,
+		dummyReceivedMessageCB, dummyDeletedMessageCB, dummyMutedUserCB)
 	if err != nil {
 		t.Fatalf("%+v", err)
 	}
@@ -127,7 +132,7 @@ func TestWasmModel_DeleteMessage(t *testing.T) {
 // Test wasmModel.UpdateSentStatus happy path and ensure fields don't change.
 func Test_wasmModel_UpdateSentStatus(t *testing.T) {
 	cipher, err := cryptoChannel.NewCipher(
-		[]byte("testpass"), []byte("testsalt"), 128, csprng.NewSystemRNG())
+		[]byte("testPass"), []byte("testSalt"), 128, csprng.NewSystemRNG())
 	if err != nil {
 		t.Fatalf("Failed to create cipher")
 	}
@@ -141,9 +146,10 @@ func Test_wasmModel_UpdateSentStatus(t *testing.T) {
 			testString := "Test_wasmModel_UpdateSentStatus" + cs
 			testMsgId := message.DeriveChannelMessageID(
 				&id.ID{1}, 0, []byte(testString))
-			eventModel, err := newWASMModel(testString, c, dummyCallback)
-			if err != nil {
-				t.Fatalf("%+v", err)
+			eventModel, err2 := newWASMModel(testString, c,
+				dummyReceivedMessageCB, dummyDeletedMessageCB, dummyMutedUserCB)
+			if err2 != nil {
+				t.Fatalf("%+v", err2)
 			}
 
 			// Store a test message
@@ -170,17 +176,17 @@ func Test_wasmModel_UpdateSentStatus(t *testing.T) {
 				uuid, nil, nil, nil, nil, nil, &expectedStatus)
 
 			// Check the resulting status
-			results, err = indexedDb.Dump(eventModel.db, messageStoreName)
-			if err != nil {
-				t.Fatalf("%+v", err)
+			results, err2 = indexedDb.Dump(eventModel.db, messageStoreName)
+			if err2 != nil {
+				t.Fatalf("%+v", err2)
 			}
 			if len(results) != 1 {
 				t.Fatalf("Expected 1 message to exist")
 			}
 			resultMsg := &Message{}
-			err = json.Unmarshal([]byte(results[0]), resultMsg)
-			if err != nil {
-				t.Fatalf("%+v", err)
+			err2 = json.Unmarshal([]byte(results[0]), resultMsg)
+			if err2 != nil {
+				t.Fatalf("%+v", err2)
 			}
 			if resultMsg.Status != uint8(expectedStatus) {
 				t.Fatalf("Unexpected Status: %v", resultMsg.Status)
@@ -197,7 +203,7 @@ func Test_wasmModel_UpdateSentStatus(t *testing.T) {
 // Smoke test wasmModel.JoinChannel/wasmModel.LeaveChannel happy paths.
 func Test_wasmModel_JoinChannel_LeaveChannel(t *testing.T) {
 	cipher, err := cryptoChannel.NewCipher(
-		[]byte("testpass"), []byte("testsalt"), 128, csprng.NewSystemRNG())
+		[]byte("testPass"), []byte("testSalt"), 128, csprng.NewSystemRNG())
 	if err != nil {
 		t.Fatalf("Failed to create cipher")
 	}
@@ -208,9 +214,10 @@ func Test_wasmModel_JoinChannel_LeaveChannel(t *testing.T) {
 		}
 		t.Run("Test_wasmModel_JoinChannel_LeaveChannel"+cs, func(t *testing.T) {
 			storage.GetLocalStorage().Clear()
-			eventModel, err := newWASMModel("test", c, dummyCallback)
-			if err != nil {
-				t.Fatalf("%+v", err)
+			eventModel, err2 := newWASMModel("test", c,
+				dummyReceivedMessageCB, dummyDeletedMessageCB, dummyMutedUserCB)
+			if err2 != nil {
+				t.Fatalf("%+v", err2)
 			}
 
 			testChannel := &cryptoBroadcast.Channel{
@@ -227,17 +234,17 @@ func Test_wasmModel_JoinChannel_LeaveChannel(t *testing.T) {
 			}
 			eventModel.JoinChannel(testChannel)
 			eventModel.JoinChannel(testChannel2)
-			results, err := indexedDb.Dump(eventModel.db, channelsStoreName)
-			if err != nil {
-				t.Fatalf("%+v", err)
+			results, err2 := indexedDb.Dump(eventModel.db, channelsStoreName)
+			if err2 != nil {
+				t.Fatalf("%+v", err2)
 			}
 			if len(results) != 2 {
 				t.Fatalf("Expected 2 channels to exist")
 			}
 			eventModel.LeaveChannel(testChannel.ReceptionID)
-			results, err = indexedDb.Dump(eventModel.db, channelsStoreName)
-			if err != nil {
-				t.Fatalf("%+v", err)
+			results, err2 = indexedDb.Dump(eventModel.db, channelsStoreName)
+			if err2 != nil {
+				t.Fatalf("%+v", err2)
 			}
 			if len(results) != 1 {
 				t.Fatalf("Expected 1 channels to exist")
@@ -249,7 +256,7 @@ func Test_wasmModel_JoinChannel_LeaveChannel(t *testing.T) {
 // Test UUID gets returned when different messages are added.
 func Test_wasmModel_UUIDTest(t *testing.T) {
 	cipher, err := cryptoChannel.NewCipher(
-		[]byte("testpass"), []byte("testsalt"), 128, csprng.NewSystemRNG())
+		[]byte("testPass"), []byte("testSalt"), 128, csprng.NewSystemRNG())
 	if err != nil {
 		t.Fatalf("Failed to create cipher")
 	}
@@ -261,9 +268,10 @@ func Test_wasmModel_UUIDTest(t *testing.T) {
 		t.Run("Test_wasmModel_UUIDTest"+cs, func(t *testing.T) {
 			storage.GetLocalStorage().Clear()
 			testString := "testHello" + cs
-			eventModel, err := newWASMModel(testString, c, dummyCallback)
-			if err != nil {
-				t.Fatalf("%+v", err)
+			eventModel, err2 := newWASMModel(testString, c,
+				dummyReceivedMessageCB, dummyDeletedMessageCB, dummyMutedUserCB)
+			if err2 != nil {
+				t.Fatalf("%+v", err2)
 			}
 
 			uuids := make([]uint64, 10)
@@ -295,7 +303,7 @@ func Test_wasmModel_UUIDTest(t *testing.T) {
 // Tests if the same message ID being sent always returns the same UUID.
 func Test_wasmModel_DuplicateReceives(t *testing.T) {
 	cipher, err := cryptoChannel.NewCipher(
-		[]byte("testpass"), []byte("testsalt"), 128, csprng.NewSystemRNG())
+		[]byte("testPass"), []byte("testSalt"), 128, csprng.NewSystemRNG())
 	if err != nil {
 		t.Fatalf("Failed to create cipher")
 	}
@@ -307,9 +315,10 @@ func Test_wasmModel_DuplicateReceives(t *testing.T) {
 		t.Run("Test_wasmModel_DuplicateReceives"+cs, func(t *testing.T) {
 			storage.GetLocalStorage().Clear()
 			testString := "testHello"
-			eventModel, err := newWASMModel(testString, c, dummyCallback)
-			if err != nil {
-				t.Fatalf("%+v", err)
+			eventModel, err2 := newWASMModel(testString, c,
+				dummyReceivedMessageCB, dummyDeletedMessageCB, dummyMutedUserCB)
+			if err2 != nil {
+				t.Fatalf("%+v", err2)
 			}
 
 			uuids := make([]uint64, 10)
@@ -342,7 +351,7 @@ func Test_wasmModel_DuplicateReceives(t *testing.T) {
 // result is as expected.
 func Test_wasmModel_deleteMsgByChannel(t *testing.T) {
 	cipher, err := cryptoChannel.NewCipher(
-		[]byte("testpass"), []byte("testsalt"), 128, csprng.NewSystemRNG())
+		[]byte("testPass"), []byte("testSalt"), 128, csprng.NewSystemRNG())
 	if err != nil {
 		t.Fatalf("Failed to create cipher")
 	}
@@ -356,9 +365,10 @@ func Test_wasmModel_deleteMsgByChannel(t *testing.T) {
 			testString := "test_deleteMsgByChannel"
 			totalMessages := 10
 			expectedMessages := 5
-			eventModel, err := newWASMModel(testString, c, dummyCallback)
-			if err != nil {
-				t.Fatalf("%+v", err)
+			eventModel, err2 := newWASMModel(testString, c,
+				dummyReceivedMessageCB, dummyDeletedMessageCB, dummyMutedUserCB)
+			if err2 != nil {
+				t.Fatalf("%+v", err2)
 			}
 
 			// Create a test channel id
@@ -384,9 +394,9 @@ func Test_wasmModel_deleteMsgByChannel(t *testing.T) {
 			}
 
 			// Check pre-results
-			result, err := indexedDb.Dump(eventModel.db, messageStoreName)
-			if err != nil {
-				t.Fatalf("%+v", err)
+			result, err2 := indexedDb.Dump(eventModel.db, messageStoreName)
+			if err2 != nil {
+				t.Fatalf("%+v", err2)
 			}
 			if len(result) != totalMessages {
 				t.Errorf("Expected %d messages, got %d", totalMessages, len(result))
@@ -414,7 +424,7 @@ func Test_wasmModel_deleteMsgByChannel(t *testing.T) {
 // Inserts will not fail, they simply will not happen.
 func TestWasmModel_receiveHelper_UniqueIndex(t *testing.T) {
 	cipher, err := cryptoChannel.NewCipher(
-		[]byte("testpass"), []byte("testsalt"), 128, csprng.NewSystemRNG())
+		[]byte("testPass"), []byte("testSalt"), 128, csprng.NewSystemRNG())
 	if err != nil {
 		t.Fatalf("Failed to create cipher")
 	}
@@ -426,29 +436,30 @@ func TestWasmModel_receiveHelper_UniqueIndex(t *testing.T) {
 		t.Run("TestWasmModel_receiveHelper_UniqueIndex"+cs, func(t *testing.T) {
 			storage.GetLocalStorage().Clear()
 			testString := fmt.Sprintf("test_receiveHelper_UniqueIndex_%d", i)
-			eventModel, err := newWASMModel(testString, c, dummyCallback)
-			if err != nil {
-				t.Fatal(err)
+			eventModel, err2 := newWASMModel(testString, c,
+				dummyReceivedMessageCB, dummyDeletedMessageCB, dummyMutedUserCB)
+			if err2 != nil {
+				t.Fatal(err2)
 			}
 
 			// Ensure index is unique
-			txn, err := eventModel.db.Transaction(
+			txn, err2 := eventModel.db.Transaction(
 				idb.TransactionReadOnly, messageStoreName)
-			if err != nil {
-				t.Fatal(err)
+			if err2 != nil {
+				t.Fatal(err2)
 			}
-			store, err := txn.ObjectStore(messageStoreName)
-			if err != nil {
-				t.Fatal(err)
+			store, err2 := txn.ObjectStore(messageStoreName)
+			if err2 != nil {
+				t.Fatal(err2)
 			}
-			idx, err := store.Index(messageStoreMessageIndex)
-			if err != nil {
-				t.Fatal(err)
+			idx, err2 := store.Index(messageStoreMessageIndex)
+			if err2 != nil {
+				t.Fatal(err2)
 			}
-			if isUnique, err2 := idx.Unique(); !isUnique {
+			if isUnique, err3 := idx.Unique(); !isUnique {
 				t.Fatalf("Index is not unique!")
-			} else if err2 != nil {
-				t.Fatal(err2)
+			} else if err3 != nil {
+				t.Fatal(err3)
 			}
 
 			// First message insert should succeed
diff --git a/indexedDb/channels/init.go b/indexedDb/channels/init.go
index 12d6bb8840f3d75c8778b871ac883a84ad008ada..54867ea9d4668195b6bb97e7341f0366b4efcecd 100644
--- a/indexedDb/channels/init.go
+++ b/indexedDb/channels/init.go
@@ -10,11 +10,13 @@
 package channels
 
 import (
+	"crypto/ed25519"
 	"github.com/hack-pad/go-indexeddb/idb"
 	"github.com/pkg/errors"
 	jww "github.com/spf13/jwalterweatherman"
 	"gitlab.com/elixxir/client/v4/channels"
 	cryptoChannel "gitlab.com/elixxir/crypto/channel"
+	"gitlab.com/elixxir/crypto/message"
 	"gitlab.com/elixxir/xxdk-wasm/indexedDb"
 	"gitlab.com/elixxir/xxdk-wasm/storage"
 	"gitlab.com/xx_network/primitives/id"
@@ -36,13 +38,24 @@ const (
 // update is true if the row is old and was edited.
 type MessageReceivedCallback func(uuid uint64, channelID *id.ID, update bool)
 
+// DeletedMessageCallback is called any time a message is deleted.
+type DeletedMessageCallback func(messageID message.ID)
+
+// MutedUserCallback is called any time a user is muted or unmuted. unmute is
+// true if the user has been unmuted and false if they have been muted.
+type MutedUserCallback func(
+	channelID *id.ID, pubKey ed25519.PublicKey, unmute bool)
+
 // NewWASMEventModelBuilder returns an EventModelBuilder which allows
 // the channel manager to define the path but the callback is the same
 // across the board.
 func NewWASMEventModelBuilder(encryption cryptoChannel.Cipher,
-	cb MessageReceivedCallback) channels.EventModelBuilder {
+	messageReceivedCB MessageReceivedCallback,
+	deletedMessageCB DeletedMessageCallback,
+	mutedUserCB MutedUserCallback) channels.EventModelBuilder {
 	fn := func(path string) (channels.EventModel, error) {
-		return NewWASMEventModel(path, encryption, cb)
+		return NewWASMEventModel(
+			path, encryption, messageReceivedCB, deletedMessageCB, mutedUserCB)
 	}
 	return fn
 }
@@ -50,14 +63,19 @@ func NewWASMEventModelBuilder(encryption cryptoChannel.Cipher,
 // NewWASMEventModel returns a [channels.EventModel] backed by a wasmModel.
 // The name should be a base64 encoding of the users public key.
 func NewWASMEventModel(path string, encryption cryptoChannel.Cipher,
-	cb MessageReceivedCallback) (channels.EventModel, error) {
+	messageReceivedCB MessageReceivedCallback,
+	deletedMessageCB DeletedMessageCallback,
+	mutedUserCB MutedUserCallback) (channels.EventModel, error) {
 	databaseName := path + databaseSuffix
-	return newWASMModel(databaseName, encryption, cb)
+	return newWASMModel(databaseName,
+		encryption, messageReceivedCB, deletedMessageCB, mutedUserCB)
 }
 
 // newWASMModel creates the given [idb.Database] and returns a wasmModel.
 func newWASMModel(databaseName string, encryption cryptoChannel.Cipher,
-	cb MessageReceivedCallback) (*wasmModel, error) {
+	messageReceivedCB MessageReceivedCallback,
+	deletedMessageCB DeletedMessageCallback,
+	mutedUserCB MutedUserCallback) (*wasmModel, error) {
 	// Attempt to open database object
 	ctx, cancel := indexedDb.NewContext()
 	defer cancel()
@@ -122,7 +140,13 @@ func newWASMModel(databaseName string, encryption cryptoChannel.Cipher,
 	if err != nil {
 		return nil, err
 	}
-	wrapper := &wasmModel{db: db, receivedMessageCB: cb, cipher: encryption}
+	wrapper := &wasmModel{
+		db:                db,
+		cipher:            encryption,
+		receivedMessageCB: messageReceivedCB,
+		deletedMessageCB:  deletedMessageCB,
+		mutedUserCB:       mutedUserCB,
+	}
 
 	return wrapper, nil
 }
diff --git a/indexedDb/dm/implementation.go b/indexedDb/dm/implementation.go
index afd4e3bff44d739d7a8d3d687563c390fde7f743..b5c023d8f2ad920783533527608ce36d9d8c0ea9 100644
--- a/indexedDb/dm/implementation.go
+++ b/indexedDb/dm/implementation.go
@@ -102,7 +102,8 @@ func (w *wasmModel) Receive(messageID message.ID, nickname string, text []byte,
 	parentErr := errors.New("failed to Receive")
 
 	// If there is no extant Conversation, create one.
-	_, err := indexedDb.Get(w.db, conversationStoreName, utils.CopyBytesToJS(pubKey))
+	_, err := indexedDb.Get(
+		w.db, conversationStoreName, utils.CopyBytesToJS(pubKey))
 	if err != nil {
 		if strings.Contains(err.Error(), indexedDb.ErrDoesNotExist) {
 			err = w.joinConversation(nickname, pubKey, dmToken, codeset)
@@ -144,7 +145,8 @@ func (w *wasmModel) ReceiveText(messageID message.ID, nickname, text string,
 	parentErr := errors.New("failed to ReceiveText")
 
 	// If there is no extant Conversation, create one.
-	_, err := indexedDb.Get(w.db, conversationStoreName, utils.CopyBytesToJS(pubKey))
+	_, err := indexedDb.Get(
+		w.db, conversationStoreName, utils.CopyBytesToJS(pubKey))
 	if err != nil {
 		if strings.Contains(err.Error(), indexedDb.ErrDoesNotExist) {
 			err = w.joinConversation(nickname, pubKey, dmToken, codeset)
@@ -188,7 +190,8 @@ func (w *wasmModel) ReceiveReply(messageID, reactionTo message.ID, nickname,
 	parentErr := errors.New("failed to ReceiveReply")
 
 	// If there is no extant Conversation, create one.
-	_, err := indexedDb.Get(w.db, conversationStoreName, utils.CopyBytesToJS(pubKey))
+	_, err := indexedDb.Get(
+		w.db, conversationStoreName, utils.CopyBytesToJS(pubKey))
 	if err != nil {
 		if strings.Contains(err.Error(), indexedDb.ErrDoesNotExist) {
 			err = w.joinConversation(nickname, pubKey, dmToken, codeset)
@@ -226,13 +229,14 @@ func (w *wasmModel) ReceiveReply(messageID, reactionTo message.ID, nickname,
 	return uuid
 }
 
-func (w *wasmModel) ReceiveReaction(messageID, reactionTo message.ID, nickname,
+func (w *wasmModel) ReceiveReaction(messageID, _ message.ID, nickname,
 	reaction string, pubKey ed25519.PublicKey, dmToken uint32, codeset uint8,
 	timestamp time.Time, round rounds.Round, status dm.Status) uint64 {
 	parentErr := errors.New("failed to ReceiveText")
 
 	// If there is no extant Conversation, create one.
-	_, err := indexedDb.Get(w.db, conversationStoreName, utils.CopyBytesToJS(pubKey))
+	_, err := indexedDb.Get(
+		w.db, conversationStoreName, utils.CopyBytesToJS(pubKey))
 	if err != nil {
 		if strings.Contains(err.Error(), indexedDb.ErrDoesNotExist) {
 			err = w.joinConversation(nickname, pubKey, dmToken, codeset)
diff --git a/indexedDb/dm/init.go b/indexedDb/dm/init.go
index 380df647c810d7fb6239cee9c4e7c1fc4181e4ca..29b80911804004539beb210dc85533afc7c98f31 100644
--- a/indexedDb/dm/init.go
+++ b/indexedDb/dm/init.go
@@ -35,7 +35,8 @@ const (
 // MessageReceivedCallback is called any time a message is received or updated.
 //
 // update is true if the row is old and was edited.
-type MessageReceivedCallback func(uuid uint64, pubKey ed25519.PublicKey, update bool)
+type MessageReceivedCallback func(
+	uuid uint64, pubKey ed25519.PublicKey, update bool)
 
 // NewWASMEventModel returns a [channels.EventModel] backed by a wasmModel.
 // The name should be a base64 encoding of the users public key.
diff --git a/wasm/channels.go b/wasm/channels.go
index 2535e7c0965eed81a1ac6f939d75812732d16042..2248b1b31d768b84982d893654e7d1b6f959cd5b 100644
--- a/wasm/channels.go
+++ b/wasm/channels.go
@@ -10,10 +10,12 @@
 package wasm
 
 import (
+	"crypto/ed25519"
 	"encoding/base64"
 	"encoding/json"
 	"errors"
 	"gitlab.com/elixxir/client/v4/channels"
+	"gitlab.com/elixxir/crypto/message"
 	channelsDb "gitlab.com/elixxir/xxdk-wasm/indexedDb/channels"
 	"gitlab.com/xx_network/primitives/id"
 	"sync"
@@ -319,14 +321,23 @@ func LoadChannelsManager(_ js.Value, args []js.Value) any {
 //     using [Cmix.GetID].
 //   - args[1] - Bytes of a private identity ([channel.PrivateIdentity]) that is
 //     generated by [GenerateChannelIdentity] (Uint8Array).
-//   - args[2] - Function that takes in the same parameters as
-//     [indexedDb.MessageReceivedCallback]. On the Javascript side, the UUID is
-//     returned as an int and the channelID as a Uint8Array. The row in the
-//     database that was updated can be found using the UUID. The channel ID is
-//     provided so that the recipient can filter if they want to the processes
-//     the update now or not. An "update" bool is present which tells you if the
-//     row is new or if it is an edited old row.
-//   - args[3] - ID of [ChannelDbCipher] object in tracker (int). Create this
+//   - args[2] - The received message callback, which is called everytime a
+//     message is added or changed in the database. It is a function that takes
+//     in the same parameters as [indexedDb.MessageReceivedCallback]. On the
+//     Javascript side, the UUID is returned as an int and the channelID as a
+//     Uint8Array. The row in the database that was updated can be found using
+//     the UUID. The channel ID is provided so that the recipient can filter if
+//     they want to the processes the update now or not. An "update" bool is
+//     present which tells you if the row is new or if it is an edited old row.
+//   - args[3] - The deleted message callback, which is called everytime a
+//     message is deleted from the database. It is a function that takes in the
+//     same parameters as [indexedDb.DeletedMessageCallback]. On the Javascript
+//     side, the message ID is returned as a Uint8Array.
+//   - args[4] - The muted user callback, which is called everytime a user is
+//     muted or unmuted. It is a function that takes in the same parameters as
+//     [indexedDb.MutedUserCallback]. On the Javascript side, the channel ID and
+//     user public key are returned as Uint8Array.
+//   - args[5] - ID of [ChannelDbCipher] object in tracker (int). Create this
 //     object with [NewChannelsDatabaseCipher] and get its id with
 //     [ChannelDbCipher.GetID].
 //
@@ -338,15 +349,17 @@ func NewChannelsManagerWithIndexedDb(_ js.Value, args []js.Value) any {
 	cmixID := args[0].Int()
 	privateIdentity := utils.CopyBytesToGo(args[1])
 	messageReceivedCB := args[2]
-	cipherID := args[3].Int()
+	deletedMessageCB := args[3]
+	mutedUserCB := args[4]
+	cipherID := args[5].Int()
 
 	cipher, err := bindings.GetChannelDbCipherTrackerFromID(cipherID)
 	if err != nil {
 		utils.Throw(utils.TypeError, err)
 	}
 
-	return newChannelsManagerWithIndexedDb(
-		cmixID, privateIdentity, messageReceivedCB, cipher)
+	return newChannelsManagerWithIndexedDb(cmixID, privateIdentity,
+		messageReceivedCB, deletedMessageCB, mutedUserCB, cipher)
 }
 
 // NewChannelsManagerWithIndexedDbUnsafe creates a new [ChannelsManager] from a
@@ -366,13 +379,22 @@ func NewChannelsManagerWithIndexedDb(_ js.Value, args []js.Value) any {
 //     using [Cmix.GetID].
 //   - args[1] - Bytes of a private identity ([channel.PrivateIdentity]) that is
 //     generated by [GenerateChannelIdentity] (Uint8Array).
-//   - args[2] - Function that takes in the same parameters as
-//     [indexedDb.MessageReceivedCallback]. On the Javascript side, the UUID is
-//     returned as an int and the channelID as a Uint8Array. The row in the
-//     database that was updated can be found using the UUID. The channel ID is
-//     provided so that the recipient can filter if they want to the processes
-//     the update now or not. An "update" bool is present which tells you if
-//     the row is new or if it is an edited old row
+//   - args[2] - The received message callback, which is called everytime a
+//     message is added or changed in the database. It is a function that takes
+//     in the same parameters as [indexedDb.MessageReceivedCallback]. On the
+//     Javascript side, the UUID is returned as an int and the channelID as a
+//     Uint8Array. The row in the database that was updated can be found using
+//     the UUID. The channel ID is provided so that the recipient can filter if
+//     they want to the processes the update now or not. An "update" bool is
+//     present which tells you if the row is new or if it is an edited old row.
+//   - args[3] - The deleted message callback, which is called everytime a
+//     message is deleted from the database. It is a function that takes in the
+//     same parameters as [indexedDb.DeletedMessageCallback]. On the Javascript
+//     side, the message ID is returned as a Uint8Array.
+//   - args[4] - The muted user callback, which is called everytime a user is
+//     muted or unmuted. It is a function that takes in the same parameters as
+//     [indexedDb.MutedUserCallback]. On the Javascript side, the channel ID and
+//     user public key are returned as Uint8Array.
 //
 // Returns a promise:
 //   - Resolves to a Javascript representation of the [ChannelsManager] object.
@@ -381,19 +403,32 @@ func NewChannelsManagerWithIndexedDbUnsafe(_ js.Value, args []js.Value) any {
 	cmixID := args[0].Int()
 	privateIdentity := utils.CopyBytesToGo(args[1])
 	messageReceivedCB := args[2]
+	deletedMessageCB := args[3]
+	mutedUserCB := args[4]
 
-	return newChannelsManagerWithIndexedDb(
-		cmixID, privateIdentity, messageReceivedCB, nil)
+	return newChannelsManagerWithIndexedDb(cmixID, privateIdentity,
+		messageReceivedCB, deletedMessageCB, mutedUserCB, nil)
 }
 
 func newChannelsManagerWithIndexedDb(cmixID int, privateIdentity []byte,
-	cb js.Value, cipher *bindings.ChannelDbCipher) any {
+	messageReceivedCB, deletedMessageCB, mutedUserCB js.Value,
+	cipher *bindings.ChannelDbCipher) any {
 
-	messageReceivedCB := func(uuid uint64, channelID *id.ID, update bool) {
-		cb.Invoke(uuid, utils.CopyBytesToJS(channelID.Marshal()), update)
+	messageReceived := func(uuid uint64, channelID *id.ID, update bool) {
+		messageReceivedCB.Invoke(uuid, utils.CopyBytesToJS(channelID.Marshal()), update)
 	}
 
-	model := channelsDb.NewWASMEventModelBuilder(cipher, messageReceivedCB)
+	deletedMessage := func(messageID message.ID) {
+		deletedMessageCB.Invoke(utils.CopyBytesToJS(messageID.Marshal()))
+	}
+
+	mutedUser := func(channelID *id.ID, pubKey ed25519.PublicKey, unmute bool) {
+		mutedUserCB.Invoke(utils.CopyBytesToJS(channelID.Marshal()),
+			utils.CopyBytesToJS(pubKey), unmute)
+	}
+
+	model := channelsDb.NewWASMEventModelBuilder(
+		cipher, messageReceived, deletedMessage, mutedUser)
 
 	promiseFn := func(resolve, reject func(args ...any) js.Value) {
 		cm, err := bindings.NewChannelsManagerGoEventModel(
@@ -421,14 +456,23 @@ func newChannelsManagerWithIndexedDb(cmixID int, privateIdentity []byte,
 //     using [Cmix.GetID].
 //   - args[1] - The storage tag associated with the previously created channel
 //     manager and retrieved with [ChannelsManager.GetStorageTag] (string).
-//   - args[2] - Function that takes in the same parameters as
-//     [indexedDb.MessageReceivedCallback]. On the Javascript side, the UUID is
-//     returned as an int and the channelID as a Uint8Array. The row in the
-//     database that was updated can be found using the UUID. The channel ID is
-//     provided so that the recipient can filter if they want to the processes
-//     the update now or not. An "update" bool is present which tells you if the
-//     row is new or if it is an edited old row.
-//   - args[3] - ID of [ChannelDbCipher] object in tracker (int). Create this
+//   - args[2] - The received message callback, which is called everytime a
+//     message is added or changed in the database. It is a function that takes
+//     in the same parameters as [indexedDb.MessageReceivedCallback]. On the
+//     Javascript side, the UUID is returned as an int and the channelID as a
+//     Uint8Array. The row in the database that was updated can be found using
+//     the UUID. The channel ID is provided so that the recipient can filter if
+//     they want to the processes the update now or not. An "update" bool is
+//     present which tells you if the row is new or if it is an edited old row.
+//   - args[3] - The deleted message callback, which is called everytime a
+//     message is deleted from the database. It is a function that takes in the
+//     same parameters as [indexedDb.DeletedMessageCallback]. On the Javascript
+//     side, the message ID is returned as a Uint8Array.
+//   - args[4] - The muted user callback, which is called everytime a user is
+//     muted or unmuted. It is a function that takes in the same parameters as
+//     [indexedDb.MutedUserCallback]. On the Javascript side, the channel ID and
+//     user public key are returned as Uint8Array.
+//   - args[5] - ID of [ChannelDbCipher] object in tracker (int). Create this
 //     object with [NewChannelsDatabaseCipher] and get its id with
 //     [ChannelDbCipher.GetID].
 //
@@ -440,15 +484,17 @@ func LoadChannelsManagerWithIndexedDb(_ js.Value, args []js.Value) any {
 	cmixID := args[0].Int()
 	storageTag := args[1].String()
 	messageReceivedCB := args[2]
-	cipherID := args[3].Int()
+	deletedMessageCB := args[3]
+	mutedUserCB := args[4]
+	cipherID := args[5].Int()
 
 	cipher, err := bindings.GetChannelDbCipherTrackerFromID(cipherID)
 	if err != nil {
 		utils.Throw(utils.TypeError, err)
 	}
 
-	return loadChannelsManagerWithIndexedDb(
-		cmixID, storageTag, messageReceivedCB, cipher)
+	return loadChannelsManagerWithIndexedDb(cmixID, storageTag,
+		messageReceivedCB, deletedMessageCB, mutedUserCB, cipher)
 }
 
 // LoadChannelsManagerWithIndexedDbUnsafe loads an existing [ChannelsManager]
@@ -466,13 +512,22 @@ func LoadChannelsManagerWithIndexedDb(_ js.Value, args []js.Value) any {
 //     using [Cmix.GetID].
 //   - args[1] - The storage tag associated with the previously created channel
 //     manager and retrieved with [ChannelsManager.GetStorageTag] (string).
-//   - args[2] - Function that takes in the same parameters as
-//     [indexedDb.MessageReceivedCallback]. On the Javascript side, the UUID is
-//     returned as an int and the channelID as a Uint8Array. The row in the
-//     database that was updated can be found using the UUID. The channel ID is
-//     provided so that the recipient can filter if they want to the processes
-//     the update now or not. An "update" bool is present which tells you if
-//     the row is new or if it is an edited old row
+//   - args[2] - The received message callback, which is called everytime a
+//     message is added or changed in the database. It is a function that takes
+//     in the same parameters as [indexedDb.MessageReceivedCallback]. On the
+//     Javascript side, the UUID is returned as an int and the channelID as a
+//     Uint8Array. The row in the database that was updated can be found using
+//     the UUID. The channel ID is provided so that the recipient can filter if
+//     they want to the processes the update now or not. An "update" bool is
+//     present which tells you if the row is new or if it is an edited old row.
+//   - args[3] - The deleted message callback, which is called everytime a
+//     message is deleted from the database. It is a function that takes in the
+//     same parameters as [indexedDb.DeletedMessageCallback]. On the Javascript
+//     side, the message ID is returned as a Uint8Array.
+//   - args[4] - The muted user callback, which is called everytime a user is
+//     muted or unmuted. It is a function that takes in the same parameters as
+//     [indexedDb.MutedUserCallback]. On the Javascript side, the channel ID and
+//     user public key are returned as Uint8Array.
 //
 // Returns a promise:
 //   - Resolves to a Javascript representation of the [ChannelsManager] object.
@@ -481,18 +536,32 @@ func LoadChannelsManagerWithIndexedDbUnsafe(_ js.Value, args []js.Value) any {
 	cmixID := args[0].Int()
 	storageTag := args[1].String()
 	messageReceivedCB := args[2]
+	deletedMessageCB := args[3]
+	mutedUserCB := args[4]
 
-	return loadChannelsManagerWithIndexedDb(
-		cmixID, storageTag, messageReceivedCB, nil)
+	return loadChannelsManagerWithIndexedDb(cmixID, storageTag,
+		messageReceivedCB, deletedMessageCB, mutedUserCB, nil)
 }
 
 func loadChannelsManagerWithIndexedDb(cmixID int, storageTag string,
-	cb js.Value, cipher *bindings.ChannelDbCipher) any {
-	messageReceivedCB := func(uuid uint64, channelID *id.ID, updated bool) {
-		cb.Invoke(uuid, utils.CopyBytesToJS(channelID.Marshal()), updated)
+	messageReceivedCB, deletedMessageCB, mutedUserCB js.Value,
+	cipher *bindings.ChannelDbCipher) any {
+
+	messageReceived := func(uuid uint64, channelID *id.ID, update bool) {
+		messageReceivedCB.Invoke(uuid, utils.CopyBytesToJS(channelID.Marshal()), update)
 	}
 
-	model := channelsDb.NewWASMEventModelBuilder(cipher, messageReceivedCB)
+	deletedMessage := func(messageID message.ID) {
+		deletedMessageCB.Invoke(utils.CopyBytesToJS(messageID.Marshal()))
+	}
+
+	mutedUser := func(channelID *id.ID, pubKey ed25519.PublicKey, unmute bool) {
+		mutedUserCB.Invoke(utils.CopyBytesToJS(channelID.Marshal()),
+			utils.CopyBytesToJS(pubKey), unmute)
+	}
+
+	model := channelsDb.NewWASMEventModelBuilder(
+		cipher, messageReceived, deletedMessage, mutedUser)
 
 	promiseFn := func(resolve, reject func(args ...any) js.Value) {
 		cm, err := bindings.LoadChannelsManagerGoEventModel(
@@ -870,14 +939,14 @@ func ValidForever(js.Value, []js.Value) any {
 func (cm *ChannelsManager) SendGeneric(_ js.Value, args []js.Value) any {
 	marshalledChanId := utils.CopyBytesToGo(args[0])
 	messageType := args[1].Int()
-	message := utils.CopyBytesToGo(args[2])
+	msg := utils.CopyBytesToGo(args[2])
 	leaseTimeMS := int64(args[3].Int())
 	tracked := args[4].Bool()
 	cmixParamsJSON := utils.CopyBytesToGo(args[5])
 
 	promiseFn := func(resolve, reject func(args ...any) js.Value) {
 		sendReport, err := cm.api.SendGeneric(marshalledChanId, messageType,
-			message, leaseTimeMS, tracked, cmixParamsJSON)
+			msg, leaseTimeMS, tracked, cmixParamsJSON)
 		if err != nil {
 			reject(utils.JsTrace(err))
 		} else {
@@ -913,13 +982,13 @@ func (cm *ChannelsManager) SendGeneric(_ js.Value, args []js.Value) any {
 //   - Rejected with an error if sending fails.
 func (cm *ChannelsManager) SendMessage(_ js.Value, args []js.Value) any {
 	marshalledChanId := utils.CopyBytesToGo(args[0])
-	message := args[1].String()
+	msg := args[1].String()
 	leaseTimeMS := int64(args[2].Int())
 	cmixParamsJSON := utils.CopyBytesToGo(args[3])
 
 	promiseFn := func(resolve, reject func(args ...any) js.Value) {
 		sendReport, err := cm.api.SendMessage(
-			marshalledChanId, message, leaseTimeMS, cmixParamsJSON)
+			marshalledChanId, msg, leaseTimeMS, cmixParamsJSON)
 		if err != nil {
 			reject(utils.JsTrace(err))
 		} else {
@@ -962,13 +1031,13 @@ func (cm *ChannelsManager) SendMessage(_ js.Value, args []js.Value) any {
 //   - Rejected with an error if sending fails.
 func (cm *ChannelsManager) SendReply(_ js.Value, args []js.Value) any {
 	marshalledChanId := utils.CopyBytesToGo(args[0])
-	message := args[1].String()
+	msg := args[1].String()
 	messageToReactTo := utils.CopyBytesToGo(args[2])
 	leaseTimeMS := int64(args[3].Int())
 	cmixParamsJSON := utils.CopyBytesToGo(args[4])
 
 	promiseFn := func(resolve, reject func(args ...any) js.Value) {
-		sendReport, err := cm.api.SendReply(marshalledChanId, message,
+		sendReport, err := cm.api.SendReply(marshalledChanId, msg,
 			messageToReactTo, leaseTimeMS, cmixParamsJSON)
 		if err != nil {
 			reject(utils.JsTrace(err))
@@ -1059,14 +1128,14 @@ func (cm *ChannelsManager) SendReaction(_ js.Value, args []js.Value) any {
 func (cm *ChannelsManager) SendAdminGeneric(_ js.Value, args []js.Value) any {
 	marshalledChanId := utils.CopyBytesToGo(args[0])
 	messageType := args[1].Int()
-	message := utils.CopyBytesToGo(args[2])
+	msg := utils.CopyBytesToGo(args[2])
 	leaseTimeMS := int64(args[3].Int())
 	tracked := args[4].Bool()
 	cmixParamsJSON := utils.CopyBytesToGo(args[5])
 
 	promiseFn := func(resolve, reject func(args ...any) js.Value) {
 		sendReport, err := cm.api.SendAdminGeneric(marshalledChanId,
-			messageType, message, leaseTimeMS, tracked, cmixParamsJSON)
+			messageType, msg, leaseTimeMS, tracked, cmixParamsJSON)
 		if err != nil {
 			reject(utils.JsTrace(err))
 		} else {
@@ -1615,6 +1684,7 @@ func (emb *eventModelBuilder) Build(path string) bindings.EventModel {
 		updateFromMessageID: utils.WrapCB(emJs, "UpdateFromMessageID"),
 		getMessage:          utils.WrapCB(emJs, "GetMessage"),
 		deleteMessage:       utils.WrapCB(emJs, "DeleteMessage"),
+		muteUser:            utils.WrapCB(emJs, "MuteUser"),
 	}
 }
 
@@ -1630,6 +1700,7 @@ type eventModel struct {
 	updateFromMessageID func(args ...any) js.Value
 	getMessage          func(args ...any) js.Value
 	deleteMessage       func(args ...any) js.Value
+	muteUser            func(args ...any) js.Value
 }
 
 // JoinChannel is called whenever a channel is joined locally.
@@ -1839,7 +1910,7 @@ func (em *eventModel) GetMessage(messageID []byte) ([]byte, error) {
 // the database.
 //
 // Parameters:
-//  - messageID - The bytes of the [channel.MessageID] of the message.
+//   - messageID - The bytes of the [channel.MessageID] of the message.
 func (em *eventModel) DeleteMessage(messageID []byte) error {
 	err := em.deleteMessage(utils.CopyBytesToJS(messageID))
 	if !err.IsUndefined() {
@@ -1849,6 +1920,17 @@ func (em *eventModel) DeleteMessage(messageID []byte) error {
 	return nil
 }
 
+// MuteUser mutes the given user or unmutes them.
+//
+// Parameters:
+//   - channelID - The bytes of the [id.ID] of the channel the user is being
+//     muted in.
+//   - pubKey - The [ed25519.PublicKey] of the user that is muted or unmuted.
+func (em *eventModel) MuteUser(channelID, pubkey []byte, unmute bool) {
+	em.muteUser(
+		utils.CopyBytesToJS(channelID), utils.CopyBytesToJS(pubkey), unmute)
+}
+
 // MessageAndError contains a message returned by eventModel.GetMessage or any
 // possible error that occurs during lookup. Only one field should be present at
 // a time; if an error occurs, ModelMessage should be empty.