diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index e1a8ef567bb5b16fa78d4bf6b5537c5103d45dc9..b145fc985b6ea48f9ef9ba9d06c0ba082b0f8955 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -25,6 +25,8 @@ stages:
 
 build:
   stage: build
+  except:
+    - tags
   script:
     - go mod vendor -v
     - mkdir -p release
@@ -36,6 +38,8 @@ build:
 
 wasm-test:
   stage: test
+  except:
+    - tags
   script:
     - export PATH=/root/go/bin:$PATH
     - echo > utils/utils_js.s
@@ -44,23 +48,29 @@ wasm-test:
 
 go-test:
   stage: test
+  except:
+    - tags
   script:
     - go mod vendor -v
     - go test ./... -v
 
 version_check:
   stage: version_check
+  except:
+    - tags
   only:
     - master
     - release
   image: $DOCKER_IMAGE
   script:
     - GITTAG=$(git describe --tags)
-    - CODEVERS=$(cat utils/version.go | grep "const SEMVER =" | cut -d ' ' -f4 | tr -d '"')
+    - CODEVERS=$(cat storage/version.go | grep "const SEMVER =" | cut -d ' ' -f4 | tr -d '"')
     - if [[ $GITTAG != $CODEVERS ]]; then echo "VERSION NUMBER BAD $GITTAG != $CODEVER"; exit -1; fi
 
 tag:
   stage: build
+  except:
+    - tags
   image: $DOCKER_IMAGE
   script:
     - git remote add origin_tags git@$GITLAB_SERVER:elixxir/xxdk-wasm.git || true
@@ -76,6 +86,8 @@ tag:
 # master/release, this will fail to pull the latest client, and the docs will not update.
 doc-update:
   stage: doc-update
+  except:
+    - tags
   image: $DOCKER_IMAGE
   script:
     # We use GOPRIVATE blank because not want to directly pull client, we want to use the public cache.
diff --git a/indexedDb/implementation.go b/indexedDb/channels/implementation.go
similarity index 69%
rename from indexedDb/implementation.go
rename to indexedDb/channels/implementation.go
index d819fde24d9d9404e3f23fb20d7376594eb07278..875682329d393031c8b89401d376374c6659e538 100644
--- a/indexedDb/implementation.go
+++ b/indexedDb/channels/implementation.go
@@ -7,13 +7,13 @@
 
 //go:build js && wasm
 
-package indexedDb
+package channels
 
 import (
-	"context"
 	"crypto/ed25519"
 	"encoding/base64"
 	"encoding/json"
+	"gitlab.com/elixxir/xxdk-wasm/indexedDb"
 	"sync"
 	"syscall/js"
 	"time"
@@ -30,10 +30,6 @@ import (
 	"gitlab.com/xx_network/primitives/id"
 )
 
-// dbTimeout is the global timeout for operations with the storage
-// [context.Context].
-const dbTimeout = time.Second
-
 // wasmModel implements [channels.EventModel] interface, which uses the channels
 // system passed an object that adheres to in order to get events on the
 // channel.
@@ -44,11 +40,6 @@ type wasmModel struct {
 	updateMux         sync.Mutex
 }
 
-// newContext builds a context for database operations.
-func newContext() (context.Context, context.CancelFunc) {
-	return context.WithTimeout(context.Background(), dbTimeout)
-}
-
 // JoinChannel is called whenever a channel is joined locally.
 func (w *wasmModel) JoinChannel(channel *cryptoBroadcast.Channel) {
 	parentErr := errors.New("failed to JoinChannel")
@@ -74,38 +65,11 @@ func (w *wasmModel) JoinChannel(channel *cryptoBroadcast.Channel) {
 		return
 	}
 
-	// Prepare the Transaction
-	txn, err := w.db.Transaction(idb.TransactionReadWrite, channelsStoreName)
-	if err != nil {
-		jww.ERROR.Printf("%+v", errors.WithMessagef(parentErr,
-			"Unable to create Transaction: %+v", err))
-		return
-	}
-	store, err := txn.ObjectStore(channelsStoreName)
-	if err != nil {
-		jww.ERROR.Printf("%+v", errors.WithMessagef(parentErr,
-			"Unable to get ObjectStore: %+v", err))
-		return
-	}
-
-	// Perform the operation
-	_, err = store.Put(channelObj)
-	if err != nil {
-		jww.ERROR.Printf("%+v", errors.WithMessagef(parentErr,
-			"Unable to Add Channel: %+v", err))
-		return
-	}
-
-	// Wait for the operation to return
-	ctx, cancel := newContext()
-	err = txn.Await(ctx)
-	cancel()
+	_, err = indexedDb.Put(w.db, channelsStoreName, channelObj)
 	if err != nil {
 		jww.ERROR.Printf("%+v", errors.WithMessagef(parentErr,
-			"Adding Channel failed: %+v", err))
-		return
+			"Unable to put Channel: %+v", err))
 	}
-	jww.DEBUG.Printf("Successfully added channel: %s", channel.ReceptionID)
 }
 
 // LeaveChannel is called whenever a channel is left locally.
@@ -135,7 +99,7 @@ func (w *wasmModel) LeaveChannel(channelID *id.ID) {
 	}
 
 	// Wait for the operation to return
