diff --git a/indexedDb/channels/implementation.go b/indexedDb/channels/implementation.go
index 875682329d393031c8b89401d376374c6659e538..bcd957f13863d47e2ce493f6c00b5e5bfd4775b4 100644
--- a/indexedDb/channels/implementation.go
+++ b/indexedDb/channels/implementation.go
@@ -167,7 +167,7 @@ func (w *wasmModel) deleteMsgByChannel(channelID *id.ID) error {
 // user of the API to filter such called by message ID.
 func (w *wasmModel) ReceiveMessage(channelID *id.ID,
 	messageID cryptoChannel.MessageID, nickname, text string,
-	pubKey ed25519.PublicKey, dmToken []byte, codeset uint8,
+	pubKey ed25519.PublicKey, dmToken uint32, codeset uint8,
 	timestamp time.Time, lease time.Duration, round rounds.Round,
 	mType channels.MessageType, status channels.SentStatus) uint64 {
 	textBytes := []byte(text)
@@ -203,7 +203,7 @@ func (w *wasmModel) ReceiveMessage(channelID *id.ID,
 // the initial message. As a result, it may be important to buffer replies.
 func (w *wasmModel) ReceiveReply(channelID *id.ID,
 	messageID cryptoChannel.MessageID, replyTo cryptoChannel.MessageID,
-	nickname, text string, pubKey ed25519.PublicKey, dmToken []byte, codeset uint8,
+	nickname, text string, pubKey ed25519.PublicKey, dmToken uint32, codeset uint8,
 	timestamp time.Time, lease time.Duration, round rounds.Round,
 	mType channels.MessageType, status channels.SentStatus) uint64 {
 	textBytes := []byte(text)
@@ -239,7 +239,7 @@ func (w *wasmModel) ReceiveReply(channelID *id.ID,
 // the initial message. As a result, it may be important to buffer reactions.
 func (w *wasmModel) ReceiveReaction(channelID *id.ID,
 	messageID cryptoChannel.MessageID, reactionTo cryptoChannel.MessageID,
-	nickname, reaction string, pubKey ed25519.PublicKey, dmToken []byte, codeset uint8,
+	nickname, reaction string, pubKey ed25519.PublicKey, dmToken uint32, codeset uint8,
 	timestamp time.Time, lease time.Duration, round rounds.Round,
 	mType channels.MessageType, status channels.SentStatus) uint64 {
 	textBytes := []byte(reaction)
@@ -332,7 +332,7 @@ func (w *wasmModel) UpdateSentStatus(uuid uint64,
 // autoincrement key by default. If you are trying to overwrite an existing
 // message, then you need to set it manually yourself.
 func buildMessage(channelID, messageID, parentID []byte, nickname string,
-	text []byte, pubKey ed25519.PublicKey, dmToken []byte, codeset uint8, timestamp time.Time,
+	text []byte, pubKey ed25519.PublicKey, dmToken uint32, codeset uint8, timestamp time.Time,
 	lease time.Duration, round id.Round, mType channels.MessageType,
 	status channels.SentStatus) *Message {
 	return &Message{
diff --git a/indexedDb/channels/implementation_test.go b/indexedDb/channels/implementation_test.go
index 27f33649eac4d02514a09b6e2117fb148c6b764e..9f17457013d26125f05d411d29b09afff4cd16c3 100644
--- a/indexedDb/channels/implementation_test.go
+++ b/indexedDb/channels/implementation_test.go
@@ -60,7 +60,7 @@ func TestWasmModel_msgIDLookup(t *testing.T) {
 			}
 
 			testMsg := buildMessage([]byte(testString), testMsgId.Bytes(), nil,
-				testString, []byte(testString), []byte{8, 6, 7, 5}, 0, netTime.Now(),
+				testString, []byte(testString), []byte{8, 6, 7, 5}, 0, 0, netTime.Now(),
 				time.Second, 0, 0, channels.Sent)
 			_, err = eventModel.receiveHelper(testMsg, false)
 			if err != nil {
@@ -100,7 +100,7 @@ func Test_wasmModel_UpdateSentStatus(t *testing.T) {
 
 			// Store a test message
 			testMsg := buildMessage([]byte(testString), testMsgId.Bytes(), nil,
-				testString, []byte(testString), []byte{8, 6, 7, 5}, 0, netTime.Now(),
+				testString, []byte(testString), []byte{8, 6, 7, 5}, 0, 0, netTime.Now(),
 				time.Second, 0, 0, channels.Sent)
 			uuid, err := eventModel.receiveHelper(testMsg, false)
 			if err != nil {
@@ -225,7 +225,7 @@ func Test_wasmModel_UUIDTest(t *testing.T) {
 				copy(msgID[:], testString+fmt.Sprintf("%d", i))
 				rnd := rounds.Round{ID: id.Round(42)}
 				uuid := eventModel.ReceiveMessage(channelID, msgID, "test",
-					testString+fmt.Sprintf("%d", i), []byte{8, 6, 7, 5}, 0,
+					testString+fmt.Sprintf("%d", i), []byte{8, 6, 7, 5}, 0, 0,
 					netTime.Now(), time.Hour, rnd, 0, channels.Sent)
 				uuids[i] = uuid
 			}
@@ -270,7 +270,7 @@ func Test_wasmModel_DuplicateReceives(t *testing.T) {
 				channelID := id.NewIdFromBytes([]byte(testString), t)
 				rnd := rounds.Round{ID: id.Round(42)}
 				uuid := eventModel.ReceiveMessage(channelID, msgID, "test",
-					testString+fmt.Sprintf("%d", i), []byte{8, 6, 7, 5}, 0,
+					testString+fmt.Sprintf("%d", i), []byte{8, 6, 7, 5}, 0, 0,
 					netTime.Now(), time.Hour, rnd, 0, channels.Sent)
 				uuids[i] = uuid
 			}
@@ -326,7 +326,7 @@ func Test_wasmModel_deleteMsgByChannel(t *testing.T) {
 
 				testMsgId := channel.MakeMessageID([]byte(testStr), &id.ID{1})
 				eventModel.ReceiveMessage(thisChannel, testMsgId, testStr, testStr,
-					[]byte{8, 6, 7, 5}, 0, netTime.Now(), time.Second,
+					[]byte{8, 6, 7, 5}, 0, 0, netTime.Now(), time.Second,
 					rounds.Round{ID: id.Round(0)}, 0, channels.Sent)
 			}
 
@@ -400,7 +400,7 @@ func TestWasmModel_receiveHelper_UniqueIndex(t *testing.T) {
 			// First message insert should succeed
 			testMsgId := channel.MakeMessageID([]byte(testString), &id.ID{1})
 			testMsg := buildMessage([]byte(testString), testMsgId.Bytes(), nil,
-				testString, []byte(testString), []byte{8, 6, 7, 5}, 0, netTime.Now(),
+				testString, []byte(testString), []byte{8, 6, 7, 5}, 0, 0, netTime.Now(),
 				time.Second, 0, 0, channels.Sent)
 			_, err = eventModel.receiveHelper(testMsg, false)
 			if err != nil {
@@ -423,7 +423,7 @@ func TestWasmModel_receiveHelper_UniqueIndex(t *testing.T) {
 			// Now insert a message with a different message ID from the first
 			testMsgId2 := channel.MakeMessageID([]byte(testString), &id.ID{2})
 			testMsg = buildMessage([]byte(testString), testMsgId2.Bytes(), nil,
-				testString, []byte(testString), []byte{8, 6, 7, 5}, 0, netTime.Now(),
+				testString, []byte(testString), []byte{8, 6, 7, 5}, 0, 0, netTime.Now(),
 				time.Second, 0, 0, channels.Sent)
 			primaryKey, err := eventModel.receiveHelper(testMsg, false)
 			if err != nil {
diff --git a/indexedDb/channels/model.go b/indexedDb/channels/model.go
index 629c9f43353ade792f024f88aba769c3c89f22c0..078d6bd67f7c43d9e50373b7801f69937cfce2ac 100644
--- a/indexedDb/channels/model.go
+++ b/indexedDb/channels/model.go
@@ -61,7 +61,7 @@ type Message struct {
 
 	// User cryptographic Identity struct -- could be pulled out
 	Pubkey         []byte `json:"pubkey"`
-	DmToken        []byte `json:"dm_token"`
+	DmToken        uint32 `json:"dm_token"`
 	CodesetVersion uint8  `json:"codeset_version"`
 }
 
diff --git a/indexedDb/dm/implementation.go b/indexedDb/dm/implementation.go
new file mode 100644
index 0000000000000000000000000000000000000000..5f2361ac1363714d3f8d899231ab7f746a8ad1b9
--- /dev/null
+++ b/indexedDb/dm/implementation.go
@@ -0,0 +1,382 @@
+////////////////////////////////////////////////////////////////////////////////
+// Copyright © 2022 xx foundation                                             //
+//                                                                            //
+// Use of this source code is governed by a license that can be found in the  //
+// LICENSE file.                                                              //
+////////////////////////////////////////////////////////////////////////////////
+
+//go:build js && wasm
+
+package channelEventModel
+
+import (
+	"crypto/ed25519"
+	"encoding/json"
+	"github.com/pkg/errors"
+	jww "github.com/spf13/jwalterweatherman"
+	"gitlab.com/elixxir/client/v4/cmix/rounds"
+	"gitlab.com/elixxir/client/v4/dm"
+	"gitlab.com/elixxir/xxdk-wasm/indexedDb"
+	"gitlab.com/elixxir/xxdk-wasm/utils"
+	"gitlab.com/xx_network/primitives/id"
+	"strings"
+	"sync"
+	"syscall/js"
+	"time"
+
+	"github.com/hack-pad/go-indexeddb/idb"
+	cryptoChannel "gitlab.com/elixxir/crypto/channel"
+)
+
+// wasmModel implements [dm.Receiver] interface, which uses the channels
+// system passed an object that adheres to in order to get events on the
+// channel.
+type wasmModel struct {
+	db                *idb.Database
+	cipher            cryptoChannel.Cipher
+	receivedMessageCB MessageReceivedCallback
+	updateMux         sync.Mutex
+}
+
+// joinConversation is used for joining new conversations.
+func (w *wasmModel) joinConversation(nickname string,
+	pubKey ed25519.PublicKey, dmToken uint32, codeset uint8) error {
+	parentErr := errors.New("failed to joinConversation")
+
+	// Build object
+	newConvo := Conversation{
+		Pubkey:         pubKey,
+		Nickname:       nickname,
+		Token:          dmToken,
+		CodesetVersion: codeset,
+		Blocked:        false,
+	}
+
+	// Convert to jsObject
+	newConvoJson, err := json.Marshal(&newConvo)
+	if err != nil {
+		return errors.WithMessagef(parentErr,
+			"Unable to marshal Conversation: %+v", err)
+	}
+	convoObj, err := utils.JsonToJS(newConvoJson)
+	if err != nil {
+		return errors.WithMessagef(parentErr,
+			"Unable to marshal Conversation: %+v", err)
+	}
+
+	_, err = indexedDb.Put(w.db, conversationStoreName, convoObj)
+	if err != nil {
+		return errors.WithMessagef(parentErr,
+			"Unable to put Conversation: %+v", err)
+	}
+	return nil
+}
+
+// buildMessage is a private helper that converts typical [dm.Receiver]
+// inputs into a basic Message structure for insertion into storage.
+//
+// NOTE: ID is not set inside this function because we want to use the
+// autoincrement key by default. If you are trying to overwrite an existing
+// message, then you need to set it manually yourself.
+func buildMessage(messageID, parentID []byte, text []byte,
+	pubKey ed25519.PublicKey, timestamp time.Time, round id.Round,
+	mType dm.MessageType, status dm.Status) *Message {
+	return &Message{
+		MessageID:          messageID,
+		ConversationPubKey: pubKey,
+		ParentMessageID:    parentID,
+		Timestamp:          timestamp,
+		Status:             uint8(status),
+		Text:               text,
+		Type:               uint16(mType),
+		Round:              uint64(round),
+	}
+}
+
+func (w *wasmModel) Receive(messageID dm.MessageID, nickname string, text []byte,
+	pubKey ed25519.PublicKey, dmToken uint32, codeset uint8, timestamp time.Time,
+	round rounds.Round, mType dm.MessageType, status dm.Status) uint64 {
+	parentErr := errors.New("failed to Receive")
+	var err error
+
+	// If there is no extant Conversation, create one.
+	if _, err := indexedDb.Get(w.db, conversationStoreName, utils.CopyBytesToJS(pubKey)); err != nil {
+		if strings.Contains(err.Error(), indexedDb.ErrDoesNotExist) {
+			err = w.joinConversation(nickname, pubKey, dmToken, codeset)
+			if err != nil {
+				jww.ERROR.Printf("%+v", err)
+			}
+		} else {
+			jww.ERROR.Printf("%+v", errors.WithMessagef(parentErr,
+				"Unable to get Conversation: %+v", err))
+		}
+		return 0
+	} else {
+		jww.DEBUG.Printf("Conversation with %s already joined", nickname)
+	}
+
+	// Handle encryption, if it is present
+	if w.cipher != nil {
+		text, err = w.cipher.Encrypt(text)
+		if err != nil {
+			jww.ERROR.Printf("Failed to encrypt Message: %+v", err)
+			return 0
+		}
+	}
+
+	msgToInsert := buildMessage(messageID.Bytes(), nil, text,
+		pubKey, timestamp, round.ID, mType, status)
+	uuid, err := w.receiveHelper(msgToInsert, false)
+	if err != nil {
+		jww.ERROR.Printf("Failed to receive Message: %+v", err)
+	}
+
+	go w.receivedMessageCB(uuid, pubKey, false)
+	return uuid
+}
+
+func (w *wasmModel) ReceiveText(messageID dm.MessageID, nickname, text string,
+	pubKey ed25519.PublicKey, dmToken uint32, codeset uint8, timestamp time.Time,
+	round rounds.Round, status dm.Status) uint64 {
+	parentErr := errors.New("failed to ReceiveText")
+	var err error
+
+	// If there is no extant Conversation, create one.
+	if _, err := indexedDb.Get(w.db, conversationStoreName, utils.CopyBytesToJS(pubKey)); err != nil {
+		if strings.Contains(err.Error(), indexedDb.ErrDoesNotExist) {
+			err = w.joinConversation(nickname, pubKey, dmToken, codeset)
+			if err != nil {
+				jww.ERROR.Printf("%+v", err)
+			}
+		} else {
+			jww.ERROR.Printf("%+v", errors.WithMessagef(parentErr,
+				"Unable to get Conversation: %+v", err))
+		}
+		return 0
+	} else {
+		jww.DEBUG.Printf("Conversation with %s already joined", nickname)
+	}
+
+	// Handle encryption, if it is present
+	textBytes := []byte(text)
+	if w.cipher != nil {
+		textBytes, err = w.cipher.Encrypt(textBytes)
+		if err != nil {
+			jww.ERROR.Printf("Failed to encrypt Message: %+v", err)
+			return 0
+		}
+	}
+
+	msgToInsert := buildMessage(messageID.Bytes(), nil, textBytes,
+		pubKey, timestamp, round.ID, dm.TextType, status)
+
+	uuid, err := w.receiveHelper(msgToInsert, false)
+	if err != nil {
+		jww.ERROR.Printf("Failed to receive Message: %+v", err)
+	}
+
+	go w.receivedMessageCB(uuid, pubKey, false)
+	return uuid
+}
+
+func (w *wasmModel) ReceiveReply(messageID dm.MessageID, reactionTo dm.MessageID,
+	nickname, text string, pubKey ed25519.PublicKey, dmToken uint32, codeset uint8,
+	timestamp time.Time, round rounds.Round, status dm.Status) uint64 {
+	parentErr := errors.New("failed to ReceiveReply")
+	var err error
+
+	// If there is no extant Conversation, create one.
+	if _, err := indexedDb.Get(w.db, conversationStoreName, utils.CopyBytesToJS(pubKey)); err != nil {
+		if strings.Contains(err.Error(), indexedDb.ErrDoesNotExist) {
+			err = w.joinConversation(nickname, pubKey, dmToken, codeset)
+			if err != nil {
+				jww.ERROR.Printf("%+v", err)
+			}
+		} else {
+			jww.ERROR.Printf("%+v", errors.WithMessagef(parentErr,
+				"Unable to get Conversation: %+v", err))
+		}
+		return 0
+	} else {
+		jww.DEBUG.Printf("Conversation with %s already joined", nickname)
+	}
+
+	// Handle encryption, if it is present
+	textBytes := []byte(text)
+	if w.cipher != nil {
+		textBytes, err = w.cipher.Encrypt(textBytes)
+		if err != nil {
+			jww.ERROR.Printf("Failed to encrypt Message: %+v", err)
+			return 0
+		}
+	}
+
+	msgToInsert := buildMessage(messageID.Bytes(), reactionTo.Marshal(), textBytes,
+		pubKey, timestamp, round.ID, dm.TextType, status)
+
+	uuid, err := w.receiveHelper(msgToInsert, false)
+	if err != nil {
+		jww.ERROR.Printf("Failed to receive Message: %+v", err)
+	}
+
+	go w.receivedMessageCB(uuid, pubKey, false)
+	return uuid
+}
+
+func (w *wasmModel) ReceiveReaction(messageID dm.MessageID, reactionTo dm.MessageID,
+	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")
+	var err error
+
+	// If there is no extant Conversation, create one.
+	if _, err := indexedDb.Get(w.db, conversationStoreName, utils.CopyBytesToJS(pubKey)); err != nil {
+		if strings.Contains(err.Error(), indexedDb.ErrDoesNotExist) {
+			err = w.joinConversation(nickname, pubKey, dmToken, codeset)
+			if err != nil {
+				jww.ERROR.Printf("%+v", err)
+			}
+		} else {
+			jww.ERROR.Printf("%+v", errors.WithMessagef(parentErr,
+				"Unable to get Conversation: %+v", err))
+		}
+		return 0
+	} else {
+		jww.DEBUG.Printf("Conversation with %s already joined", nickname)
+	}
+
+	// Handle encryption, if it is present
+	textBytes := []byte(reaction)
+	if w.cipher != nil {
+		textBytes, err = w.cipher.Encrypt(textBytes)
+		if err != nil {
+			jww.ERROR.Printf("Failed to encrypt Message: %+v", err)
+			return 0
+		}
+	}
+
+	msgToInsert := buildMessage(messageID.Bytes(), nil, textBytes,
+		pubKey, timestamp, round.ID, dm.ReactionType, status)
+
+	uuid, err := w.receiveHelper(msgToInsert, false)
+	if err != nil {
+		jww.ERROR.Printf("Failed to receive Message: %+v", err)
+	}
+
+	go w.receivedMessageCB(uuid, pubKey, false)
+	return uuid
+}
+
+func (w *wasmModel) UpdateSentStatus(uuid uint64,
+	messageID dm.MessageID, timestamp time.Time, round rounds.Round,
+	status dm.Status) {
+	parentErr := errors.New("failed to UpdateSentStatus")
+
+	// FIXME: this is a bit of race condition without the mux.
+	//        This should be done via the transactions (i.e., make a
+	//        special version of receiveHelper)
+	w.updateMux.Lock()
+	defer w.updateMux.Unlock()
+
+	// Convert messageID to the key generated by json.Marshal
+	key := js.ValueOf(uuid)
+
+	// Use the key to get the existing Message
+	currentMsg, err := indexedDb.Get(w.db, messageStoreName, key)
+	if err != nil {
+		jww.ERROR.Printf("%+v", errors.WithMessagef(parentErr,
+			"Unable to get message: %+v", err))
+		return
+	}
+
+	// Extract the existing Message and update the Status
+	newMessage := &Message{}
+	err = json.Unmarshal([]byte(utils.JsToJson(currentMsg)), newMessage)
+	if err != nil {
+		jww.ERROR.Printf("%+v", errors.WithMessagef(parentErr,
+			"Could not JSON unmarshal message: %+v", err))
+		return
+	}
+
+	newMessage.Status = uint8(status)
+	if !messageID.Equals(dm.MessageID{}) {
+		newMessage.MessageID = messageID.Bytes()
+	}
+
+	if round.ID != 0 {
+		newMessage.Round = uint64(round.ID)
+	}
+
+	if !timestamp.Equal(time.Time{}) {
+		newMessage.Timestamp = timestamp
+	}
+
+	// Store the updated Message
+	_, err = w.receiveHelper(newMessage, true)
+	if err != nil {
+		jww.ERROR.Printf("%+v", errors.Wrap(parentErr, err.Error()))
+	}
+	go w.receivedMessageCB(uuid, newMessage.ConversationPubKey, true)
+}
+
+// receiveHelper is a private helper for receiving any sort of message.
+func (w *wasmModel) receiveHelper(newMessage *Message, isUpdate bool) (uint64,
+	error) {
+	// Convert to jsObject
+	newMessageJson, err := json.Marshal(newMessage)
+	if err != nil {
+		return 0, errors.Errorf("Unable to marshal Message: %+v", err)
+	}
+	messageObj, err := utils.JsonToJS(newMessageJson)
+	if err != nil {
+		return 0, errors.Errorf("Unable to marshal Message: %+v", err)
+	}
+
+	// Unset the primaryKey for inserts so that it can be auto-populated and
+	// incremented
+	if !isUpdate {
+		messageObj.Delete("id")
+	}
+
+	// Store message to database
+	addReq, err := indexedDb.Put(w.db, messageStoreName, messageObj)
+	if err != nil {
+		return 0, errors.Errorf("Unable to put Message: %+v", err)
+	}
+	res, err := addReq.Result()
+	if err != nil {
+		return 0, errors.Errorf("Unable to get Message result: %+v", err)
+	}
+
+	// NOTE: Sometimes the insert fails to return an error but hits a duplicate
+	//  insert, so this fallthrough returns the UUID entry in that case.
+	if res.IsUndefined() {
+		msgID := cryptoChannel.MessageID{}
+		copy(msgID[:], newMessage.MessageID)
+		uuid, errLookup := w.msgIDLookup(msgID)
+		if uuid != 0 && errLookup == nil {
+			return uuid, nil
+		}
+		return 0, errors.Errorf("uuid lookup failure: %+v", err)
+	}
+	uuid := uint64(res.Int())
+	jww.DEBUG.Printf("Successfully stored message %d", uuid)
+
+	return uuid, nil
+}
+
+// msgIDLookup gets the UUID of the Message with the given messageID.
+func (w *wasmModel) msgIDLookup(messageID cryptoChannel.MessageID) (uint64,
+	error) {
+	resultObj, err := indexedDb.GetIndex(w.db, messageStoreName,
+		messageStoreMessageIndex, utils.CopyBytesToJS(messageID.Marshal()))
+	if err != nil {
+		return 0, err
+	}
+
+	uuid := uint64(0)
+	if !resultObj.IsUndefined() {
+		uuid = uint64(resultObj.Get("id").Int())
+	}
+	return uuid, nil
+}
diff --git a/indexedDb/dm/init.go b/indexedDb/dm/init.go
new file mode 100644
index 0000000000000000000000000000000000000000..d2c3ba1dbb2d6306a39ffc8f5f0b4e6f0159dc73
--- /dev/null
+++ b/indexedDb/dm/init.go
@@ -0,0 +1,181 @@
+////////////////////////////////////////////////////////////////////////////////
+// Copyright © 2022 xx foundation                                             //
+//                                                                            //
+// Use of this source code is governed by a license that can be found in the  //
+// LICENSE file.                                                              //
+////////////////////////////////////////////////////////////////////////////////
+
+//go:build js && wasm
+
+package channelEventModel
+
+import (
+	"crypto/ed25519"
+	"github.com/hack-pad/go-indexeddb/idb"
+	"github.com/pkg/errors"
+	jww "github.com/spf13/jwalterweatherman"
+	"gitlab.com/elixxir/client/v4/dm"
+	cryptoChannel "gitlab.com/elixxir/crypto/channel"
+	"gitlab.com/elixxir/xxdk-wasm/indexedDb"
+	"gitlab.com/elixxir/xxdk-wasm/storage"
+	"syscall/js"
+)
+
+const (
+	// databaseSuffix is the suffix to be appended to the name of
+	// the database.
+	databaseSuffix = "_speakeasy_dm"
+
+	// currentVersion is the current version of the IndexDb
+	// runtime. Used for migration purposes.
+	currentVersion uint = 1
+)
+
+// 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)
+
+// 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) (dm.Receiver, error) {
+	databaseName := path + databaseSuffix
+	return newWASMModel(databaseName, encryption, cb)
+}
+
+// newWASMModel creates the given [idb.Database] and returns a wasmModel.
+func newWASMModel(databaseName string, encryption cryptoChannel.Cipher,
+	cb MessageReceivedCallback) (*wasmModel, error) {
+	// Attempt to open database object
+	ctx, cancel := indexedDb.NewContext()
+	defer cancel()
+	openRequest, err := idb.Global().Open(ctx, databaseName, currentVersion,
+		func(db *idb.Database, oldVersion, newVersion uint) error {
+			if oldVersion == newVersion {
+				jww.INFO.Printf("IndexDb version is current: v%d",
+					newVersion)
+				return nil
+			}
+
+			jww.INFO.Printf("IndexDb upgrade required: v%d -> v%d",
+				oldVersion, newVersion)
+
+			if oldVersion == 0 && newVersion >= 1 {
+				err := v1Upgrade(db)
+				if err != nil {
+					return err
+				}
+				oldVersion = 1
+			}
+
+			// if oldVersion == 1 && newVersion >= 2 { v2Upgrade(), oldVersion = 2 }
+			return nil
+		})
+	if err != nil {
+		return nil, err
+	}
+
+	// Wait for database open to finish
+	db, err := openRequest.Await(ctx)
+	if err != nil {
+		return nil, err
+	}
+
+	// Save the encryption status to storage
+	encryptionStatus := encryption != nil
+	loadedEncryptionStatus, err := storage.StoreIndexedDbEncryptionStatus(
+		databaseName, encryptionStatus)
+	if err != nil {
+		return nil, err
+	}
+
+	// Verify encryption status does not change
+	if encryptionStatus != loadedEncryptionStatus {
+		return nil, errors.New(
+			"Cannot load database with different encryption status.")
+	} else if !encryptionStatus {
+		jww.WARN.Printf("IndexedDb encryption disabled!")
+	}
+
+	// Attempt to ensure the database has been properly initialized
+	openRequest, err = idb.Global().Open(ctx, databaseName, currentVersion,
+		func(db *idb.Database, oldVersion, newVersion uint) error {
+			return nil
+		})
+	if err != nil {
+		return nil, err
+	}
+	// Wait for database open to finish
+	db, err = openRequest.Await(ctx)
+	if err != nil {
+		return nil, err
+	}
+	wrapper := &wasmModel{db: db, receivedMessageCB: cb, cipher: encryption}
+
+	return wrapper, nil
+}
+
+// v1Upgrade performs the v0 -> v1 database upgrade.
+//
+// This can never be changed without permanently breaking backwards
+// compatibility.
+func v1Upgrade(db *idb.Database) error {
+	indexOpts := idb.IndexOptions{
+		Unique:     false,
+		MultiEntry: false,
+	}
+
+	// Build Message ObjectStore and Indexes
+	messageStoreOpts := idb.ObjectStoreOptions{
+		KeyPath:       js.ValueOf(msgPkeyName),
+		AutoIncrement: true,
+	}
+	messageStore, err := db.CreateObjectStore(messageStoreName, messageStoreOpts)
+	if err != nil {
+		return err
+	}
+	_, err = messageStore.CreateIndex(messageStoreMessageIndex,
+		js.ValueOf(messageStoreMessage),
+		idb.IndexOptions{
+			Unique:     true,
+			MultiEntry: false,
+		})
+	if err != nil {
+		return err
+	}
+	_, err = messageStore.CreateIndex(messageStoreConversationIndex,
+		js.ValueOf(messageStoreConversation), indexOpts)
+	if err != nil {
+		return err
+	}
+	_, err = messageStore.CreateIndex(messageStoreParentIndex,
+		js.ValueOf(messageStoreParent), indexOpts)
+	if err != nil {
+		return err
+	}
+	_, err = messageStore.CreateIndex(messageStoreTimestampIndex,
+		js.ValueOf(messageStoreTimestamp), indexOpts)
+	if err != nil {
+		return err
+	}
+
+	// Build Channel ObjectStore
+	conversationStoreOpts := idb.ObjectStoreOptions{
+		KeyPath:       js.ValueOf(convoPkeyName),
+		AutoIncrement: false,
+	}
+	_, err = db.CreateObjectStore(conversationStoreName, conversationStoreOpts)
+	if err != nil {
+		return err
+	}
+
+	// Get the database name and save it to storage
+	if databaseName, err := db.Name(); err != nil {
+		return err
+	} else if err = storage.StoreIndexedDb(databaseName); err != nil {
+		return err
+	}
+
+	return nil
+}
diff --git a/indexedDb/dm/model.go b/indexedDb/dm/model.go
new file mode 100644
index 0000000000000000000000000000000000000000..bb6f34588aa2976c1689b629c6df31219de6b585
--- /dev/null
+++ b/indexedDb/dm/model.go
@@ -0,0 +1,63 @@
+////////////////////////////////////////////////////////////////////////////////
+// Copyright © 2022 xx foundation                                             //
+//                                                                            //
+// Use of this source code is governed by a license that can be found in the  //
+// LICENSE file.                                                              //
+////////////////////////////////////////////////////////////////////////////////
+
+//go:build js && wasm
+
+package channelEventModel
+
+import (
+	"time"
+)
+
+const (
+	// Text representation of primary key value (keyPath).
+	msgPkeyName   = "id"
+	convoPkeyName = "pub_key"
+
+	// Text representation of the names of the various [idb.ObjectStore].
+	messageStoreName      = "messages"
+	conversationStoreName = "conversations"
+
+	// Message index names.
+	messageStoreMessageIndex      = "message_id_index"
+	messageStoreConversationIndex = "conversation_id_index"
+	messageStoreParentIndex       = "parent_message_id_index"
+	messageStoreTimestampIndex    = "timestamp_index"
+
+	// Message keyPath names (must match json struct tags).
+	messageStoreMessage      = "message_id"
+	messageStoreConversation = "conversation_id"
+	messageStoreParent       = "parent_message_id"
+	messageStoreTimestamp    = "timestamp"
+)
+
+// Message defines the IndexedDb representation of a single Message.
+//
+// A Message belongs to one Conversation.
+// A Message may belong to one Message (Parent).
+type Message struct {
+	ID                 uint64    `json:"id"`                   // Matches msgPkeyName
+	MessageID          []byte    `json:"message_id"`           // Index
+	ConversationPubKey []byte    `json:"conversation_pub_key"` // Index
+	ParentMessageID    []byte    `json:"parent_message_id"`    // Index
+	Timestamp          time.Time `json:"timestamp"`            // Index
+	Status             uint8     `json:"status"`
+	Text               []byte    `json:"text"`
+	Type               uint16    `json:"type"`
+	Round              uint64    `json:"round"`
+}
+
+// Conversation defines the IndexedDb representation of a single
+// message exchange between two recipients.
+// A Conversation has many Message.
+type Conversation struct {
+	Pubkey         []byte `json:"pub_key"` // Matches convoPkeyName
+	Nickname       string `json:"nickname"`
+	Token          uint32 `json:"token"`
+	CodesetVersion uint8  `json:"codeset_version"`
+	Blocked        bool   `json:"blocked"`
+}
diff --git a/indexedDb/implementation_test.go b/indexedDb/implementation_test.go
deleted file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..0000000000000000000000000000000000000000
diff --git a/indexedDb/utils.go b/indexedDb/utils.go
index 1aa6ff45805c1c65b9ad27e51a8b0590e7b8201d..a8e7fd8e528a185cef11f65baa351bc602a9a745 100644
--- a/indexedDb/utils.go
+++ b/indexedDb/utils.go
@@ -22,9 +22,13 @@ import (
 	"time"
 )
 
-// dbTimeout is the global timeout for operations with the storage
-// [context.Context].
-const dbTimeout = time.Second
+const (
+	// dbTimeout is the global timeout for operations with the storage
+	// [context.Context].
+	dbTimeout = time.Second
+	// ErrDoesNotExist is an error string for got undefined on Get operations.
+	ErrDoesNotExist = "result is undefined"
+)
 
 // NewContext builds a context for indexedDb operations.
 func NewContext() (context.Context, context.CancelFunc) {
@@ -62,8 +66,8 @@ func Get(db *idb.Database, objectStoreName string, key js.Value) (js.Value, erro
 		return js.Undefined(), errors.WithMessagef(parentErr,
 			"Unable to get from ObjectStore: %+v", err)
 	} else if resultObj.IsUndefined() {
-		return js.Undefined(), errors.WithMessage(parentErr,
-			"Unable to get from ObjectStore: result is undefined")
+		return js.Undefined(), errors.WithMessagef(parentErr,
+			"Unable to get from ObjectStore: %s", ErrDoesNotExist)
 	}
 
 	// Process result into string
@@ -111,8 +115,8 @@ func GetIndex(db *idb.Database, objectStoreName string,
 		return js.Undefined(), errors.WithMessagef(parentErr,
 			"Unable to get from ObjectStore: %+v", err)
 	} else if resultObj.IsUndefined() {
-		return js.Undefined(), errors.WithMessage(parentErr,
-			"Unable to get from ObjectStore: result is undefined")
+		return js.Undefined(), errors.WithMessagef(parentErr,
+			"Unable to get from ObjectStore: %s", ErrDoesNotExist)
 	}
 
 	// Process result into string
diff --git a/wasm/channels.go b/wasm/channels.go
index 447bea74ea42a6674c3859f10a12ef5ab2bf6fce..66dd9e656a156c275d692a7a7bfb7ca2efadaf7c 100644
--- a/wasm/channels.go
+++ b/wasm/channels.go
@@ -1284,7 +1284,7 @@ func (em *eventModel) LeaveChannel(channelID []byte) {
 //   - nickname - The nickname of the sender of the message (string).
 //   - text - The content of the message (string).
 //   - pubKey - The sender's Ed25519 public key (Uint8Array).
-//   - dmToken - The dmToken (Uint8Array).
+//   - dmToken - The dmToken (int32).
 //   - codeset - The codeset version (int).
 //   - timestamp - Time the message was received; represented as nanoseconds
 //     since unix epoch (int).
@@ -1303,7 +1303,7 @@ func (em *eventModel) LeaveChannel(channelID []byte) {
 //   - A non-negative unique UUID for the message that it can be referenced by
 //     later with [eventModel.UpdateSentStatus].
 func (em *eventModel) ReceiveMessage(channelID, messageID []byte, nickname,
-	text string, pubKey []byte, dmToken []byte, codeset int, timestamp, lease, roundId, msgType,
+	text string, pubKey []byte, dmToken int32, codeset int, timestamp, lease, roundId, msgType,
 	status int64) int64 {
 	uuid := em.receiveMessage(utils.CopyBytesToJS(channelID),
 		utils.CopyBytesToJS(messageID), nickname, text,
@@ -1329,7 +1329,7 @@ func (em *eventModel) ReceiveMessage(channelID, messageID []byte, nickname,
 //   - senderUsername - The username of the sender of the message (string).
 //   - text - The content of the message (string).
 //   - pubKey - The sender's Ed25519 public key (Uint8Array).
-//   - dmToken - The dmToken (Uint8Array).
+//   - dmToken - The dmToken (int32).
 //   - codeset - The codeset version (int).
 //   - timestamp - Time the message was received; represented as nanoseconds
 //     since unix epoch (int).
@@ -1348,7 +1348,7 @@ func (em *eventModel) ReceiveMessage(channelID, messageID []byte, nickname,
 //   - A non-negative unique UUID for the message that it can be referenced by
 //     later with [eventModel.UpdateSentStatus].
 func (em *eventModel) ReceiveReply(channelID, messageID, reactionTo []byte,
-	senderUsername, text string, pubKey []byte, dmToken []byte, codeset int, timestamp, lease,
+	senderUsername, text string, pubKey []byte, dmToken int32, codeset int, timestamp, lease,
 	roundId, msgType, status int64) int64 {
 	uuid := em.receiveReply(utils.CopyBytesToJS(channelID),
 		utils.CopyBytesToJS(messageID), utils.CopyBytesToJS(reactionTo),
@@ -1374,7 +1374,7 @@ func (em *eventModel) ReceiveReply(channelID, messageID, reactionTo []byte,
 //   - senderUsername - The username of the sender of the message (string).
 //   - reaction - The contents of the reaction message (string).
 //   - pubKey - The sender's Ed25519 public key (Uint8Array).
-//   - dmToken - The dmToken (Uint8Array).
+//   - dmToken - The dmToken (int32).
 //   - codeset - The codeset version (int).
 //   - timestamp - Time the message was received; represented as nanoseconds
 //     since unix epoch (int).
@@ -1393,7 +1393,7 @@ func (em *eventModel) ReceiveReply(channelID, messageID, reactionTo []byte,
 //   - A non-negative unique UUID for the message that it can be referenced by
 //     later with [eventModel.UpdateSentStatus].
 func (em *eventModel) ReceiveReaction(channelID, messageID, reactionTo []byte,
-	senderUsername, reaction string, pubKey []byte, dmToken []byte, codeset int, timestamp,
+	senderUsername, reaction string, pubKey []byte, dmToken int32, codeset int, timestamp,
 	lease, roundId, msgType, status int64) int64 {
 	uuid := em.receiveReaction(utils.CopyBytesToJS(channelID),
 		utils.CopyBytesToJS(messageID), utils.CopyBytesToJS(reactionTo),