-	ctx, cancel := newContext()
+	ctx, cancel := indexedDb.NewContext()
 	err = txn.Await(ctx)
 	cancel()
 	if err != nil {
@@ -183,7 +147,7 @@ func (w *wasmModel) deleteMsgByChannel(channelID *id.ID) error {
 	if err != nil {
 		return errors.WithMessagef(parentErr, "Unable to open Cursor: %+v", err)
 	}
-	ctx, cancel := newContext()
+	ctx, cancel := indexedDb.NewContext()
 	err = cursorRequest.Iter(ctx,
 		func(cursor *idb.CursorWithValue) error {
 			_, err := cursor.Delete()
@@ -322,17 +286,22 @@ func (w *wasmModel) UpdateSentStatus(uuid uint64,
 	key := js.ValueOf(uuid)
 
 	// Use the key to get the existing Message
-	currentMsg, err := w.get(messageStoreName, key)
+	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(currentMsg), newMessage)
+	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(cryptoChannel.MessageID{}) {
 		newMessage.MessageID = messageID.Bytes()
@@ -405,31 +374,15 @@ func (w *wasmModel) receiveHelper(newMessage *Message, isUpdate bool) (uint64,
 		messageObj.Delete("id")
 	}
 
-	// Prepare the Transaction
-	txn, err := w.db.Transaction(idb.TransactionReadWrite, messageStoreName)
-	if err != nil {
-		return 0, errors.Errorf("Unable to create Transaction: %+v",
-			err)
-	}
-	store, err := txn.ObjectStore(messageStoreName)
-	if err != nil {
-		return 0, errors.Errorf("Unable to get ObjectStore: %+v", err)
-	}
-
-	// Perform the upsert (put) operation
-	addReq, err := store.Put(messageObj)
+	// Store message to database
+	addReq, err := indexedDb.Put(w.db, messageStoreName, messageObj)
 	if err != nil {
-		return 0, errors.Errorf("Unable to upsert Message: %+v", err)
+		return 0, errors.Errorf("Unable to put Message: %+v", err)
 	}
-
-	// Wait for the operation to return
-	ctx, cancel := newContext()
-	err = txn.Await(ctx)
-	cancel()
+	res, err := addReq.Result()
 	if err != nil {
-		return 0, errors.Errorf("Upserting Message failed: %+v", err)
+		return 0, errors.Errorf("Unable to get Message result: %+v", err)
 	}
-	res, err := addReq.Result()
 
 	// NOTE: Sometimes the insert fails to return an error but hits a duplicate
 	//  insert, so this fallthrough returns the UUID entry in that case.
@@ -448,135 +401,19 @@ func (w *wasmModel) receiveHelper(newMessage *Message, isUpdate bool) (uint64,
 	return uuid, nil
 }
 
-// get is a generic private helper for getting values from the given
-// [idb.ObjectStore].
-func (w *wasmModel) get(objectStoreName string, key js.Value) (string, error) {
-	parentErr := errors.Errorf("failed to get %s/%s", objectStoreName, key)
-
-	// Prepare the Transaction
-	txn, err := w.db.Transaction(idb.TransactionReadOnly, objectStoreName)
-	if err != nil {
-		return "", errors.WithMessagef(parentErr,
-			"Unable to create Transaction: %+v", err)
-	}
-	store, err := txn.ObjectStore(objectStoreName)
-	if err != nil {
-		return "", errors.WithMessagef(parentErr,
-			"Unable to get ObjectStore: %+v", err)
-	}
-
-	// Perform the operation
-	getRequest, err := store.Get(key)
-	if err != nil {
-		return "", errors.WithMessagef(parentErr,
-			"Unable to Get from ObjectStore: %+v", err)
-	}
-
-	// Wait for the operation to return
-	ctx, cancel := newContext()
-	resultObj, err := getRequest.Await(ctx)
-	cancel()
-	if err != nil {
-		return "", errors.WithMessagef(parentErr,
-			"Unable to get from ObjectStore: %+v", err)
-	}
-
-	// Process result into string
-	resultStr := utils.JsToJson(resultObj)
-	jww.DEBUG.Printf("Got from %s/%s: %s", objectStoreName, key, resultStr)
-	return resultStr, nil
-}
-
+// msgIDLookup gets the UUID of the Message with the given messageID.
 func (w *wasmModel) msgIDLookup(messageID cryptoChannel.MessageID) (uint64,
 	error) {
-	parentErr := errors.Errorf("failed to get %s/%s", messageStoreName,
-		messageID)
-
-	// Prepare the Transaction
-	txn, err := w.db.Transaction(idb.TransactionReadOnly, messageStoreName)
-	if err != nil {
-		return 0, errors.WithMessagef(parentErr,
-			"Unable to create Transaction: %+v", err)
-	}
-	store, err := txn.ObjectStore(messageStoreName)
-	if err != nil {
-		return 0, errors.WithMessagef(parentErr,
-			"Unable to get ObjectStore: %+v", err)
-	}
-	idx, err := store.Index(messageStoreMessageIndex)
-	if err != nil {
-		return 0, errors.WithMessagef(parentErr,
-			"Unable to get index: %+v", err)
-	}
-
-	msgIDStr := base64.StdEncoding.EncodeToString(messageID.Bytes())
-
-	keyReq, err := idx.Get(js.ValueOf(msgIDStr))
+	msgIDStr := js.ValueOf(base64.StdEncoding.EncodeToString(messageID.Bytes()))
+	resultObj, err := indexedDb.GetIndex(w.db, messageStoreName,
+		messageStoreMessageIndex, msgIDStr)
 	if err != nil {
-		return 0, errors.WithMessagef(parentErr,
-			"Unable to get keyReq: %+v", err)
+		return 0, err
 	}
-	// Wait for the operation to return
-	ctx, cancel := newContext()
-	keyObj, err := keyReq.Await(ctx)
-	cancel()
-	if err != nil {
-		return 0, errors.WithMessagef(parentErr,
-			"Unable to get from ObjectStore: %+v", err)
-	}
-
-	// Process result into string
-	resultStr := utils.JsToJson(keyObj)
-	jww.DEBUG.Printf("Index lookup of %s/%s/%s: %s", messageStoreName,
-		messageStoreMessageIndex, msgIDStr, resultStr)
 
 	uuid := uint64(0)
-	if !keyObj.IsUndefined() {
-		uuid = uint64(keyObj.Get("id").Int())
+	if !resultObj.IsUndefined() {
+		uuid = uint64(resultObj.Get("id").Int())
 	}
 	return uuid, nil
 }
-
-// dump returns the given [idb.ObjectStore] contents to string slice for
-// debugging purposes.
-func (w *wasmModel) dump(objectStoreName string) ([]string, error) {
-	parentErr := errors.Errorf("failed to dump %s", objectStoreName)
-
-	txn, err := w.db.Transaction(idb.TransactionReadOnly, objectStoreName)
-	if err != nil {
-		return nil, errors.WithMessagef(parentErr,
-			"Unable to create Transaction: %+v", err)
-	}
-	store, err := txn.ObjectStore(objectStoreName)
-	if err != nil {
-		return nil, errors.WithMessagef(parentErr,
-			"Unable to get ObjectStore: %+v", err)
-	}
-	cursorRequest, err := store.OpenCursor(idb.CursorNext)
-	if err != nil {
-		return nil, errors.WithMessagef(parentErr,
-			"Unable to open Cursor: %+v", err)
-	}
-
-	// Run the query
-	jww.DEBUG.Printf("%s values:", objectStoreName)
-	results := make([]string, 0)
-	ctx, cancel := newContext()
-	err = cursorRequest.Iter(ctx,
-		func(cursor *idb.CursorWithValue) error {
-			value, err := cursor.Value()
-			if err != nil {
-				return err
-			}
-			valueStr := utils.JsToJson(value)
-			results = append(results, valueStr)
-			jww.DEBUG.Printf("- %v", valueStr)
-			return nil
-		})
-	cancel()
-	if err != nil {
-		return nil, errors.WithMessagef(parentErr,
-			"Unable to dump ObjectStore: %+v", err)
-	}
-	return results, nil
-}
diff --git a/indexedDb/channels/implementation_test.go b/indexedDb/channels/implementation_test.go
new file mode 100644
index 0000000000000000000000000000000000000000..27f33649eac4d02514a09b6e2117fb148c6b764e
--- /dev/null
+++ b/indexedDb/channels/implementation_test.go
@@ -0,0 +1,452 @@
+////////////////////////////////////////////////////////////////////////////////
+// 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 channels
+
+import (
+	"encoding/json"
+	"fmt"
+	"github.com/hack-pad/go-indexeddb/idb"
+	"gitlab.com/elixxir/xxdk-wasm/indexedDb"
+	"gitlab.com/elixxir/xxdk-wasm/storage"
+	"gitlab.com/xx_network/crypto/csprng"
+	"gitlab.com/xx_network/primitives/netTime"
+	"os"
+	"strconv"
+	"testing"
+	"time"
+
+	jww "github.com/spf13/jwalterweatherman"
+	"gitlab.com/elixxir/client/v4/channels"
+	"gitlab.com/elixxir/client/v4/cmix/rounds"
+	cryptoBroadcast "gitlab.com/elixxir/crypto/broadcast"
+	"gitlab.com/elixxir/crypto/channel"
+	cryptoChannel "gitlab.com/elixxir/crypto/channel"
+	"gitlab.com/xx_network/primitives/id"
+)
+
+func TestMain(m *testing.M) {
+	jww.SetStdoutThreshold(jww.LevelDebug)
+	os.Exit(m.Run())
+}
+
+func dummyCallback(uint64, *id.ID, 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())
+	if err != nil {
+		t.Fatalf("Failed to create cipher")
+	}
+	for _, c := range []cryptoChannel.Cipher{nil, cipher} {
+		cs := ""
+		if cipher != nil {
+			cs = "_withCipher"
+		}
+		t.Run(fmt.Sprintf("TestWasmModel_msgIDLookup%s", cs), func(t *testing.T) {
+
+			storage.GetLocalStorage().Clear()
+			testString := "test"
+			testMsgId := channel.MakeMessageID([]byte(testString), &id.ID{1})
+			eventModel, err := newWASMModel(testString, c, dummyCallback)
+			if err != nil {
+				t.Fatalf("%+v", err)
+			}
+
+			testMsg := buildMessage([]byte(testString), testMsgId.Bytes(), nil,
+				testString, []byte(testString), []byte{8, 6, 7, 5}, 0, netTime.Now(),
+				time.Second, 0, 0, channels.Sent)
+			_, err = eventModel.receiveHelper(testMsg, false)
+			if err != nil {
+				t.Fatalf("%+v", err)
+			}
+
+			uuid, err := eventModel.msgIDLookup(testMsgId)
+			if err != nil {
+				t.Fatalf("%+v", err)
+			}
+			if uuid == 0 {
+				t.Fatalf("Expected to get a UUID!")
+			}
+		})
+	}
+}
+
+// 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())
+	if err != nil {
+		t.Fatalf("Failed to create cipher")
+	}
+	for _, c := range []cryptoChannel.Cipher{nil, cipher} {
+		cs := ""
+		if cipher != nil {
+			cs = "_withCipher"
+		}
+		t.Run(fmt.Sprintf("Test_wasmModel_UpdateSentStatus%s", cs), func(t *testing.T) {
+			storage.GetLocalStorage().Clear()
+			testString := "test"
+			testMsgId := channel.MakeMessageID([]byte(testString), &id.ID{1})
+			eventModel, err := newWASMModel(testString, c, dummyCallback)
+			if err != nil {
+				t.Fatalf("%+v", err)
+			}
+
+			// Store a test message
+			testMsg := buildMessage([]byte(testString), testMsgId.Bytes(), nil,
+				testString, []byte(testString), []byte{8, 6, 7, 5}, 0, netTime.Now(),
+				time.Second, 0, 0, channels.Sent)
+			uuid, err := eventModel.receiveHelper(testMsg, false)
+			if err != nil {
+				t.Fatalf("%+v", err)
+			}
+
+			// Ensure one message is stored
+			results, err := indexedDb.Dump(eventModel.db, messageStoreName)
+			if err != nil {
+				t.Fatalf("%+v", err)
+			}
+			if len(results) != 1 {
+				t.Fatalf("Expected 1 message to exist")
+			}
+
+			// Update the sentStatus
+			expectedStatus := channels.Failed
+			eventModel.UpdateSentStatus(uuid, testMsgId, netTime.Now(),
+				rounds.Round{ID: 8675309}, expectedStatus)
+
+			// Check the resulting status
+			results, err = indexedDb.Dump(eventModel.db, messageStoreName)
+			if err != nil {
+				t.Fatalf("%+v", err)
+			}
+			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)
+			}
+			if resultMsg.Status != uint8(expectedStatus) {
+				t.Fatalf("Unexpected Status: %v", resultMsg.Status)
+			}
+
+			// Make sure other fields didn't change
+			if resultMsg.Nickname != testString {
+				t.Fatalf("Unexpected Nickname: %v", resultMsg.Nickname)
+			}
+		})
+	}
+}
+
+// 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())
+	if err != nil {
+		t.Fatalf("Failed to create cipher")
+	}
+	for _, c := range []cryptoChannel.Cipher{nil, cipher} {
+		cs := ""
+		if cipher != nil {
+			cs = "_withCipher"
+		}
+		t.Run(fmt.Sprintf("Test_wasmModel_JoinChannel_LeaveChannel%s", cs), func(t *testing.T) {
+			storage.GetLocalStorage().Clear()
+			eventModel, err := newWASMModel("test", c, dummyCallback)
+			if err != nil {
+				t.Fatalf("%+v", err)
+			}
+
+			testChannel := &cryptoBroadcast.Channel{
+				ReceptionID: id.NewIdFromString("test", id.Generic, t),
+				Name:        "test",
+				Description: "test",
+				Salt:        nil,
+			}
+			testChannel2 := &cryptoBroadcast.Channel{
+				ReceptionID: id.NewIdFromString("test2", id.Generic, t),
+				Name:        "test2",
+				Description: "test2",
+				Salt:        nil,
+			}
+			eventModel.JoinChannel(testChannel)
+			eventModel.JoinChannel(testChannel2)
+			results, err := indexedDb.Dump(eventModel.db, channelsStoreName)
+			if err != nil {
+				t.Fatalf("%+v", err)
+			}
+			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)
+			}
+			if len(results) != 1 {
+				t.Fatalf("Expected 1 channels to exist")
+			}
+		})
+	}
+}
+
+// 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())
+	if err != nil {
+		t.Fatalf("Failed to create cipher")
+	}
+	for _, c := range []cryptoChannel.Cipher{nil, cipher} {
+		cs := ""
+		if cipher != nil {
+			cs = "_withCipher"
+		}
+		t.Run(fmt.Sprintf("Test_wasmModel_UUIDTest%s", cs), func(t *testing.T) {
+			storage.GetLocalStorage().Clear()
+			testString := "testHello"
+			eventModel, err := newWASMModel(testString, c, dummyCallback)
+			if err != nil {
+				t.Fatalf("%+v", err)
+			}
+
+			uuids := make([]uint64, 10)
+
+			for i := 0; i < 10; i++ {
+				// Store a test message
+				channelID := id.NewIdFromBytes([]byte(testString), t)
+				msgID := channel.MessageID{}
+				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,
+					netTime.Now(), time.Hour, rnd, 0, channels.Sent)
+				uuids[i] = uuid
+			}
+
+			for i := 0; i < 10; i++ {
+				for j := i + 1; j < 10; j++ {
+					if uuids[i] == uuids[j] {
+						t.Fatalf("uuid failed: %d[%d] == %d[%d]",
+							uuids[i], i, uuids[j], j)
+					}
+				}
+			}
+		})
+	}
+}
+
+// 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())
+	if err != nil {
+		t.Fatalf("Failed to create cipher")
+	}
+	for _, c := range []cryptoChannel.Cipher{nil, cipher} {
+		cs := ""
+		if cipher != nil {
+			cs = "_withCipher"
+		}
+		t.Run(fmt.Sprintf("Test_wasmModel_DuplicateReceives%s", cs), func(t *testing.T) {
+			storage.GetLocalStorage().Clear()
+			testString := "testHello"
+			eventModel, err := newWASMModel(testString, c, dummyCallback)
+			if err != nil {
+				t.Fatalf("%+v", err)
+			}
+
+			uuids := make([]uint64, 10)
+
+			msgID := channel.MessageID{}
+			copy(msgID[:], testString)
+			for i := 0; i < 10; i++ {
+				// Store a test message
+				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,
+					netTime.Now(), time.Hour, rnd, 0, channels.Sent)
+				uuids[i] = uuid
+			}
+
+			for i := 0; i < 10; i++ {
+				for j := i + 1; j < 10; j++ {
+					if uuids[i] != uuids[j] {
+						t.Fatalf("uuid failed: %d[%d] != %d[%d]",
+							uuids[i], i, uuids[j], j)
+					}
+				}
+			}
+		})
+	}
+
+}
+
+// Happy path: Inserts many messages, deletes some, and checks that the final
+// result is as expected.
+func Test_wasmModel_deleteMsgByChannel(t *testing.T) {
+	cipher, err := cryptoChannel.NewCipher([]byte("testpass"), []byte("testsalt"), 128, csprng.NewSystemRNG())
+	if err != nil {
+		t.Fatalf("Failed to create cipher")
+	}
+	for _, c := range []cryptoChannel.Cipher{nil, cipher} {
+		cs := ""
+		if cipher != nil {
+			cs = "_withCipher"
+		}
+		t.Run(fmt.Sprintf("Test_wasmModel_deleteMsgByChannel%s", cs), func(t *testing.T) {
+			storage.GetLocalStorage().Clear()
+			testString := "test_deleteMsgByChannel"
+			totalMessages := 10
+			expectedMessages := 5
+			eventModel, err := newWASMModel(testString, c, dummyCallback)
+			if err != nil {
+				t.Fatalf("%+v", err)
+			}
+
+			// Create a test channel id
+			deleteChannel := id.NewIdFromString("deleteMe", id.Generic, t)
+			keepChannel := id.NewIdFromString("dontDeleteMe", id.Generic, t)
+
+			// Store some test messages
+			for i := 0; i < totalMessages; i++ {
+				testStr := testString + strconv.Itoa(i)
+
+				// Interleave the channel id to ensure cursor is behaving intelligently
+				thisChannel := deleteChannel
+				if i%2 == 0 {
+					thisChannel = keepChannel
+				}
+
+				testMsgId := channel.MakeMessageID([]byte(testStr), &id.ID{1})
+				eventModel.ReceiveMessage(thisChannel, testMsgId, testStr, testStr,
+					[]byte{8, 6, 7, 5}, 0, netTime.Now(), time.Second,
+					rounds.Round{ID: id.Round(0)}, 0, channels.Sent)
+			}
+
+			// Check pre-results
+			result, err := indexedDb.Dump(eventModel.db, messageStoreName)
+			if err != nil {
+				t.Fatalf("%+v", err)
+			}
+			if len(result) != totalMessages {
+				t.Errorf("Expected %d messages, got %d", totalMessages, len(result))
+			}
+
+			// Do delete
+			err = eventModel.deleteMsgByChannel(deleteChannel)
+			if err != nil {
+				t.Error(err)
+			}
+
+			// Check final results
+			result, err = indexedDb.Dump(eventModel.db, messageStoreName)
+			if err != nil {
+				t.Fatalf("%+v", err)
+			}
+			if len(result) != expectedMessages {
+				t.Errorf("Expected %d messages, got %d", expectedMessages, len(result))
+			}
+		})
+	}
+}
+
+// This test is designed to prove the behavior of unique indexes.
+// 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())
+	if err != nil {
+		t.Fatalf("Failed to create cipher")
+	}
+	for i, c := range []cryptoChannel.Cipher{nil, cipher} {
+		cs := ""
+		if cipher != nil {
+			cs = "_withCipher"
+		}
+		t.Run(fmt.Sprintf("TestWasmModel_receiveHelper_UniqueIndex%s", 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)
+			}
+
+			// Ensure index is unique
+			txn, err := eventModel.db.Transaction(
+				idb.TransactionReadOnly, messageStoreName)
+			if err != nil {
+				t.Fatal(err)
+			}
+			store, err := txn.ObjectStore(messageStoreName)
+			if err != nil {
+				t.Fatal(err)
+			}
+			idx, err := store.Index(messageStoreMessageIndex)
+			if err != nil {
+				t.Fatal(err)
+			}
+			if isUnique, err2 := idx.Unique(); !isUnique {
+				t.Fatalf("Index is not unique!")
+			} else if err2 != nil {
+				t.Fatal(err2)
+			}
+
+			// 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(),
+				time.Second, 0, 0, channels.Sent)
+			_, err = eventModel.receiveHelper(testMsg, false)
+			if err != nil {
+				t.Fatal(err)
+			}
+
+			// The duplicate entry won't fail, but it just silently shouldn't happen
+			_, err = eventModel.receiveHelper(testMsg, false)
+			if err != nil {
+				t.Fatalf("%+v", err)
+			}
+			results, err := indexedDb.Dump(eventModel.db, messageStoreName)
+			if err != nil {
+				t.Fatalf("%+v", err)
+			}
+			if len(results) != 1 {
+				t.Fatalf("Expected only a single message, got %d", len(results))
+			}
+
+			// 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(),
+				time.Second, 0, 0, channels.Sent)
+			primaryKey, err := eventModel.receiveHelper(testMsg, false)
+			if err != nil {
+				t.Fatal(err)
+			}
+
+			// Except this time, we update the second entry to have the same
+			// message ID as the first
+			testMsg.ID = primaryKey
+			testMsg.MessageID = testMsgId.Bytes()
+			_, err = eventModel.receiveHelper(testMsg, true)
+			if err != nil {
+				t.Fatal(err)
+			}
+
+			// The update to duplicate message ID won't fail,
+			// but it just silently shouldn't happen
+			results, err = indexedDb.Dump(eventModel.db, messageStoreName)
+			if err != nil {
+				t.Fatalf("%+v", err)
+			}
+			// TODO: Convert JSON to Message, ensure Message ID fields differ
+
+		})
+	}
+}
diff --git a/indexedDb/init.go b/indexedDb/channels/init.go
similarity index 96%
rename from indexedDb/init.go
rename to indexedDb/channels/init.go
index f3f4503dec9284896ce6f66d2db44209640a46ad..12d6bb8840f3d75c8778b871ac883a84ad008ada 100644
--- a/indexedDb/init.go
+++ b/indexedDb/channels/init.go
@@ -7,7 +7,7 @@
 
 //go:build js && wasm
 
-package indexedDb
+package channels
 
 import (
 	"github.com/hack-pad/go-indexeddb/idb"
@@ -15,6 +15,7 @@ import (
 	jww "github.com/spf13/jwalterweatherman"
 	"gitlab.com/elixxir/client/v4/channels"
 	cryptoChannel "gitlab.com/elixxir/crypto/channel"
+	"gitlab.com/elixxir/xxdk-wasm/indexedDb"
 	"gitlab.com/elixxir/xxdk-wasm/storage"
 	"gitlab.com/xx_network/primitives/id"
 	"syscall/js"
@@ -58,7 +59,7 @@ func NewWASMEventModel(path string, encryption cryptoChannel.Cipher,
 func newWASMModel(databaseName string, encryption cryptoChannel.Cipher,
 	cb MessageReceivedCallback) (*wasmModel, error) {
 	// Attempt to open database object
-	ctx, cancel := newContext()
+	ctx, cancel := indexedDb.NewContext()
 	defer cancel()
 	openRequest, err := idb.Global().Open(ctx, databaseName, currentVersion,
 		func(db *idb.Database, oldVersion, newVersion uint) error {
@@ -109,9 +110,6 @@ func newWASMModel(databaseName string, encryption cryptoChannel.Cipher,
 	}
 
 	// Attempt to ensure the database has been properly initialized
-	if err != nil {
-		return nil, err
-	}
 	openRequest, err = idb.Global().Open(ctx, databaseName, currentVersion,
 		func(db *idb.Database, oldVersion, newVersion uint) error {
 			return nil
@@ -209,11 +207,11 @@ func (w *wasmModel) hackTestDb() error {
 	if helper != nil {
 		return helper
 	}
-	result, err := w.get(messageStoreName, js.ValueOf(msgId))
+	result, err := indexedDb.Get(w.db, messageStoreName, js.ValueOf(msgId))
 	if err != nil {
 		return err
 	}
-	if len(result) == 0 {
+	if result.IsUndefined() {
 		return errors.Errorf("Failed to test db, record not present")
 	}
 	return nil
diff --git a/indexedDb/model.go b/indexedDb/channels/model.go
similarity index 95%
rename from indexedDb/model.go
rename to indexedDb/channels/model.go
index 839ed29c3c7b6aafe33ab9a0001d7b919964dd18..629c9f43353ade792f024f88aba769c3c89f22c0 100644
--- a/indexedDb/model.go
+++ b/indexedDb/channels/model.go
@@ -7,7 +7,7 @@
 
 //go:build js && wasm
 
-package indexedDb
+package channels
 
 import (
 	"time"
@@ -60,8 +60,8 @@ type Message struct {
 	Round           uint64        `json:"round"`
 
 	// User cryptographic Identity struct -- could be pulled out
-	Pubkey         []byte `json:"pubkey"`   // Index
-	DmToken        []byte `json:"dm_token"` // Index
+	Pubkey         []byte `json:"pubkey"`
+	DmToken        []byte `json:"dm_token"`
 	CodesetVersion uint8  `json:"codeset_version"`
 }
 
diff --git a/indexedDb/implementation_test.go b/indexedDb/implementation_test.go
index 511a022b6c6257f6e8c5877c7be914addedd725d..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 100644
--- a/indexedDb/implementation_test.go
+++ b/indexedDb/implementation_test.go
@@ -1,335 +0,0 @@
-////////////////////////////////////////////////////////////////////////////////
-// 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 indexedDb
-
-import (
-	"encoding/json"
-	"fmt"
-	"github.com/hack-pad/go-indexeddb/idb"
-	"gitlab.com/elixxir/xxdk-wasm/storage"
-	"gitlab.com/xx_network/primitives/netTime"
-	"os"
-	"strconv"
-	"testing"
-	"time"
-
-	jww "github.com/spf13/jwalterweatherman"
-	"gitlab.com/elixxir/client/v4/channels"
-	"gitlab.com/elixxir/client/v4/cmix/rounds"
-	cryptoBroadcast "gitlab.com/elixxir/crypto/broadcast"
-	"gitlab.com/elixxir/crypto/channel"
-	"gitlab.com/xx_network/primitives/id"
-)
-
-func TestMain(m *testing.M) {
-	jww.SetStdoutThreshold(jww.LevelDebug)
-	os.Exit(m.Run())
-}
-
-func dummyCallback(uint64, *id.ID, bool) {}
-
-// Test wasmModel.UpdateSentStatus happy path and ensure fields don't change.
-func Test_wasmModel_UpdateSentStatus(t *testing.T) {
-	testString := "test"
-	testMsgId := channel.MakeMessageID([]byte(testString), &id.ID{1})
-	eventModel, err := newWASMModel(testString, nil, dummyCallback)
-	if err != nil {
-		t.Fatalf("%+v", err)
-	}
-
-	// Store a test message
-	testMsg := buildMessage([]byte(testString), testMsgId.Bytes(), nil,
-		testString, []byte(testString), []byte{8, 6, 7, 5}, nil, 0, netTime.Now(),
-		time.Second, 0, 0, channels.Sent)
-	uuid, err := eventModel.receiveHelper(testMsg, false)
-	if err != nil {
-		t.Fatalf("%+v", err)
-	}
-
-	// Ensure one message is stored
-	results, err := eventModel.dump(messageStoreName)
-	if err != nil {
-		t.Fatalf("%+v", err)
-	}
-	if len(results) != 1 {
-		t.Fatalf("Expected 1 message to exist")
-	}
-
-	// Update the sentStatus
-	expectedStatus := channels.Failed
-	eventModel.UpdateSentStatus(uuid, testMsgId, netTime.Now(),
-		rounds.Round{ID: 8675309}, expectedStatus)
-
-	// Check the resulting status
-	results, err = eventModel.dump(messageStoreName)
-	if err != nil {
-		t.Fatalf("%+v", err)
-	}
-	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)
-	}
-	if resultMsg.Status != uint8(expectedStatus) {
-		t.Fatalf("Unexpected Status: %v", resultMsg.Status)
-	}
-
-	// Make sure other fields didn't change
-	if resultMsg.Nickname != testString {
-		t.Fatalf("Unexpected Nickname: %v", resultMsg.Nickname)
-	}
-}
-
-// Smoke test wasmModel.JoinChannel/wasmModel.LeaveChannel happy paths.
-func Test_wasmModel_JoinChannel_LeaveChannel(t *testing.T) {
-	storage.GetLocalStorage().Clear()
-	eventModel, err := newWASMModel("test", nil, dummyCallback)
-	if err != nil {
-		t.Fatalf("%+v", err)
-	}
-
-	testChannel := &cryptoBroadcast.Channel{
-		ReceptionID: id.NewIdFromString("test", id.Generic, t),
-		Name:        "test",
-		Description: "test",
-		Salt:        nil,
-	}
-	testChannel2 := &cryptoBroadcast.Channel{
-		ReceptionID: id.NewIdFromString("test2", id.Generic, t),
-		Name:        "test2",
-		Description: "test2",
-		Salt:        nil,
-	}
-	eventModel.JoinChannel(testChannel)
-	eventModel.JoinChannel(testChannel2)
-	results, err := eventModel.dump(channelsStoreName)
-	if err != nil {
-		t.Fatalf("%+v", err)
-	}
-	if len(results) != 2 {
-		t.Fatalf("Expected 2 channels to exist")
-	}
-	eventModel.LeaveChannel(testChannel.ReceptionID)
-	results, err = eventModel.dump(channelsStoreName)
-	if err != nil {
-		t.Fatalf("%+v", err)
-	}
-	if len(results) != 1 {
-		t.Fatalf("Expected 1 channels to exist")
-	}
-}
-
-// Test UUID gets returned when different messages are added.
-func Test_wasmModel_UUIDTest(t *testing.T) {
-	testString := "testHello"
-	eventModel, err := newWASMModel(testString, nil, dummyCallback)
-	if err != nil {
-		t.Fatalf("%+v", err)
-	}
-
-	uuids := make([]uint64, 10)
-
-	for i := 0; i < 10; i++ {
-		// Store a test message
-		channelID := id.NewIdFromBytes([]byte(testString), t)
-		msgID := channel.MessageID{}
-		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}, nil, 0,
-			netTime.Now(), time.Hour, rnd, 0, channels.Sent)
-		uuids[i] = uuid
-	}
-
-	_, _ = eventModel.dump(messageStoreName)
-
-	for i := 0; i < 10; i++ {
-		for j := i + 1; j < 10; j++ {
-			if uuids[i] == uuids[j] {
-				t.Fatalf("uuid failed: %d[%d] == %d[%d]",
-					uuids[i], i, uuids[j], j)
-			}
-		}
-	}
-}
-
-// Tests if the same message ID being sent always returns the same UUID.
-func Test_wasmModel_DuplicateReceives(t *testing.T) {
-	storage.GetLocalStorage().Clear()
-	testString := "testHello"
-	eventModel, err := newWASMModel(testString, nil, dummyCallback)
-	if err != nil {
-		t.Fatalf("%+v", err)
-	}
-
-	uuids := make([]uint64, 10)
-
-	msgID := channel.MessageID{}
-	copy(msgID[:], testString)
-	for i := 0; i < 10; i++ {
-		// Store a test message
-		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}, nil, 0,
-			netTime.Now(), time.Hour, rnd, 0, channels.Sent)
-		uuids[i] = uuid
-	}
-
-	_, _ = eventModel.dump(messageStoreName)
-
-	for i := 0; i < 10; i++ {
-		for j := i + 1; j < 10; j++ {
-			if uuids[i] != uuids[j] {
-				t.Fatalf("uuid failed: %d[%d] != %d[%d]",
-					uuids[i], i, uuids[j], j)
-			}
-		}
-	}
-}
-
-// Happy path: Inserts many messages, deletes some, and checks that the final
-// result is as expected.
-func Test_wasmModel_deleteMsgByChannel(t *testing.T) {
-	testString := "test_deleteMsgByChannel"
-	totalMessages := 10
-	expectedMessages := 5
-	eventModel, err := newWASMModel(testString, nil, dummyCallback)
-	if err != nil {
-		t.Fatalf("%+v", err)
-	}
-
-	// Create a test channel id
-	deleteChannel := id.NewIdFromString("deleteMe", id.Generic, t)
-	keepChannel := id.NewIdFromString("dontDeleteMe", id.Generic, t)
-
-	// Store some test messages
-	for i := 0; i < totalMessages; i++ {
-		testStr := testString + strconv.Itoa(i)
-
-		// Interleave the channel id to ensure cursor is behaving intelligently
-		thisChannel := deleteChannel
-		if i%2 == 0 {
-			thisChannel = keepChannel
-		}
-
-		testMsgId := channel.MakeMessageID([]byte(testStr), &id.ID{1})
-		eventModel.ReceiveMessage(thisChannel, testMsgId, testStr, testStr,
-			[]byte{8, 6, 7, 5}, nil, 0, netTime.Now(), time.Second,
-			rounds.Round{ID: id.Round(0)}, 0, channels.Sent)
-	}
-
-	// Check pre-results
-	result, err := eventModel.dump(messageStoreName)
-	if err != nil {
-		t.Fatalf("%+v", err)
-	}
-	if len(result) != totalMessages {
-		t.Errorf("Expected %d messages, got %d", totalMessages, len(result))
-	}
-
-	// Do delete
-	err = eventModel.deleteMsgByChannel(deleteChannel)
-	if err != nil {
-		t.Error(err)
-	}
-
-	// Check final results
-	result, err = eventModel.dump(messageStoreName)
-	if err != nil {
-		t.Fatalf("%+v", err)
-	}
-	if len(result) != expectedMessages {
-		t.Errorf("Expected %d messages, got %d", expectedMessages, len(result))
-	}
-}
-
-// This test is designed to prove the behavior of unique indexes.
-// Inserts will not fail, they simply will not happen.
-func TestWasmModel_receiveHelper_UniqueIndex(t *testing.T) {
-	testString := "test_receiveHelper_UniqueIndex"
-	eventModel, err := newWASMModel(testString, nil, dummyCallback)
-	if err != nil {
-		t.Fatal(err)
-	}
-
-	// Ensure index is unique
-	txn, err := eventModel.db.Transaction(
-		idb.TransactionReadOnly, messageStoreName)
-	if err != nil {
-		t.Fatal(err)
-	}
-	store, err := txn.ObjectStore(messageStoreName)
-	if err != nil {
-		t.Fatal(err)
-	}
-	idx, err := store.Index(messageStoreMessageIndex)
-	if err != nil {
-		t.Fatal(err)
-	}
-	if isUnique, err2 := idx.Unique(); !isUnique {
-		t.Fatalf("Index is not unique!")
-	} else if err2 != nil {
-		t.Fatal(err2)
-	}
-
-	// 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}, nil, 0, netTime.Now(),
-		time.Second, 0, 0, channels.Sent)
-	_, err = eventModel.receiveHelper(testMsg, false)
-	if err != nil {
-		t.Fatal(err)
-	}
-
-	// The duplicate entry won't fail, but it just silently shouldn't happen
-	_, err = eventModel.receiveHelper(testMsg, false)
-	if err != nil {
-		t.Fatalf("%+v", err)
-	}
-	results, err := eventModel.dump(messageStoreName)
-	if err != nil {
-		t.Fatalf("%+v", err)
-	}
-	if len(results) != 1 {
-		t.Fatalf("Expected only a single message, got %d", len(results))
-	}
-
-	// 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}, nil, 0, netTime.Now(),
-		time.Second, 0, 0, channels.Sent)
-	primaryKey, err := eventModel.receiveHelper(testMsg, false)
-	if err != nil {
-		t.Fatal(err)
-	}
-
-	// Except this time, we update the second entry to have the same
-	// message ID as the first
-	testMsg.ID = primaryKey
-	testMsg.MessageID = testMsgId.Bytes()
-	_, err = eventModel.receiveHelper(testMsg, true)
-	if err != nil {
-		t.Fatal(err)
-	}
-
-	// The update to duplicate message ID won't fail,
-	// but it just silently shouldn't happen
-	results, err = eventModel.dump(messageStoreName)
-	if err != nil {
-		t.Fatalf("%+v", err)
-	}
-	// TODO: Convert JSON to Message, ensure Message ID fields differ
-}
diff --git a/indexedDb/utils.go b/indexedDb/utils.go
new file mode 100644
index 0000000000000000000000000000000000000000..1aa6ff45805c1c65b9ad27e51a8b0590e7b8201d
--- /dev/null
+++ b/indexedDb/utils.go
@@ -0,0 +1,231 @@
+////////////////////////////////////////////////////////////////////////////////
+// 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
+
+// This file contains several generic IndexedDB helper functions that
+// may be useful for any IndexedDB implementations.
+
+package indexedDb
+
+import (
+	"context"
+	"github.com/hack-pad/go-indexeddb/idb"
+	"github.com/pkg/errors"
+	jww "github.com/spf13/jwalterweatherman"
+	"gitlab.com/elixxir/xxdk-wasm/utils"
+	"syscall/js"
+	"time"
+)
+
+// dbTimeout is the global timeout for operations with the storage
+// [context.Context].
+const dbTimeout = time.Second
+
+// NewContext builds a context for indexedDb operations.
+func NewContext() (context.Context, context.CancelFunc) {
+	return context.WithTimeout(context.Background(), dbTimeout)
+}
+
+// Get is a generic helper for getting values from the given [idb.ObjectStore].
+func Get(db *idb.Database, objectStoreName string, key js.Value) (js.Value, error) {
+	parentErr := errors.Errorf("failed to Get %s/%s", objectStoreName, key)
+
+	// Prepare the Transaction
+	txn, err := db.Transaction(idb.TransactionReadOnly, objectStoreName)
+	if err != nil {
+		return js.Undefined(), errors.WithMessagef(parentErr,
+			"Unable to create Transaction: %+v", err)
+	}
+	store, err := txn.ObjectStore(objectStoreName)
+	if err != nil {
+		return js.Undefined(), errors.WithMessagef(parentErr,
+			"Unable to get ObjectStore: %+v", err)
+	}
+
+	// Perform the operation
+	getRequest, err := store.Get(key)
+	if err != nil {
+		return js.Undefined(), errors.WithMessagef(parentErr,
+			"Unable to Get from ObjectStore: %+v", err)
+	}
+
+	// Wait for the operation to return
+	ctx, cancel := NewContext()
+	resultObj, err := getRequest.Await(ctx)
+	cancel()
+	if err != nil {
+		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")
+	}
+
+	// Process result into string
+	jww.DEBUG.Printf("Got from %s/%s: %s",
+		objectStoreName, key, utils.JsToJson(resultObj))
+	return resultObj, nil
+}
+
+// GetIndex is a generic helper for getting values from the given
+// [idb.ObjectStore] using the given [idb.Index].
+func GetIndex(db *idb.Database, objectStoreName string,
+	indexName string, key js.Value) (js.Value, error) {
+	parentErr := errors.Errorf("failed to GetIndex %s/%s/%s",
+		objectStoreName, indexName, key)
+
+	// Prepare the Transaction
+	txn, err := db.Transaction(idb.TransactionReadOnly, objectStoreName)
+	if err != nil {
+		return js.Undefined(), errors.WithMessagef(parentErr,
+			"Unable to create Transaction: %+v", err)
+	}
+	store, err := txn.ObjectStore(objectStoreName)
+	if err != nil {
+		return js.Undefined(), errors.WithMessagef(parentErr,
+			"Unable to get ObjectStore: %+v", err)
+	}
+	idx, err := store.Index(indexName)
+	if err != nil {
+		return js.Undefined(), errors.WithMessagef(parentErr,
+			"Unable to get Index: %+v", err)
+	}
+
+	// Perform the operation
+	getRequest, err := idx.Get(key)
+	if err != nil {
+		return js.Undefined(), errors.WithMessagef(parentErr,
+			"Unable to Get from ObjectStore: %+v", err)
+	}
+
+	// Wait for the operation to return
+	ctx, cancel := NewContext()
+	resultObj, err := getRequest.Await(ctx)
+	cancel()
+	if err != nil {
+		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")
+	}
+
+	// Process result into string
+	jww.DEBUG.Printf("Got from %s/%s/%s: %s",
+		objectStoreName, indexName, key, utils.JsToJson(resultObj))
+	return resultObj, nil
+}
+
+// Put is a generic helper for putting values into the given [idb.ObjectStore].
+// Equivalent to insert if not exists else update.
+func Put(db *idb.Database, objectStoreName string, value js.Value) (*idb.Request, error) {
+	// Prepare the Transaction
+	txn, err := db.Transaction(idb.TransactionReadWrite, objectStoreName)
+	if err != nil {
+		return nil, errors.Errorf("Unable to create Transaction: %+v", err)
+	}
+	store, err := txn.ObjectStore(objectStoreName)
+	if err != nil {
+		return nil, errors.Errorf("Unable to get ObjectStore: %+v", err)
+	}
+
+	// Perform the operation
+	request, err := store.Put(value)
+	if err != nil {
+		return nil, errors.Errorf("Unable to Put: %+v", err)
+	}
+
+	// Wait for the operation to return
+	ctx, cancel := NewContext()
+	err = txn.Await(ctx)
+	cancel()
+	if err != nil {
+		return nil, errors.Errorf("Putting value failed: %+v", err)
+	}
+	jww.DEBUG.Printf("Successfully put value in %s: %v",
+		objectStoreName, utils.JsToJson(value))
+	return request, nil
+}
+
+// Delete is a generic helper for removing values from the given [idb.ObjectStore].
+func Delete(db *idb.Database, objectStoreName string, key js.Value) error {
+	parentErr := errors.Errorf("failed to Delete %s/%s", objectStoreName, key)
+
+	// Prepare the Transaction
+	txn, err := db.Transaction(idb.TransactionReadOnly, objectStoreName)
+	if err != nil {
+		return errors.WithMessagef(parentErr,
+			"Unable to create Transaction: %+v", err)
+	}
+	store, err := txn.ObjectStore(objectStoreName)
+	if err != nil {
+		return errors.WithMessagef(parentErr,
+			"Unable to get ObjectStore: %+v", err)
+	}
+
+	// Perform the operation
+	deleteRequest, err := store.Delete(key)
+	if err != nil {
+		return errors.WithMessagef(parentErr,
+			"Unable to Get from ObjectStore: %+v", err)
+	}
+
+	// Wait for the operation to return
+	ctx, cancel := NewContext()
+	err = deleteRequest.Await(ctx)
+	cancel()
+	if err != nil {
+		return errors.WithMessagef(parentErr,
+			"Unable to delete from ObjectStore: %+v", err)
+	}
+	return nil
+}
+
+// Dump returns the given [idb.ObjectStore] contents to string slice for
+// testing and debugging purposes.
+func Dump(db *idb.Database, objectStoreName string) ([]string, error) {
+	parentErr := errors.Errorf("failed to Dump %s", objectStoreName)
+
+	txn, err := db.Transaction(idb.TransactionReadOnly, objectStoreName)
+	if err != nil {
+		return nil, errors.WithMessagef(parentErr,
+			"Unable to create Transaction: %+v", err)
+	}
+	store, err := txn.ObjectStore(objectStoreName)
+	if err != nil {
+		return nil, errors.WithMessagef(parentErr,
+			"Unable to get ObjectStore: %+v", err)
+	}
+	cursorRequest, err := store.OpenCursor(idb.CursorNext)
+	if err != nil {
+		return nil, errors.WithMessagef(parentErr,
+			"Unable to open Cursor: %+v", err)
+	}
+
+	// Run the query
+	jww.DEBUG.Printf("%s values:", objectStoreName)
+	results := make([]string, 0)
+	ctx, cancel := NewContext()
+	err = cursorRequest.Iter(ctx,
+		func(cursor *idb.CursorWithValue) error {
+			value, err := cursor.Value()
+			if err != nil {
+				return err
+			}
+			valueStr := utils.JsToJson(value)
+			results = append(results, valueStr)
+			jww.DEBUG.Printf("- %v", valueStr)
+			return nil
+		})
+	cancel()
+	if err != nil {
+		return nil, errors.WithMessagef(parentErr,
+			"Unable to dump ObjectStore: %+v", err)
+	}
+	return results, nil
+}
diff --git a/indexedDb/utils_test.go b/indexedDb/utils_test.go
new file mode 100644
index 0000000000000000000000000000000000000000..1c657ebd5dbb4607c977323dc8883042989f05f9
--- /dev/null
+++ b/indexedDb/utils_test.go
@@ -0,0 +1,80 @@
+////////////////////////////////////////////////////////////////////////////////
+// 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 indexedDb
+
+import (
+	"github.com/hack-pad/go-indexeddb/idb"
+	"strings"
+	"syscall/js"
+	"testing"
+)
+
+// Error path: Tests that Get returns an error when trying to get a message that
+// does not exist.
+func TestGet_NoMessageError(t *testing.T) {
+	db := newTestDB("messages", "index", t)
+
+	_, err := Get(db, "messages", js.ValueOf(5))
+	if err == nil || !strings.Contains(err.Error(), "undefined") {
+		t.Errorf("Did not get expected error when getting a message that "+
+			"does not exist: %+v", err)
+	}
+}
+
+// Error path: Tests that GetIndex returns an error when trying to get a message
+// that does not exist.
+func TestGetIndex_NoMessageError(t *testing.T) {
+	db := newTestDB("messages", "index", t)
+
+	_, err := GetIndex(db, "messages", "index", js.ValueOf(5))
+	if err == nil || !strings.Contains(err.Error(), "undefined") {
+		t.Errorf("Did not get expected error when getting a message that "+
+			"does not exist: %+v", err)
+	}
+}
+
+// newTestDB creates a new idb.Database for testing.
+func newTestDB(name, index string, t *testing.T) *idb.Database {
+	// Attempt to open database object
+	ctx, cancel := NewContext()
+	defer cancel()
+	openRequest, err := idb.Global().Open(ctx, "databaseName", 0,
+		func(db *idb.Database, _ uint, _ uint) error {
+			storeOpts := idb.ObjectStoreOptions{
+				KeyPath:       js.ValueOf("id"),
+				AutoIncrement: true,
+			}
+
+			// Build Message ObjectStore and Indexes
+			messageStore, err := db.CreateObjectStore(name, storeOpts)
+			if err != nil {
+				return err
+			}
+
+			_, err = messageStore.CreateIndex(
+				index, js.ValueOf("id"), idb.IndexOptions{})
+			if err != nil {
+				return err
+			}
+
+			return nil
+		})
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	// Wait for database open to finish
+	db, err := openRequest.Await(ctx)
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	return db
+}
diff --git a/storage/version.go b/storage/version.go
index 61bc23937a227f407b059b08362938c62222d558..9530aca55472b737e1905fe233f70a78689a52b5 100644
--- a/storage/version.go
+++ b/storage/version.go
@@ -18,7 +18,7 @@ import (
 )
 
 // SEMVER is the current semantic version of xxDK WASM.
-const SEMVER = "0.1.8"
+const SEMVER = "0.1.13"
 
 // Storage keys.
 const (
diff --git a/wasm/channels.go b/wasm/channels.go
index c906ae53f50081b8bdc6c26c55a56992c0ba1bd4..447bea74ea42a6674c3859f10a12ef5ab2bf6fce 100644
--- a/wasm/channels.go
+++ b/wasm/channels.go
@@ -11,12 +11,12 @@ package wasm
 
 import (
 	"encoding/base64"
+	"gitlab.com/elixxir/xxdk-wasm/indexedDb/channels"
 	"gitlab.com/xx_network/primitives/id"
 	"sync"
 	"syscall/js"
 
 	"gitlab.com/elixxir/client/v4/bindings"
-	"gitlab.com/elixxir/xxdk-wasm/indexedDb"
 	"gitlab.com/elixxir/xxdk-wasm/utils"
 )
 
@@ -394,7 +394,7 @@ func newChannelsManagerWithIndexedDb(cmixID int, privateIdentity []byte,
 		cb.Invoke(uuid, utils.CopyBytesToJS(channelID.Marshal()), update)
 	}
 
-	model := indexedDb.NewWASMEventModelBuilder(cipher, messageReceivedCB)
+	model := channels.NewWASMEventModelBuilder(cipher, messageReceivedCB)
 
 	promiseFn := func(resolve, reject func(args ...any) js.Value) {
 		cm, err := bindings.NewChannelsManagerGoEventModel(
@@ -493,7 +493,7 @@ func loadChannelsManagerWithIndexedDb(cmixID int, storageTag string,
 		cb.Invoke(uuid, utils.CopyBytesToJS(channelID.Marshal()), updated)
 	}
 
-	model := indexedDb.NewWASMEventModelBuilder(cipher, messageReceivedCB)
+	model := channels.NewWASMEventModelBuilder(cipher, messageReceivedCB)
 
 	promiseFn := func(resolve, reject func(args ...any) js.Value) {
 		cm, err := bindings.LoadChannelsManagerGoEventModel(