diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 3f20ee763de06116b49e3d0692dec10c2822cb64..e1db8fbdd50c958f5701105d7aacc72f0a493127 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -19,9 +19,10 @@ before_script:
 stages:
   - test
   - build
+  - combine-artifacts
   - tag
   - doc-update
-  - combine_artefacts
+  - version-check
 
 go-test:
   stage: test
@@ -50,26 +51,28 @@ build:
   script:
     - go mod vendor -v
     - mkdir -p release
-    - GOOS=js GOARCH=wasm go build -ldflags '-w -s' -o release/xxdk.wasm main.go
+    - GOOS=js GOARCH=wasm go build -ldflags '-w -s' -trimpath -o release/xxdk.wasm main.go
     - cp wasm_exec.js release/
   artifacts:
     paths:
       - release/
-    expire_in: 1 hour
 
-emoji-update:
+build-workers:
   stage: build
   except:
     - tags
   script:
     - go mod vendor -v
     - mkdir -p release
-    - go run -ldflags '-w -s' ./emoji/... -o emojiSet.json -v 2
-    - cp emojiSet.json release/
+    - GOOS=js GOARCH=wasm go build -ldflags '-w -s' -trimpath -o release/xxdk-channelsIndexedDkWorker.wasm ./indexedDb/impl/channels/...
+    - GOOS=js GOARCH=wasm go build -ldflags '-w -s' -trimpath -o release/xxdk-dmIndexedDkWorker.wasm ./indexedDb/impl/dm/...
+    - GOOS=js GOARCH=wasm go build -ldflags '-w -s' -trimpath -o release/xxdk-logFileWorker.wasm ./logging/workerThread/...
+    - cp indexedDb/impl/channels/channelsIndexedDbWorker.js release/
+    - cp indexedDb/impl/dm/dmIndexedDbWorker.js release/
+    - cp logging/workerThread/logFileWorker.js release/
   artifacts:
     paths:
       - release/
-    expire_in: 1 hour
 
 tag:
   stage: build
@@ -82,8 +85,8 @@ tag:
     - git tag $(sha256sum release/xxdk.wasm | awk '{ print $1 }') -f
     - git push origin_tags -f --tags
 
-combine_artefacts:
-  stage: combine_artefacts
+combine-artifacts:
+  stage: combine-artifacts
   except:
     - tags
   image: $DOCKER_IMAGE
@@ -92,17 +95,22 @@ combine_artefacts:
     - 'PIPELINE_JOBS=$(curl --header "PRIVATE-TOKEN: $GITLAB_ACCESS_TOKEN" $CI_SERVER_URL/api/v4/projects/$CI_PROJECT_ID/pipelines/$CI_PIPELINE_ID/jobs)'
     - echo $PIPELINE_JOBS
     - BUILD_JOB_JSON=$(echo $PIPELINE_JOBS | jq '.[] | select(.name=="build")')
-    - EMOJI_UPDATE_JOB_JSON=$(echo $PIPELINE_JOBS | jq '.[] | select(.name=="emoji-update")')
+    - BUILD_WORKERS_JOB_JSON=$(echo $PIPELINE_JOBS | jq '.[] | select(.name=="build-workers")')
 
     - BUILD_JOB_ID=$(echo $BUILD_JOB_JSON | jq -r '.["id"]')
-    - EMOJI_UPDATE_JOB_ID=$(echo $EMOJI_UPDATE_JOB_JSON | jq -r '.["id"]')
+    - BUILD_WORKERS_JOB_ID=$(echo $BUILD_WORKERS_JOB_JSON | jq -r '.["id"]')
 
     - rm -rf release
     - mkdir -p release
 
     - 'curl --header "PRIVATE-TOKEN: $GITLAB_ACCESS_TOKEN" --output release/wasm_exec.js $CI_SERVER_URL/api/v4/projects/$CI_PROJECT_ID/jobs/$BUILD_JOB_ID/artifacts/release/wasm_exec.js'
     - 'curl --header "PRIVATE-TOKEN: $GITLAB_ACCESS_TOKEN" --output release/xxdk.wasm $CI_SERVER_URL/api/v4/projects/$CI_PROJECT_ID/jobs/$BUILD_JOB_ID/artifacts/release/xxdk.wasm'
-    - 'curl --header "PRIVATE-TOKEN: $GITLAB_ACCESS_TOKEN" --output release/emojiSet.json $CI_SERVER_URL/api/v4/projects/$CI_PROJECT_ID/jobs/$EMOJI_UPDATE_JOB_ID/artifacts/release/emojiSet.json'
+    - 'curl --header "PRIVATE-TOKEN: $GITLAB_ACCESS_TOKEN" --output release/xxdk-channelsIndexedDkWorker.wasm $CI_SERVER_URL/api/v4/projects/$CI_PROJECT_ID/jobs/$BUILD_WORKERS_JOB_ID/artifacts/release/xxdk-channelsIndexedDkWorker.wasm'
+    - 'curl --header "PRIVATE-TOKEN: $GITLAB_ACCESS_TOKEN" --output release/xxdk-dmIndexedDkWorker.wasm $CI_SERVER_URL/api/v4/projects/$CI_PROJECT_ID/jobs/$BUILD_WORKERS_JOB_ID/artifacts/release/xxdk-dmIndexedDkWorker.wasm'
+    - 'curl --header "PRIVATE-TOKEN: $GITLAB_ACCESS_TOKEN" --output release/xxdk-logFileWorker.wasm $CI_SERVER_URL/api/v4/projects/$CI_PROJECT_ID/jobs/$BUILD_WORKERS_JOB_ID/artifacts/release/xxdk-logFileWorker.wasm'
+    - 'curl --header "PRIVATE-TOKEN: $GITLAB_ACCESS_TOKEN" --output release/channelsIndexedDbWorker.js $CI_SERVER_URL/api/v4/projects/$CI_PROJECT_ID/jobs/$BUILD_WORKERS_JOB_ID/artifacts/release/channelsIndexedDbWorker.js'
+    - 'curl --header "PRIVATE-TOKEN: $GITLAB_ACCESS_TOKEN" --output release/dmIndexedDbWorker.js $CI_SERVER_URL/api/v4/projects/$CI_PROJECT_ID/jobs/$BUILD_WORKERS_JOB_ID/artifacts/release/dmIndexedDbWorker.js'
+    - 'curl --header "PRIVATE-TOKEN: $GITLAB_ACCESS_TOKEN" --output release/logFileWorker.js $CI_SERVER_URL/api/v4/projects/$CI_PROJECT_ID/jobs/$BUILD_WORKERS_JOB_ID/artifacts/release/logFileWorker.js'
 
     - ls release
   artifacts:
@@ -112,18 +120,31 @@ combine_artefacts:
 
 # This pipeline job will attempt to have pkg.go.dev update docs for xxdk-wasm.
 #
-# pkg.go.dev relies on the proxy.golang.org service (go module cache/proxy) to discover versions of
-# Go modules to make docs of. The proxy keeps a list of all known versions of Go modules. The go
-# mod proxy does cache pulls for about 30 minutes, so if quickly successive commits are done in
-# master/release, this will fail to pull the latest client, and the docs will not update.
+# pkg.go.dev relies on the proxy.golang.org service (go module cache/proxy) to
+# discover versions of Go modules to make docs of. The proxy keeps a list of all
+# known versions of Go modules. The go mod proxy does cache pulls for about 30
+# minutes, so if quickly successive commits are done in 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.
+    # GOPRIVATE is cleared so that the public cache is pulled instead of directly pulling client.
     - GOOS=js GOARCH=wasm GOPRIVATE="" go install gitlab.com/elixxir/xxdk-wasm@$CI_COMMIT_SHA
   only:
     - release
     - master
+
+version-check:
+  stage: version-check
+  except:
+    - tags
+  only:
+    - master
+  image: $DOCKER_IMAGE
+  script:
+    - GITTAG=$(git describe --tags)
+    - CODEVERS=$(cat storage/version.go | grep "const SEMVER =" | cut -d ' ' -f4 | tr -d '"')
+    - if [[ $GITTAG != $CODEVERS ]]; then echo "VERSION NUMBER BAD $GITTAG != $CODEVERS"; exit -1; fi
\ No newline at end of file
diff --git a/Makefile b/Makefile
index 4a6c40fd662d82ea2d2060f874c3a88ca266a0e3..bc170d109666ddd78cbfd9a9dc0388b2559b5504 100644
--- a/Makefile
+++ b/Makefile
@@ -28,6 +28,13 @@ update_master:
 binary:
 	GOOS=js GOARCH=wasm go build -ldflags '-w -s' -trimpath -o xxdk.wasm main.go
 
+worker_binaries:
+	GOOS=js GOARCH=wasm go build -ldflags '-w -s' -trimpath -o xxdk-channelsIndexedDkWorker.wasm ./indexedDb/impl/channels/...
+	GOOS=js GOARCH=wasm go build -ldflags '-w -s' -trimpath -o xxdk-dmIndexedDkWorker.wasm ./indexedDb/impl/dm/...
+	GOOS=js GOARCH=wasm go build -ldflags '-w -s' -trimpath -o xxdk-logFileWorker.wasm ./logging/workerThread/...
+
+binaries: binary worker_binaries
+
 wasm_tests:
 	cp utils/utils_js.s utils/utils_js.s.bak
 	> utils/utils_js.s
diff --git a/indexedDb/impl/channels/callbacks.go b/indexedDb/impl/channels/callbacks.go
new file mode 100644
index 0000000000000000000000000000000000000000..68cc92d71f962ab4adc5d734f598561c95b006ea
--- /dev/null
+++ b/indexedDb/impl/channels/callbacks.go
@@ -0,0 +1,446 @@
+////////////////////////////////////////////////////////////////////////////////
+// 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 main
+
+import (
+	"crypto/ed25519"
+	"encoding/json"
+	"github.com/pkg/errors"
+	jww "github.com/spf13/jwalterweatherman"
+	"gitlab.com/elixxir/client/v4/channels"
+	"gitlab.com/elixxir/client/v4/cmix/rounds"
+	cryptoBroadcast "gitlab.com/elixxir/crypto/broadcast"
+	cryptoChannel "gitlab.com/elixxir/crypto/channel"
+	"gitlab.com/elixxir/crypto/fastRNG"
+	"gitlab.com/elixxir/crypto/message"
+	wChannels "gitlab.com/elixxir/xxdk-wasm/indexedDb/worker/channels"
+	"gitlab.com/elixxir/xxdk-wasm/worker"
+	"gitlab.com/xx_network/crypto/csprng"
+	"gitlab.com/xx_network/primitives/id"
+	"time"
+)
+
+var zeroUUID = []byte{0, 0, 0, 0, 0, 0, 0, 0}
+
+// manager handles the event model and the message callbacks, which is used to
+// send information between the event model and the main thread.
+type manager struct {
+	mh    *worker.ThreadManager
+	model channels.EventModel
+}
+
+// registerCallbacks registers all the reception callbacks to manage messages
+// from the main thread for the channels.EventModel.
+func (m *manager) registerCallbacks() {
+	m.mh.RegisterCallback(wChannels.NewWASMEventModelTag, m.newWASMEventModelCB)
+	m.mh.RegisterCallback(wChannels.JoinChannelTag, m.joinChannelCB)
+	m.mh.RegisterCallback(wChannels.LeaveChannelTag, m.leaveChannelCB)
+	m.mh.RegisterCallback(wChannels.ReceiveMessageTag, m.receiveMessageCB)
+	m.mh.RegisterCallback(wChannels.ReceiveReplyTag, m.receiveReplyCB)
+	m.mh.RegisterCallback(wChannels.ReceiveReactionTag, m.receiveReactionCB)
+	m.mh.RegisterCallback(wChannels.UpdateFromUUIDTag, m.updateFromUUIDCB)
+	m.mh.RegisterCallback(wChannels.UpdateFromMessageIDTag, m.updateFromMessageIDCB)
+	m.mh.RegisterCallback(wChannels.GetMessageTag, m.getMessageCB)
+	m.mh.RegisterCallback(wChannels.DeleteMessageTag, m.deleteMessageCB)
+	m.mh.RegisterCallback(wChannels.MuteUserTag, m.muteUserCB)
+}
+
+// newWASMEventModelCB is the callback for NewWASMEventModel. Returns an empty
+// slice on success or an error message on failure.
+func (m *manager) newWASMEventModelCB(data []byte) ([]byte, error) {
+	var msg wChannels.NewWASMEventModelMessage
+	err := json.Unmarshal(data, &msg)
+	if err != nil {
+		return []byte{}, errors.Errorf(
+			"failed to JSON unmarshal %T from main thread: %+v", msg, err)
+	}
+
+	// Create new encryption cipher
+	rng := fastRNG.NewStreamGenerator(12, 1024, csprng.NewSystemRNG)
+	encryption, err := cryptoChannel.NewCipherFromJSON(
+		[]byte(msg.EncryptionJSON), rng.GetStream())
+	if err != nil {
+		return []byte{}, errors.Errorf(
+			"failed to JSON unmarshal Cipher from main thread: %+v", err)
+	}
+
+	m.model, err = NewWASMEventModel(msg.Path, encryption,
+		m.messageReceivedCallback, m.deletedMessageCallback, m.mutedUserCallback,
+		m.storeDatabaseName, m.storeEncryptionStatus)
+	if err != nil {
+		return []byte(err.Error()), nil
+	}
+	return []byte{}, nil
+}
+
+// messageReceivedCallback sends calls to the channels.MessageReceivedCallback
+// in the main thread.
+//
+// storeEncryptionStatus adhere to the channels.MessageReceivedCallback type.
+func (m *manager) messageReceivedCallback(
+	uuid uint64, channelID *id.ID, update bool) {
+	// Package parameters for sending
+	msg := &wChannels.MessageReceivedCallbackMessage{
+		UUID:      uuid,
+		ChannelID: channelID,
+		Update:    update,
+	}
+	data, err := json.Marshal(msg)
+	if err != nil {
+		jww.ERROR.Printf("Could not JSON marshal %T: %+v", msg, err)
+		return
+	}
+
+	// Send it to the main thread
+	m.mh.SendMessage(wChannels.MessageReceivedCallbackTag, data)
+}
+
+// deletedMessageCallback sends calls to the channels.DeletedMessageCallback in
+// the main thread.
+//
+// storeEncryptionStatus adhere to the channels.MessageReceivedCallback type.
+func (m *manager) deletedMessageCallback(messageID message.ID) {
+	m.mh.SendMessage(wChannels.DeletedMessageCallbackTag, messageID.Marshal())
+}
+
+// mutedUserCallback sends calls to the channels.MutedUserCallback in the main
+// thread.
+//
+// storeEncryptionStatus adhere to the channels.MessageReceivedCallback type.
+func (m *manager) mutedUserCallback(
+	channelID *id.ID, pubKey ed25519.PublicKey, unmute bool) {
+	// Package parameters for sending
+	msg := &wChannels.MuteUserMessage{
+		ChannelID: channelID,
+		PubKey:    pubKey,
+		Unmute:    unmute,
+	}
+	data, err := json.Marshal(msg)
+	if err != nil {
+		jww.ERROR.Printf("Could not JSON marshal %T: %+v", msg, err)
+		return
+	}
+
+	// Send it to the main thread
+	m.mh.SendMessage(wChannels.MutedUserCallbackTag, data)
+}
+
+// storeDatabaseName sends the database name to the main thread and waits for
+// the response. This function mocks the behavior of storage.StoreIndexedDb.
+//
+// storeDatabaseName adheres to the storeDatabaseNameFn type.
+func (m *manager) storeDatabaseName(databaseName string) error {
+	// Register response callback with channel that will wait for the response
+	responseChan := make(chan []byte)
+	m.mh.RegisterCallback(wChannels.StoreDatabaseNameTag,
+		func(data []byte) ([]byte, error) {
+			responseChan <- data
+			return nil, nil
+		})
+
+	// Send encryption status to main thread
+	m.mh.SendMessage(wChannels.StoreDatabaseNameTag, []byte(databaseName))
+
+	// Wait for response
+	select {
+	case response := <-responseChan:
+		if len(response) > 0 {
+			return errors.New(string(response))
+		}
+	case <-time.After(worker.ResponseTimeout):
+		return errors.Errorf("[WW] Timed out after %s waiting for response "+
+			"about storing the database name in local storage in the main "+
+			"thread", worker.ResponseTimeout)
+	}
+
+	return nil
+}
+
+// storeEncryptionStatus sends the database name and encryption status to the
+// main thread and waits for the response. If the value has not been previously
+// saved, it returns the saves encryption status. This function mocks the
+// behavior of storage.StoreIndexedDbEncryptionStatus.
+//
+// storeEncryptionStatus adheres to the storeEncryptionStatusFn type.
+func (m *manager) storeEncryptionStatus(
+	databaseName string, encryption bool) (bool, error) {
+	// Package parameters for sending
+	msg := &wChannels.EncryptionStatusMessage{
+		DatabaseName:     databaseName,
+		EncryptionStatus: encryption,
+	}
+	data, err := json.Marshal(msg)
+	if err != nil {
+		return false, err
+	}
+
+	// Register response callback with channel that will wait for the response
+	responseChan := make(chan []byte)
+	m.mh.RegisterCallback(wChannels.EncryptionStatusTag,
+		func(data []byte) ([]byte, error) {
+			responseChan <- data
+			return nil, nil
+		})
+
+	// Send encryption status to main thread
+	m.mh.SendMessage(wChannels.EncryptionStatusTag, data)
+
+	// Wait for response
+	var response wChannels.EncryptionStatusReply
+	select {
+	case responseData := <-responseChan:
+		if err = json.Unmarshal(responseData, &response); err != nil {
+			return false, err
+		}
+	case <-time.After(worker.ResponseTimeout):
+		return false, errors.Errorf("timed out after %s waiting for "+
+			"response about the database encryption status from local "+
+			"storage in the main thread", worker.ResponseTimeout)
+	}
+
+	// If the response contain an error, return it
+	if response.Error != "" {
+		return false, errors.New(response.Error)
+	}
+
+	// Return the encryption status
+	return response.EncryptionStatus, nil
+}
+
+// joinChannelCB is the callback for wasmModel.JoinChannel. Always returns nil;
+// meaning, no response is supplied (or expected).
+func (m *manager) joinChannelCB(data []byte) ([]byte, error) {
+	var channel cryptoBroadcast.Channel
+	err := json.Unmarshal(data, &channel)
+	if err != nil {
+		return nil, errors.Errorf(
+			"failed to JSON unmarshal %T from main thread: %+v", channel, err)
+	}
+
+	m.model.JoinChannel(&channel)
+	return nil, nil
+}
+
+// leaveChannelCB is the callback for wasmModel.LeaveChannel. Always returns
+// nil; meaning, no response is supplied (or expected).
+func (m *manager) leaveChannelCB(data []byte) ([]byte, error) {
+	channelID, err := id.Unmarshal(data)
+	if err != nil {
+		return nil, errors.Errorf(
+			"failed to JSON unmarshal %T from main thread: %+v", channelID, err)
+	}
+
+	m.model.LeaveChannel(channelID)
+	return nil, nil
+}
+
+// receiveMessageCB is the callback for wasmModel.ReceiveMessage. Returns a UUID
+// of 0 on error or the JSON marshalled UUID (uint64) on success.
+func (m *manager) receiveMessageCB(data []byte) ([]byte, error) {
+	var msg channels.ModelMessage
+	err := json.Unmarshal(data, &msg)
+	if err != nil {
+		return zeroUUID, errors.Errorf(
+			"failed to JSON unmarshal %T from main thread: %+v", msg, err)
+	}
+
+	uuid := m.model.ReceiveMessage(msg.ChannelID, msg.MessageID, msg.Nickname,
+		string(msg.Content), msg.PubKey, msg.DmToken, msg.CodesetVersion,
+		msg.Timestamp, msg.Lease, rounds.Round{ID: msg.Round}, msg.Type,
+		msg.Status, msg.Hidden)
+
+	uuidData, err := json.Marshal(uuid)
+	if err != nil {
+		return zeroUUID, errors.Errorf("failed to JSON marshal UUID : %+v", err)
+	}
+	return uuidData, nil
+}
+
+// receiveReplyCB is the callback for wasmModel.ReceiveReply. Returns a UUID of
+// 0 on error or the JSON marshalled UUID (uint64) on success.
+func (m *manager) receiveReplyCB(data []byte) ([]byte, error) {
+	var msg wChannels.ReceiveReplyMessage
+	err := json.Unmarshal(data, &msg)
+	if err != nil {
+		return zeroUUID, errors.Errorf(
+			"failed to JSON unmarshal %T from main thread: %+v", msg, err)
+	}
+
+	uuid := m.model.ReceiveReply(msg.ChannelID, msg.MessageID, msg.ReactionTo,
+		msg.Nickname, string(msg.Content), msg.PubKey, msg.DmToken,
+		msg.CodesetVersion, msg.Timestamp, msg.Lease,
+		rounds.Round{ID: msg.Round}, msg.Type, msg.Status, msg.Hidden)
+
+	uuidData, err := json.Marshal(uuid)
+	if err != nil {
+		return zeroUUID, errors.Errorf("failed to JSON marshal UUID : %+v", err)
+	}
+	return uuidData, nil
+}
+
+// receiveReactionCB is the callback for wasmModel.ReceiveReaction. Returns a
+// UUID of 0 on error or the JSON marshalled UUID (uint64) on success.
+func (m *manager) receiveReactionCB(data []byte) ([]byte, error) {
+	var msg wChannels.ReceiveReplyMessage
+	err := json.Unmarshal(data, &msg)
+	if err != nil {
+		return zeroUUID, errors.Errorf(
+			"failed to JSON unmarshal %T from main thread: %+v", msg, err)
+	}
+
+	uuid := m.model.ReceiveReaction(msg.ChannelID, msg.MessageID,
+		msg.ReactionTo, msg.Nickname, string(msg.Content), msg.PubKey,
+		msg.DmToken, msg.CodesetVersion, msg.Timestamp, msg.Lease,
+		rounds.Round{ID: msg.Round}, msg.Type, msg.Status, msg.Hidden)
+
+	uuidData, err := json.Marshal(uuid)
+	if err != nil {
+		return zeroUUID, errors.Errorf("failed to JSON marshal UUID : %+v", err)
+	}
+	return uuidData, nil
+}
+
+// updateFromUUIDCB is the callback for wasmModel.UpdateFromUUID. Always returns
+// nil; meaning, no response is supplied (or expected).
+func (m *manager) updateFromUUIDCB(data []byte) ([]byte, error) {
+	var msg wChannels.MessageUpdateInfo
+	err := json.Unmarshal(data, &msg)
+	if err != nil {
+		return nil, errors.Errorf(
+			"failed to JSON unmarshal %T from main thread: %+v", msg, err)
+	}
+	var messageID *message.ID
+	var timestamp *time.Time
+	var round *rounds.Round
+	var pinned, hidden *bool
+	var status *channels.SentStatus
+	if msg.MessageIDSet {
+		messageID = &msg.MessageID
+	}
+	if msg.TimestampSet {
+		timestamp = &msg.Timestamp
+	}
+	if msg.RoundIDSet {
+		round = &rounds.Round{ID: msg.RoundID}
+	}
+	if msg.PinnedSet {
+		pinned = &msg.Pinned
+	}
+	if msg.HiddenSet {
+		hidden = &msg.Hidden
+	}
+	if msg.StatusSet {
+		status = &msg.Status
+	}
+
+	m.model.UpdateFromUUID(
+		msg.UUID, messageID, timestamp, round, pinned, hidden, status)
+	return nil, nil
+}
+
+// updateFromMessageIDCB is the callback for wasmModel.UpdateFromMessageID.
+// Always returns nil; meaning, no response is supplied (or expected).
+func (m *manager) updateFromMessageIDCB(data []byte) ([]byte, error) {
+	var msg wChannels.MessageUpdateInfo
+	err := json.Unmarshal(data, &msg)
+	if err != nil {
+		return nil, errors.Errorf(
+			"failed to JSON unmarshal %T from main thread: %+v", msg, err)
+	}
+	var timestamp *time.Time
+	var round *rounds.Round
+	var pinned, hidden *bool
+	var status *channels.SentStatus
+	if msg.TimestampSet {
+		timestamp = &msg.Timestamp
+	}
+	if msg.RoundIDSet {
+		round = &rounds.Round{ID: msg.RoundID}
+	}
+	if msg.PinnedSet {
+		pinned = &msg.Pinned
+	}
+	if msg.HiddenSet {
+		hidden = &msg.Hidden
+	}
+	if msg.StatusSet {
+		status = &msg.Status
+	}
+
+	uuid := m.model.UpdateFromMessageID(
+		msg.MessageID, timestamp, round, pinned, hidden, status)
+
+	uuidData, err := json.Marshal(uuid)
+	if err != nil {
+		return nil, errors.Errorf("failed to JSON marshal UUID : %+v", err)
+	}
+
+	return uuidData, nil
+}
+
+// getMessageCB is the callback for wasmModel.GetMessage. Returns JSON
+// marshalled channels.GetMessageMessage. If an error occurs, then Error will
+// be set with the error message. Otherwise, Message will be set. Only one field
+// will be set.
+func (m *manager) getMessageCB(data []byte) ([]byte, error) {
+	messageID, err := message.UnmarshalID(data)
+	if err != nil {
+		return nil, errors.Errorf(
+			"failed to JSON unmarshal %T from main thread: %+v", messageID, err)
+	}
+
+	reply := wChannels.GetMessageMessage{}
+
+	msg, err := m.model.GetMessage(messageID)
+	if err != nil {
+		reply.Error = err.Error()
+	} else {
+		reply.Message = msg
+	}
+
+	messageData, err := json.Marshal(reply)
+	if err != nil {
+		return nil, errors.Errorf("failed to JSON marshal %T from main thread "+
+			"for GetMessage reply: %+v", reply, err)
+	}
+	return messageData, nil
+}
+
+// deleteMessageCB is the callback for wasmModel.DeleteMessage. Always returns
+// nil; meaning, no response is supplied (or expected).
+func (m *manager) deleteMessageCB(data []byte) ([]byte, error) {
+	messageID, err := message.UnmarshalID(data)
+	if err != nil {
+		return nil, errors.Errorf(
+			"failed to JSON unmarshal %T from main thread: %+v", messageID, err)
+	}
+
+	err = m.model.DeleteMessage(messageID)
+	if err != nil {
+		return []byte(err.Error()), nil
+	}
+
+	return nil, nil
+}
+
+// muteUserCB is the callback for wasmModel.MuteUser. Always returns nil;
+// meaning, no response is supplied (or expected).
+func (m *manager) muteUserCB(data []byte) ([]byte, error) {
+	var msg wChannels.MuteUserMessage
+	err := json.Unmarshal(data, &msg)
+	if err != nil {
+		return nil, errors.Errorf(
+			"failed to JSON unmarshal %T from main thread: %+v", msg, err)
+	}
+
+	m.model.MuteUser(msg.ChannelID, msg.PubKey, msg.Unmute)
+
+	return nil, nil
+}
diff --git a/indexedDb/impl/channels/channelsIndexedDbWorker.js b/indexedDb/impl/channels/channelsIndexedDbWorker.js
new file mode 100644
index 0000000000000000000000000000000000000000..9e69bdd70eddebc9f82b23d04d823221ad3c1622
--- /dev/null
+++ b/indexedDb/impl/channels/channelsIndexedDbWorker.js
@@ -0,0 +1,17 @@
+////////////////////////////////////////////////////////////////////////////////
+// Copyright © 2022 xx foundation                                             //
+//                                                                            //
+// Use of this source code is governed by a license that can be found in the  //
+// LICENSE file.                                                              //
+////////////////////////////////////////////////////////////////////////////////
+
+importScripts('wasm_exec.js');
+
+const go = new Go();
+const binPath = 'xxdk-channelsIndexedDkWorker.wasm'
+WebAssembly.instantiateStreaming(fetch(binPath), go.importObject).then((result) => {
+    go.run(result.instance);
+    LogLevel(1);
+}).catch((err) => {
+    console.error(err);
+});
\ No newline at end of file
diff --git a/indexedDb/channels/implementation.go b/indexedDb/impl/channels/implementation.go
similarity index 95%
rename from indexedDb/channels/implementation.go
rename to indexedDb/impl/channels/implementation.go
index ee099fbfc1d8b55c9b575ce56ad3e0a8973685dd..9098fd666e3c19d59a7df58546da1ea0165783bb 100644
--- a/indexedDb/channels/implementation.go
+++ b/indexedDb/impl/channels/implementation.go
@@ -7,7 +7,7 @@
 
 //go:build js && wasm
 
-package channels
+package main
 
 import (
 	"crypto/ed25519"
@@ -18,8 +18,6 @@ import (
 	"syscall/js"
 	"time"
 
-	"gitlab.com/elixxir/xxdk-wasm/indexedDb"
-
 	"github.com/hack-pad/go-indexeddb/idb"
 	"github.com/pkg/errors"
 	jww "github.com/spf13/jwalterweatherman"
@@ -29,6 +27,8 @@ import (
 	cryptoBroadcast "gitlab.com/elixxir/crypto/broadcast"
 	cryptoChannel "gitlab.com/elixxir/crypto/channel"
 	"gitlab.com/elixxir/crypto/message"
+	"gitlab.com/elixxir/xxdk-wasm/indexedDb/impl"
+	wChannels "gitlab.com/elixxir/xxdk-wasm/indexedDb/worker/channels"
 	"gitlab.com/elixxir/xxdk-wasm/utils"
 	"gitlab.com/xx_network/primitives/id"
 )
@@ -39,9 +39,9 @@ import (
 type wasmModel struct {
 	db                *idb.Database
 	cipher            cryptoChannel.Cipher
-	receivedMessageCB MessageReceivedCallback
-	deletedMessageCB  DeletedMessageCallback
-	mutedUserCB       MutedUserCallback
+	receivedMessageCB wChannels.MessageReceivedCallback
+	deletedMessageCB  wChannels.DeletedMessageCallback
+	mutedUserCB       wChannels.MutedUserCallback
 	updateMux         sync.Mutex
 }
 
@@ -70,7 +70,7 @@ func (w *wasmModel) JoinChannel(channel *cryptoBroadcast.Channel) {
 		return
 	}
 
-	_, err = indexedDb.Put(w.db, channelsStoreName, channelObj)
+	_, err = impl.Put(w.db, channelsStoreName, channelObj)
 	if err != nil {
 		jww.ERROR.Printf("%+v", errors.WithMessagef(parentErr,
 			"Unable to put Channel: %+v", err))
@@ -82,8 +82,7 @@ func (w *wasmModel) LeaveChannel(channelID *id.ID) {
 	parentErr := errors.New("failed to LeaveChannel")
 
 	// Delete the channel from storage
-	err := indexedDb.Delete(w.db, channelsStoreName,
-		js.ValueOf(channelID.String()))
+	err := impl.Delete(w.db, channelsStoreName, js.ValueOf(channelID.String()))
 	if err != nil {
 		jww.ERROR.Printf("%+v", errors.WithMessagef(parentErr,
 			"Unable to delete Channel: %+v", err))
@@ -129,7 +128,7 @@ func (w *wasmModel) deleteMsgByChannel(channelID *id.ID) error {
 	if err != nil {
 		return errors.WithMessagef(parentErr, "Unable to open Cursor: %+v", err)
 	}
-	ctx, cancel := indexedDb.NewContext()
+	ctx, cancel := impl.NewContext()
 	err = cursorRequest.Iter(ctx,
 		func(cursor *idb.CursorWithValue) error {
 			_, err := cursor.Delete()
@@ -269,7 +268,7 @@ func (w *wasmModel) UpdateFromUUID(uuid uint64, messageID *message.ID,
 	key := js.ValueOf(uuid)
 
 	// Use the key to get the existing Message
-	currentMsg, err := indexedDb.Get(w.db, messageStoreName, key)
+	currentMsg, err := impl.Get(w.db, messageStoreName, key)
 	if err != nil {
 		jww.ERROR.Printf("%+v", errors.WithMessagef(parentErr,
 			"Unable to get message: %+v", err))
@@ -305,7 +304,7 @@ func (w *wasmModel) UpdateFromMessageID(messageID message.ID,
 	defer w.updateMux.Unlock()
 
 	msgIDStr := base64.StdEncoding.EncodeToString(messageID.Marshal())
-	currentMsgObj, err := indexedDb.GetIndex(w.db, messageStoreName,
+	currentMsgObj, err := impl.GetIndex(w.db, messageStoreName,
 		messageStoreMessageIndex, js.ValueOf(msgIDStr))
 	if err != nil {
 		jww.ERROR.Printf("%+v", errors.WithMessagef(parentErr,
@@ -420,7 +419,7 @@ func (w *wasmModel) receiveHelper(
 	}
 
 	// Store message to database
-	result, err := indexedDb.Put(w.db, messageStoreName, messageObj)
+	result, err := impl.Put(w.db, messageStoreName, messageObj)
 	if err != nil && !strings.Contains(err.Error(),
 		"at least one key does not satisfy the uniqueness requirements") {
 		// Only return non-unique constraint errors so that the case
@@ -492,8 +491,8 @@ func (w *wasmModel) GetMessage(
 func (w *wasmModel) DeleteMessage(messageID message.ID) error {
 	msgId := js.ValueOf(base64.StdEncoding.EncodeToString(messageID.Bytes()))
 
-	err := indexedDb.DeleteIndex(w.db, messageStoreName,
-		messageStoreMessageIndex, pkeyName, msgId)
+	err := impl.DeleteIndex(
+		w.db, messageStoreName, messageStoreMessageIndex, pkeyName, msgId)
 	if err != nil {
 		return err
 	}
@@ -512,7 +511,7 @@ func (w *wasmModel) MuteUser(
 // msgIDLookup gets the UUID of the Message with the given messageID.
 func (w *wasmModel) msgIDLookup(messageID message.ID) (*Message, error) {
 	msgIDStr := js.ValueOf(base64.StdEncoding.EncodeToString(messageID.Bytes()))
-	resultObj, err := indexedDb.GetIndex(w.db, messageStoreName,
+	resultObj, err := impl.GetIndex(w.db, messageStoreName,
 		messageStoreMessageIndex, msgIDStr)
 	if err != nil {
 		return nil, err
diff --git a/indexedDb/channels/implementation_test.go b/indexedDb/impl/channels/implementation_test.go
similarity index 85%
rename from indexedDb/channels/implementation_test.go
rename to indexedDb/impl/channels/implementation_test.go
index 19b0f313dd4bc11325150838007dd689b873a031..20bb4ed7aa9041a1339b54b34b234022b97e9933 100644
--- a/indexedDb/channels/implementation_test.go
+++ b/indexedDb/impl/channels/implementation_test.go
@@ -7,29 +7,30 @@
 
 //go:build js && wasm
 
-package channels
+package main
 
 import (
 	"crypto/ed25519"
 	"encoding/json"
 	"fmt"
-	"github.com/hack-pad/go-indexeddb/idb"
-	"gitlab.com/elixxir/crypto/message"
-	"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"
 
+	"github.com/hack-pad/go-indexeddb/idb"
 	jww "github.com/spf13/jwalterweatherman"
+
 	"gitlab.com/elixxir/client/v4/channels"
 	"gitlab.com/elixxir/client/v4/cmix/rounds"
 	cryptoBroadcast "gitlab.com/elixxir/crypto/broadcast"
 	cryptoChannel "gitlab.com/elixxir/crypto/channel"
+	"gitlab.com/elixxir/crypto/message"
+	"gitlab.com/elixxir/xxdk-wasm/indexedDb/impl"
+	"gitlab.com/elixxir/xxdk-wasm/storage"
+	"gitlab.com/xx_network/crypto/csprng"
 	"gitlab.com/xx_network/primitives/id"
+	"gitlab.com/xx_network/primitives/netTime"
 )
 
 func TestMain(m *testing.M) {
@@ -40,6 +41,10 @@ func TestMain(m *testing.M) {
 func dummyReceivedMessageCB(uint64, *id.ID, bool)      {}
 func dummyDeletedMessageCB(message.ID)                 {}
 func dummyMutedUserCB(*id.ID, ed25519.PublicKey, bool) {}
+func dummyStoreDatabaseName(string) error              { return nil }
+func dummyStoreEncryptionStatus(_ string, encryptionStatus bool) (bool, error) {
+	return encryptionStatus, nil
+}
 
 // Happy path, insert message and look it up
 func TestWasmModel_msgIDLookup(t *testing.T) {
@@ -60,9 +65,10 @@ func TestWasmModel_msgIDLookup(t *testing.T) {
 			testMsgId := message.DeriveChannelMessageID(&id.ID{1}, 0, []byte(testString))
 
 			eventModel, err2 := newWASMModel(testString, c,
-				dummyReceivedMessageCB, dummyDeletedMessageCB, dummyMutedUserCB)
+				dummyReceivedMessageCB, dummyDeletedMessageCB, dummyMutedUserCB,
+				dummyStoreDatabaseName, dummyStoreEncryptionStatus)
 			if err2 != nil {
-				t.Fatalf("%+v", err2)
+				t.Fatal(err2)
 			}
 
 			testMsg := buildMessage([]byte(testString), testMsgId.Bytes(), nil,
@@ -70,12 +76,12 @@ func TestWasmModel_msgIDLookup(t *testing.T) {
 				netTime.Now(), time.Second, 0, 0, false, false, channels.Sent)
 			_, err = eventModel.receiveHelper(testMsg, false)
 			if err != nil {
-				t.Fatalf("%+v", err)
+				t.Fatal(err)
 			}
 
 			msg, err2 := eventModel.msgIDLookup(testMsgId)
 			if err2 != nil {
-				t.Fatalf("%+v", err2)
+				t.Fatal(err2)
 			}
 			if msg.ID == 0 {
 				t.Fatalf("Expected to get a UUID!")
@@ -89,10 +95,11 @@ 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,
-		dummyReceivedMessageCB, dummyDeletedMessageCB, dummyMutedUserCB)
+	eventModel, err := newWASMModel(testString, nil, dummyReceivedMessageCB,
+		dummyDeletedMessageCB, dummyMutedUserCB, dummyStoreDatabaseName,
+		dummyStoreEncryptionStatus)
 	if err != nil {
-		t.Fatalf("%+v", err)
+		t.Fatal(err)
 	}
 
 	// Insert a message
@@ -101,13 +108,13 @@ func TestWasmModel_DeleteMessage(t *testing.T) {
 		time.Second, 0, 0, false, false, channels.Sent)
 	_, err = eventModel.receiveHelper(testMsg, false)
 	if err != nil {
-		t.Fatalf("%+v", err)
+		t.Fatal(err)
 	}
 
 	// Check the resulting status
-	results, err := indexedDb.Dump(eventModel.db, messageStoreName)
+	results, err := impl.Dump(eventModel.db, messageStoreName)
 	if err != nil {
-		t.Fatalf("%+v", err)
+		t.Fatal(err)
 	}
 	if len(results) != 1 {
 		t.Fatalf("Expected 1 message to exist")
@@ -116,13 +123,13 @@ func TestWasmModel_DeleteMessage(t *testing.T) {
 	// Delete the message
 	err = eventModel.DeleteMessage(testMsgId)
 	if err != nil {
-		t.Fatalf("%+v", err)
+		t.Fatal(err)
 	}
 
 	// Check the resulting status
-	results, err = indexedDb.Dump(eventModel.db, messageStoreName)
+	results, err = impl.Dump(eventModel.db, messageStoreName)
 	if err != nil {
-		t.Fatalf("%+v", err)
+		t.Fatal(err)
 	}
 	if len(results) != 0 {
 		t.Fatalf("Expected no messages to exist")
@@ -147,24 +154,25 @@ func Test_wasmModel_UpdateSentStatus(t *testing.T) {
 			testMsgId := message.DeriveChannelMessageID(
 				&id.ID{1}, 0, []byte(testString))
 			eventModel, err2 := newWASMModel(testString, c,
-				dummyReceivedMessageCB, dummyDeletedMessageCB, dummyMutedUserCB)
+				dummyReceivedMessageCB, dummyDeletedMessageCB, dummyMutedUserCB,
+				dummyStoreDatabaseName, dummyStoreEncryptionStatus)
 			if err2 != nil {
-				t.Fatalf("%+v", err2)
+				t.Fatal(err)
 			}
 
 			// Store a test message
 			testMsg := buildMessage([]byte(testString), testMsgId.Bytes(), nil,
-				testString, []byte(testString), []byte{8, 6, 7, 5}, 0, 0, netTime.Now(),
-				time.Second, 0, 0, false, false, channels.Sent)
-			uuid, err := eventModel.receiveHelper(testMsg, false)
-			if err != nil {
-				t.Fatalf("%+v", err)
+				testString, []byte(testString), []byte{8, 6, 7, 5}, 0, 0,
+				netTime.Now(), time.Second, 0, 0, false, false, channels.Sent)
+			uuid, err2 := eventModel.receiveHelper(testMsg, false)
+			if err2 != nil {
+				t.Fatal(err2)
 			}
 
 			// Ensure one message is stored
-			results, err := indexedDb.Dump(eventModel.db, messageStoreName)
-			if err != nil {
-				t.Fatalf("%+v", err)
+			results, err2 := impl.Dump(eventModel.db, messageStoreName)
+			if err2 != nil {
+				t.Fatal(err2)
 			}
 			if len(results) != 1 {
 				t.Fatalf("Expected 1 message to exist")
@@ -176,17 +184,17 @@ func Test_wasmModel_UpdateSentStatus(t *testing.T) {
 				uuid, nil, nil, nil, nil, nil, &expectedStatus)
 
 			// Check the resulting status
-			results, err2 = indexedDb.Dump(eventModel.db, messageStoreName)
-			if err2 != nil {
-				t.Fatalf("%+v", err2)
+			results, err = impl.Dump(eventModel.db, messageStoreName)
+			if err != nil {
+				t.Fatal(err)
 			}
 			if len(results) != 1 {
 				t.Fatalf("Expected 1 message to exist")
 			}
 			resultMsg := &Message{}
-			err2 = json.Unmarshal([]byte(results[0]), resultMsg)
-			if err2 != nil {
-				t.Fatalf("%+v", err2)
+			err = json.Unmarshal([]byte(results[0]), resultMsg)
+			if err != nil {
+				t.Fatal(err)
 			}
 			if resultMsg.Status != uint8(expectedStatus) {
 				t.Fatalf("Unexpected Status: %v", resultMsg.Status)
@@ -214,10 +222,11 @@ func Test_wasmModel_JoinChannel_LeaveChannel(t *testing.T) {
 		}
 		t.Run("Test_wasmModel_JoinChannel_LeaveChannel"+cs, func(t *testing.T) {
 			storage.GetLocalStorage().Clear()
-			eventModel, err2 := newWASMModel("test", c,
-				dummyReceivedMessageCB, dummyDeletedMessageCB, dummyMutedUserCB)
+			eventModel, err2 := newWASMModel("test", c, dummyReceivedMessageCB,
+				dummyDeletedMessageCB, dummyMutedUserCB, dummyStoreDatabaseName,
+				dummyStoreEncryptionStatus)
 			if err2 != nil {
-				t.Fatalf("%+v", err2)
+				t.Fatal(err2)
 			}
 
 			testChannel := &cryptoBroadcast.Channel{
@@ -234,17 +243,17 @@ func Test_wasmModel_JoinChannel_LeaveChannel(t *testing.T) {
 			}
 			eventModel.JoinChannel(testChannel)
 			eventModel.JoinChannel(testChannel2)
-			results, err2 := indexedDb.Dump(eventModel.db, channelsStoreName)
+			results, err2 := impl.Dump(eventModel.db, channelsStoreName)
 			if err2 != nil {
-				t.Fatalf("%+v", err2)
+				t.Fatal(err2)
 			}
 			if len(results) != 2 {
 				t.Fatalf("Expected 2 channels to exist")
 			}
 			eventModel.LeaveChannel(testChannel.ReceptionID)
-			results, err2 = indexedDb.Dump(eventModel.db, channelsStoreName)
-			if err2 != nil {
-				t.Fatalf("%+v", err2)
+			results, err = impl.Dump(eventModel.db, channelsStoreName)
+			if err != nil {
+				t.Fatal(err)
 			}
 			if len(results) != 1 {
 				t.Fatalf("Expected 1 channels to exist")
@@ -269,9 +278,10 @@ func Test_wasmModel_UUIDTest(t *testing.T) {
 			storage.GetLocalStorage().Clear()
 			testString := "testHello" + cs
 			eventModel, err2 := newWASMModel(testString, c,
-				dummyReceivedMessageCB, dummyDeletedMessageCB, dummyMutedUserCB)
+				dummyReceivedMessageCB, dummyDeletedMessageCB, dummyMutedUserCB,
+				dummyStoreDatabaseName, dummyStoreEncryptionStatus)
 			if err2 != nil {
-				t.Fatalf("%+v", err2)
+				t.Fatal(err2)
 			}
 
 			uuids := make([]uint64, 10)
@@ -316,9 +326,10 @@ func Test_wasmModel_DuplicateReceives(t *testing.T) {
 			storage.GetLocalStorage().Clear()
 			testString := "testHello"
 			eventModel, err2 := newWASMModel(testString, c,
-				dummyReceivedMessageCB, dummyDeletedMessageCB, dummyMutedUserCB)
+				dummyReceivedMessageCB, dummyDeletedMessageCB, dummyMutedUserCB,
+				dummyStoreDatabaseName, dummyStoreEncryptionStatus)
 			if err2 != nil {
-				t.Fatalf("%+v", err2)
+				t.Fatal(err2)
 			}
 
 			uuids := make([]uint64, 10)
@@ -366,9 +377,10 @@ func Test_wasmModel_deleteMsgByChannel(t *testing.T) {
 			totalMessages := 10
 			expectedMessages := 5
 			eventModel, err2 := newWASMModel(testString, c,
-				dummyReceivedMessageCB, dummyDeletedMessageCB, dummyMutedUserCB)
+				dummyReceivedMessageCB, dummyDeletedMessageCB, dummyMutedUserCB,
+				dummyStoreDatabaseName, dummyStoreEncryptionStatus)
 			if err2 != nil {
-				t.Fatalf("%+v", err2)
+				t.Fatal(err2)
 			}
 
 			// Create a test channel id
@@ -394,9 +406,9 @@ func Test_wasmModel_deleteMsgByChannel(t *testing.T) {
 			}
 
 			// Check pre-results
-			result, err2 := indexedDb.Dump(eventModel.db, messageStoreName)
+			result, err2 := impl.Dump(eventModel.db, messageStoreName)
 			if err2 != nil {
-				t.Fatalf("%+v", err2)
+				t.Fatal(err2)
 			}
 			if len(result) != totalMessages {
 				t.Errorf("Expected %d messages, got %d", totalMessages, len(result))
@@ -409,9 +421,9 @@ func Test_wasmModel_deleteMsgByChannel(t *testing.T) {
 			}
 
 			// Check final results
-			result, err = indexedDb.Dump(eventModel.db, messageStoreName)
+			result, err = impl.Dump(eventModel.db, messageStoreName)
 			if err != nil {
-				t.Fatalf("%+v", err)
+				t.Fatal(err)
 			}
 			if len(result) != expectedMessages {
 				t.Errorf("Expected %d messages, got %d", expectedMessages, len(result))
@@ -437,7 +449,8 @@ func TestWasmModel_receiveHelper_UniqueIndex(t *testing.T) {
 			storage.GetLocalStorage().Clear()
 			testString := fmt.Sprintf("test_receiveHelper_UniqueIndex_%d", i)
 			eventModel, err2 := newWASMModel(testString, c,
-				dummyReceivedMessageCB, dummyDeletedMessageCB, dummyMutedUserCB)
+				dummyReceivedMessageCB, dummyDeletedMessageCB, dummyMutedUserCB,
+				dummyStoreDatabaseName, dummyStoreEncryptionStatus)
 			if err2 != nil {
 				t.Fatal(err2)
 			}
@@ -467,15 +480,15 @@ func TestWasmModel_receiveHelper_UniqueIndex(t *testing.T) {
 			testMsg := buildMessage([]byte(testString), testMsgId.Bytes(), nil,
 				testString, []byte(testString), []byte{8, 6, 7, 5}, 0, 0,
 				netTime.Now(), time.Second, 0, 0, false, false, channels.Sent)
-			uuid, err := eventModel.receiveHelper(testMsg, false)
-			if err != nil {
-				t.Fatal(err)
+			uuid, err2 := eventModel.receiveHelper(testMsg, false)
+			if err2 != nil {
+				t.Fatal(err2)
 			}
 
 			// The duplicate entry should return the same UUID
-			duplicateUuid, err := eventModel.receiveHelper(testMsg, false)
-			if err != nil {
-				t.Fatal(err)
+			duplicateUuid, err2 := eventModel.receiveHelper(testMsg, false)
+			if err2 != nil {
+				t.Fatal(err2)
 			}
 			if uuid != duplicateUuid {
 				t.Fatalf("Expected UUID %d to match %d", uuid, duplicateUuid)
@@ -487,9 +500,9 @@ func TestWasmModel_receiveHelper_UniqueIndex(t *testing.T) {
 			testMsg = buildMessage([]byte(testString), testMsgId2.Bytes(), nil,
 				testString, []byte(testString), []byte{8, 6, 7, 5}, 0, 0,
 				netTime.Now(), time.Second, 0, 0, false, false, channels.Sent)
-			uuid2, err := eventModel.receiveHelper(testMsg, false)
-			if err != nil {
-				t.Fatal(err)
+			uuid2, err2 := eventModel.receiveHelper(testMsg, false)
+			if err2 != nil {
+				t.Fatal(err2)
 			}
 			if uuid2 == uuid {
 				t.Fatalf("Expected UUID %d to NOT match %d", uuid, duplicateUuid)
@@ -499,9 +512,9 @@ func TestWasmModel_receiveHelper_UniqueIndex(t *testing.T) {
 			// message ID as the first
 			testMsg.ID = uuid
 			testMsg.MessageID = testMsgId.Bytes()
-			duplicateUuid2, err := eventModel.receiveHelper(testMsg, true)
-			if err != nil {
-				t.Fatal(err)
+			duplicateUuid2, err2 := eventModel.receiveHelper(testMsg, true)
+			if err2 != nil {
+				t.Fatal(err2)
 			}
 			if duplicateUuid2 != duplicateUuid {
 				t.Fatalf("Expected UUID %d to match %d", uuid, duplicateUuid)
diff --git a/indexedDb/channels/init.go b/indexedDb/impl/channels/init.go
similarity index 56%
rename from indexedDb/channels/init.go
rename to indexedDb/impl/channels/init.go
index 54867ea9d4668195b6bb97e7341f0366b4efcecd..45107fa2c437b82bce04a50fe71eebeaac0625ee 100644
--- a/indexedDb/channels/init.go
+++ b/indexedDb/impl/channels/init.go
@@ -7,20 +7,19 @@
 
 //go:build js && wasm
 
-package channels
+package main
 
 import (
-	"crypto/ed25519"
+	"syscall/js"
+
 	"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"
-	"syscall/js"
+	"gitlab.com/elixxir/xxdk-wasm/indexedDb/impl"
+	wChannels "gitlab.com/elixxir/xxdk-wasm/indexedDb/worker/channels"
 )
 
 const (
@@ -33,57 +32,42 @@ const (
 	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, 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,
-	messageReceivedCB MessageReceivedCallback,
-	deletedMessageCB DeletedMessageCallback,
-	mutedUserCB MutedUserCallback) channels.EventModelBuilder {
-	fn := func(path string) (channels.EventModel, error) {
-		return NewWASMEventModel(
-			path, encryption, messageReceivedCB, deletedMessageCB, mutedUserCB)
-	}
-	return fn
-}
+// storeDatabaseNameFn matches storage.StoreIndexedDb so that the data can be
+// sent between the worker and main thread.
+type storeDatabaseNameFn func(databaseName string) error
+
+// storeEncryptionStatusFn matches storage.StoreIndexedDbEncryptionStatus so
+// that the data can be sent between the worker and main thread.
+type storeEncryptionStatusFn func(
+	databaseName string, encryptionStatus bool) (bool, error)
 
 // 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,
-	messageReceivedCB MessageReceivedCallback,
-	deletedMessageCB DeletedMessageCallback,
-	mutedUserCB MutedUserCallback) (channels.EventModel, error) {
+	messageReceivedCB wChannels.MessageReceivedCallback,
+	deletedMessageCB wChannels.DeletedMessageCallback,
+	mutedUserCB wChannels.MutedUserCallback,
+	storeDatabaseName storeDatabaseNameFn,
+	storeEncryptionStatus storeEncryptionStatusFn) (channels.EventModel, error) {
 	databaseName := path + databaseSuffix
-	return newWASMModel(databaseName,
-		encryption, messageReceivedCB, deletedMessageCB, mutedUserCB)
+	return newWASMModel(databaseName, encryption, messageReceivedCB,
+		deletedMessageCB, mutedUserCB, storeDatabaseName, storeEncryptionStatus)
 }
 
 // newWASMModel creates the given [idb.Database] and returns a wasmModel.
 func newWASMModel(databaseName string, encryption cryptoChannel.Cipher,
-	messageReceivedCB MessageReceivedCallback,
-	deletedMessageCB DeletedMessageCallback,
-	mutedUserCB MutedUserCallback) (*wasmModel, error) {
+	messageReceivedCB wChannels.MessageReceivedCallback,
+	deletedMessageCB wChannels.DeletedMessageCallback,
+	mutedUserCB wChannels.MutedUserCallback,
+	storeDatabaseName storeDatabaseNameFn,
+	storeEncryptionStatus storeEncryptionStatusFn) (*wasmModel, error) {
 	// Attempt to open database object
-	ctx, cancel := indexedDb.NewContext()
+	ctx, cancel := impl.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)
+				jww.INFO.Printf("IndexDb version is current: v%d", newVersion)
 				return nil
 			}
 
@@ -111,10 +95,17 @@ func newWASMModel(databaseName string, encryption cryptoChannel.Cipher,
 		return nil, err
 	}
 
+	// Get the database name and save it to storage
+	if dbName, err2 := db.Name(); err2 != nil {
+		return nil, err2
+	} else if err = storeDatabaseName(dbName); err != nil {
+		return nil, err
+	}
+
 	// Save the encryption status to storage
 	encryptionStatus := encryption != nil
-	loadedEncryptionStatus, err := storage.StoreIndexedDbEncryptionStatus(
-		databaseName, encryptionStatus)
+	loadedEncryptionStatus, err :=
+		storeEncryptionStatus(databaseName, encryptionStatus)
 	if err != nil {
 		return nil, err
 	}
@@ -127,19 +118,6 @@ func newWASMModel(databaseName string, encryption cryptoChannel.Cipher,
 		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,
 		cipher:            encryption,
@@ -206,37 +184,5 @@ func v1Upgrade(db *idb.Database) error {
 		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
-}
-
-// hackTestDb is a horrible function that exists as the result of an extremely
-// long discussion about why initializing the IndexedDb sometimes silently
-// fails. It ultimately tries to prevent an unrecoverable situation by actually
-// inserting some nonsense data and then checking to see if it persists.
-// If this function still exists in 2023, god help us all. Amen.
-func (w *wasmModel) hackTestDb() error {
-	testMessage := &Message{
-		ID:        0,
-		Nickname:  "test",
-		MessageID: id.DummyUser.Marshal(),
-	}
-	msgId, helper := w.receiveHelper(testMessage, false)
-	if helper != nil {
-		return helper
-	}
-	result, err := indexedDb.Get(w.db, messageStoreName, js.ValueOf(msgId))
-	if err != nil {
-		return err
-	}
-	if result.IsUndefined() {
-		return errors.Errorf("Failed to test db, record not present")
-	}
 	return nil
 }
diff --git a/indexedDb/impl/channels/main.go b/indexedDb/impl/channels/main.go
new file mode 100644
index 0000000000000000000000000000000000000000..5290d0c89b6eedf92a1571aea04ff5b7fbfdc668
--- /dev/null
+++ b/indexedDb/impl/channels/main.go
@@ -0,0 +1,42 @@
+////////////////////////////////////////////////////////////////////////////////
+// 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 main
+
+import (
+	"fmt"
+	jww "github.com/spf13/jwalterweatherman"
+	"gitlab.com/elixxir/xxdk-wasm/logging"
+	"gitlab.com/elixxir/xxdk-wasm/wasm"
+	"gitlab.com/elixxir/xxdk-wasm/worker"
+	"syscall/js"
+)
+
+// SEMVER is the current semantic version of the xxDK channels web worker.
+const SEMVER = "0.1.0"
+
+func init() {
+	// Set up Javascript console listener set at level INFO
+	ll := logging.NewJsConsoleLogListener(jww.LevelInfo)
+	logging.AddLogListener(ll.Listen)
+	jww.SetStdoutThreshold(jww.LevelFatal + 1)
+	jww.INFO.Printf("xxDK channels web worker version: v%s", SEMVER)
+}
+
+func main() {
+	jww.INFO.Print("[WW] Starting xxDK WebAssembly Channels Database Worker.")
+
+	js.Global().Set("LogLevel", js.FuncOf(wasm.LogLevel))
+
+	m := &manager{mh: worker.NewThreadManager("ChannelsIndexedDbWorker", true)}
+	m.registerCallbacks()
+	m.mh.SignalReady()
+	<-make(chan bool)
+	fmt.Println("[WW] Closing xxDK WebAssembly Channels Database Worker.")
+}
diff --git a/indexedDb/channels/model.go b/indexedDb/impl/channels/model.go
similarity index 99%
rename from indexedDb/channels/model.go
rename to indexedDb/impl/channels/model.go
index 078d6bd67f7c43d9e50373b7801f69937cfce2ac..d90d5d0ff932c922634cdc49276074173631aea1 100644
--- a/indexedDb/channels/model.go
+++ b/indexedDb/impl/channels/model.go
@@ -7,7 +7,7 @@
 
 //go:build js && wasm
 
-package channels
+package main
 
 import (
 	"time"
diff --git a/indexedDb/impl/dm/callbacks.go b/indexedDb/impl/dm/callbacks.go
new file mode 100644
index 0000000000000000000000000000000000000000..e7ec65bc4b921a220b489d13aa8d4f893470f753
--- /dev/null
+++ b/indexedDb/impl/dm/callbacks.go
@@ -0,0 +1,279 @@
+////////////////////////////////////////////////////////////////////////////////
+// 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 main
+
+import (
+	"crypto/ed25519"
+	"encoding/json"
+	"github.com/pkg/errors"
+	jww "github.com/spf13/jwalterweatherman"
+	"gitlab.com/elixxir/client/v4/dm"
+	cryptoChannel "gitlab.com/elixxir/crypto/channel"
+	"gitlab.com/elixxir/crypto/fastRNG"
+	wDm "gitlab.com/elixxir/xxdk-wasm/indexedDb/worker/dm"
+	"gitlab.com/elixxir/xxdk-wasm/worker"
+	"gitlab.com/xx_network/crypto/csprng"
+	"time"
+)
+
+var zeroUUID = []byte{0, 0, 0, 0, 0, 0, 0, 0}
+
+// manager handles the event model and the message callbacks, which is used to
+// send information between the event model and the main thread.
+type manager struct {
+	mh    *worker.ThreadManager
+	model dm.EventModel
+}
+
+// registerCallbacks registers all the reception callbacks to manage messages
+// from the main thread for the channels.EventModel.
+func (m *manager) registerCallbacks() {
+	m.mh.RegisterCallback(wDm.NewWASMEventModelTag, m.newWASMEventModelCB)
+	m.mh.RegisterCallback(wDm.ReceiveTag, m.receiveCB)
+	m.mh.RegisterCallback(wDm.ReceiveTextTag, m.receiveTextCB)
+	m.mh.RegisterCallback(wDm.ReceiveReplyTag, m.receiveReplyCB)
+	m.mh.RegisterCallback(wDm.ReceiveReactionTag, m.receiveReactionCB)
+	m.mh.RegisterCallback(wDm.UpdateSentStatusTag, m.updateSentStatusCB)
+}
+
+// newWASMEventModelCB is the callback for NewWASMEventModel. Returns an empty
+// slice on success or an error message on failure.
+func (m *manager) newWASMEventModelCB(data []byte) ([]byte, error) {
+	var msg wDm.NewWASMEventModelMessage
+	err := json.Unmarshal(data, &msg)
+	if err != nil {
+		return []byte{}, errors.Errorf(
+			"failed to JSON unmarshal %T from main thread: %+v", msg, err)
+	}
+
+	// Create new encryption cipher
+	rng := fastRNG.NewStreamGenerator(12, 1024, csprng.NewSystemRNG)
+	encryption, err := cryptoChannel.NewCipherFromJSON(
+		[]byte(msg.EncryptionJSON), rng.GetStream())
+	if err != nil {
+		return []byte{}, errors.Errorf("failed to JSON unmarshal channel "+
+			"cipher from main thread: %+v", err)
+	}
+
+	m.model, err = NewWASMEventModel(msg.Path, encryption,
+		m.messageReceivedCallback, m.storeDatabaseName, m.storeEncryptionStatus)
+	if err != nil {
+		return []byte(err.Error()), nil
+	}
+	return []byte{}, nil
+}
+
+// messageReceivedCallback sends calls to the MessageReceivedCallback in the
+// main thread.
+//
+// messageReceivedCallback adhere to the MessageReceivedCallback type.
+func (m *manager) messageReceivedCallback(
+	uuid uint64, pubKey ed25519.PublicKey, update bool) {
+	// Package parameters for sending
+	msg := &wDm.MessageReceivedCallbackMessage{
+		UUID:   uuid,
+		PubKey: pubKey,
+		Update: update,
+	}
+	data, err := json.Marshal(msg)
+	if err != nil {
+		jww.ERROR.Printf(
+			"Could not JSON marshal MessageReceivedCallbackMessage: %+v", err)
+		return
+	}
+
+	// Send it to the main thread
+	m.mh.SendMessage(wDm.MessageReceivedCallbackTag, data)
+}
+
+// storeDatabaseName sends the database name to the main thread and waits for
+// the response. This function mocks the behavior of storage.StoreIndexedDb.
+//
+// storeDatabaseName adheres to the storeDatabaseNameFn type.
+func (m *manager) storeDatabaseName(databaseName string) error {
+	// Register response callback with channel that will wait for the response
+	responseChan := make(chan []byte)
+	m.mh.RegisterCallback(wDm.StoreDatabaseNameTag,
+		func(data []byte) ([]byte, error) {
+			responseChan <- data
+			return nil, nil
+		})
+
+	// Send encryption status to main thread
+	m.mh.SendMessage(wDm.StoreDatabaseNameTag, []byte(databaseName))
+
+	// Wait for response
+	select {
+	case response := <-responseChan:
+		if len(response) > 0 {
+			return errors.New(string(response))
+		}
+	case <-time.After(worker.ResponseTimeout):
+		return errors.Errorf("[WW] Timed out after %s waiting for response "+
+			"about storing the database name in local storage in the main "+
+			"thread", worker.ResponseTimeout)
+	}
+
+	return nil
+}
+
+// storeEncryptionStatus sends the database name and encryption status to the
+// main thread and waits for the response. If the value has not been previously
+// saved, it returns the saves encryption status. This function mocks the
+// behavior of storage.StoreIndexedDbEncryptionStatus.
+//
+// storeEncryptionStatus adheres to the storeEncryptionStatusFn type.
+func (m *manager) storeEncryptionStatus(
+	databaseName string, encryption bool) (bool, error) {
+	// Package parameters for sending
+	msg := &wDm.EncryptionStatusMessage{
+		DatabaseName:     databaseName,
+		EncryptionStatus: encryption,
+	}
+	data, err := json.Marshal(msg)
+	if err != nil {
+		return false, err
+	}
+
+	// Register response callback with channel that will wait for the response
+	responseChan := make(chan []byte)
+	m.mh.RegisterCallback(wDm.EncryptionStatusTag,
+		func(data []byte) ([]byte, error) {
+			responseChan <- data
+			return nil, nil
+		})
+
+	// Send encryption status to main thread
+	m.mh.SendMessage(wDm.EncryptionStatusTag, data)
+
+	// Wait for response
+	var response wDm.EncryptionStatusReply
+	select {
+	case responseData := <-responseChan:
+		if err = json.Unmarshal(responseData, &response); err != nil {
+			return false, err
+		}
+	case <-time.After(worker.ResponseTimeout):
+		return false, errors.Errorf("timed out after %s waiting for "+
+			"response about the database encryption status from local "+
+			"storage in the main thread", worker.ResponseTimeout)
+	}
+
+	// If the response contain an error, return it
+	if response.Error != "" {
+		return false, errors.New(response.Error)
+	}
+
+	// Return the encryption status
+	return response.EncryptionStatus, nil
+}
+
+// receiveCB is the callback for wasmModel.Receive. Returns a UUID of 0 on error
+// or the JSON marshalled UUID (uint64) on success.
+func (m *manager) receiveCB(data []byte) ([]byte, error) {
+	var msg wDm.TransferMessage
+	err := json.Unmarshal(data, &msg)
+	if err != nil {
+		return zeroUUID, errors.Errorf(
+			"failed to JSON unmarshal %T from main thread: %+v", msg, err)
+	}
+
+	uuid := m.model.Receive(
+		msg.MessageID, msg.Nickname, msg.Text, msg.PubKey, msg.DmToken,
+		msg.Codeset, msg.Timestamp, msg.Round, msg.MType, msg.Status)
+
+	uuidData, err := json.Marshal(uuid)
+	if err != nil {
+		return zeroUUID, errors.Errorf("failed to JSON marshal UUID : %+v", err)
+	}
+	return uuidData, nil
+}
+
+// receiveTextCB is the callback for wasmModel.ReceiveText. Returns a UUID of 0
+// on error or the JSON marshalled UUID (uint64) on success.
+func (m *manager) receiveTextCB(data []byte) ([]byte, error) {
+	var msg wDm.TransferMessage
+	err := json.Unmarshal(data, &msg)
+	if err != nil {
+		return []byte{}, errors.Errorf(
+			"failed to JSON unmarshal %T from main thread: %+v", msg, err)
+	}
+
+	uuid := m.model.ReceiveText(
+		msg.MessageID, msg.Nickname, string(msg.Text), msg.PubKey, msg.DmToken,
+		msg.Codeset, msg.Timestamp, msg.Round, msg.Status)
+
+	uuidData, err := json.Marshal(uuid)
+	if err != nil {
+		return []byte{}, errors.Errorf("failed to JSON marshal UUID : %+v", err)
+	}
+
+	return uuidData, nil
+}
+
+// receiveReplyCB is the callback for wasmModel.ReceiveReply. Returns a UUID of
+// 0 on error or the JSON marshalled UUID (uint64) on success.
+func (m *manager) receiveReplyCB(data []byte) ([]byte, error) {
+	var msg wDm.TransferMessage
+	err := json.Unmarshal(data, &msg)
+	if err != nil {
+		return zeroUUID, errors.Errorf(
+			"failed to JSON unmarshal %T from main thread: %+v", msg, err)
+	}
+
+	uuid := m.model.ReceiveReply(msg.MessageID, msg.ReactionTo, msg.Nickname,
+		string(msg.Text), msg.PubKey, msg.DmToken, msg.Codeset, msg.Timestamp,
+		msg.Round, msg.Status)
+
+	uuidData, err := json.Marshal(uuid)
+	if err != nil {
+		return zeroUUID, errors.Errorf("failed to JSON marshal UUID : %+v", err)
+	}
+
+	return uuidData, nil
+}
+
+// receiveReactionCB is the callback for wasmModel.ReceiveReaction. Returns a
+// UUID of 0 on error or the JSON marshalled UUID (uint64) on success.
+func (m *manager) receiveReactionCB(data []byte) ([]byte, error) {
+	var msg wDm.TransferMessage
+	err := json.Unmarshal(data, &msg)
+	if err != nil {
+		return zeroUUID, errors.Errorf(
+			"failed to JSON unmarshal %T from main thread: %+v", msg, err)
+	}
+
+	uuid := m.model.ReceiveReaction(msg.MessageID, msg.ReactionTo, msg.Nickname,
+		string(msg.Text), msg.PubKey, msg.DmToken, msg.Codeset, msg.Timestamp,
+		msg.Round, msg.Status)
+
+	uuidData, err := json.Marshal(uuid)
+	if err != nil {
+		return zeroUUID, errors.Errorf("failed to JSON marshal UUID : %+v", err)
+	}
+
+	return uuidData, nil
+}
+
+// updateSentStatusCB is the callback for wasmModel.UpdateSentStatus. Always
+// returns nil; meaning, no response is supplied (or expected).
+func (m *manager) updateSentStatusCB(data []byte) ([]byte, error) {
+	var msg wDm.TransferMessage
+	err := json.Unmarshal(data, &msg)
+	if err != nil {
+		return nil, errors.Errorf(
+			"failed to JSON unmarshal %T from main thread: %+v", msg, err)
+	}
+
+	m.model.UpdateSentStatus(
+		msg.UUID, msg.MessageID, msg.Timestamp, msg.Round, msg.Status)
+
+	return nil, nil
+}
diff --git a/indexedDb/impl/dm/dmIndexedDbWorker.js b/indexedDb/impl/dm/dmIndexedDbWorker.js
new file mode 100644
index 0000000000000000000000000000000000000000..e199a7bb812b9ff119b7f130f41d3bb555247302
--- /dev/null
+++ b/indexedDb/impl/dm/dmIndexedDbWorker.js
@@ -0,0 +1,17 @@
+////////////////////////////////////////////////////////////////////////////////
+// Copyright © 2022 xx foundation                                             //
+//                                                                            //
+// Use of this source code is governed by a license that can be found in the  //
+// LICENSE file.                                                              //
+////////////////////////////////////////////////////////////////////////////////
+
+importScripts('wasm_exec.js');
+
+const go = new Go();
+const binPath = 'xxdk-dmIndexedDkWorker.wasm'
+WebAssembly.instantiateStreaming(fetch(binPath), go.importObject).then((result) => {
+    go.run(result.instance);
+    LogLevel(1);
+}).catch((err) => {
+    console.error(err);
+});
\ No newline at end of file
diff --git a/indexedDb/dm/implementation.go b/indexedDb/impl/dm/implementation.go
similarity index 85%
rename from indexedDb/dm/implementation.go
rename to indexedDb/impl/dm/implementation.go
index b5c023d8f2ad920783533527608ce36d9d8c0ea9..8280d7b5138c00eab29b150ecc1fd749b6d60b13 100644
--- a/indexedDb/dm/implementation.go
+++ b/indexedDb/impl/dm/implementation.go
@@ -7,7 +7,7 @@
 
 //go:build js && wasm
 
-package channelEventModel
+package main
 
 import (
 	"crypto/ed25519"
@@ -21,18 +21,17 @@ import (
 	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"
 
 	"github.com/hack-pad/go-indexeddb/idb"
 	cryptoChannel "gitlab.com/elixxir/crypto/channel"
 	"gitlab.com/elixxir/crypto/message"
+	"gitlab.com/elixxir/xxdk-wasm/indexedDb/impl"
 )
 
-// wasmModel implements [dm.Receiver] interface, which uses the channels
-// system passed an object that adheres to in order to get events on the
-// channel.
+// wasmModel implements dm.EventModel 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
@@ -66,7 +65,7 @@ func (w *wasmModel) joinConversation(nickname string,
 			"Unable to marshal Conversation: %+v", err)
 	}
 
-	_, err = indexedDb.Put(w.db, conversationStoreName, convoObj)
+	_, err = impl.Put(w.db, conversationStoreName, convoObj)
 	if err != nil {
 		return errors.WithMessagef(parentErr,
 			"Unable to put Conversation: %+v", err)
@@ -74,15 +73,15 @@ func (w *wasmModel) joinConversation(nickname string,
 	return nil
 }
 
-// buildMessage is a private helper that converts typical [dm.Receiver]
-// inputs into a basic Message structure for insertion into storage.
+// buildMessage is a private helper that converts typical dm.EventModel 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 {
+func buildMessage(messageID, parentID, text []byte, pubKey ed25519.PublicKey,
+	timestamp time.Time, round id.Round, mType dm.MessageType,
+	status dm.Status) *Message {
 	return &Message{
 		MessageID:          messageID,
 		ConversationPubKey: pubKey,
@@ -96,16 +95,14 @@ func buildMessage(messageID, parentID []byte, text []byte,
 }
 
 func (w *wasmModel) Receive(messageID message.ID, nickname string, text []byte,
-	pubKey ed25519.PublicKey, dmToken uint32, codeset uint8,
-	timestamp time.Time, round rounds.Round, mType dm.MessageType,
-	status dm.Status) uint64 {
+	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")
 
 	// If there is no extant Conversation, create one.
-	_, err := indexedDb.Get(
-		w.db, conversationStoreName, utils.CopyBytesToJS(pubKey))
+	_, err := impl.Get(w.db, conversationStoreName, utils.CopyBytesToJS(pubKey))
 	if err != nil {
-		if strings.Contains(err.Error(), indexedDb.ErrDoesNotExist) {
+		if strings.Contains(err.Error(), impl.ErrDoesNotExist) {
 			err = w.joinConversation(nickname, pubKey, dmToken, codeset)
 			if err != nil {
 				jww.ERROR.Printf("%+v", err)
@@ -128,8 +125,8 @@ func (w *wasmModel) Receive(messageID message.ID, nickname string, text []byte,
 		}
 	}
 
-	msgToInsert := buildMessage(messageID.Bytes(), nil, text,
-		pubKey, timestamp, round.ID, mType, status)
+	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)
@@ -145,10 +142,9 @@ 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 := impl.Get(w.db, conversationStoreName, utils.CopyBytesToJS(pubKey))
 	if err != nil {
-		if strings.Contains(err.Error(), indexedDb.ErrDoesNotExist) {
+		if strings.Contains(err.Error(), impl.ErrDoesNotExist) {
 			err = w.joinConversation(nickname, pubKey, dmToken, codeset)
 			if err != nil {
 				jww.ERROR.Printf("%+v", err)
@@ -190,10 +186,9 @@ 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 := impl.Get(w.db, conversationStoreName, utils.CopyBytesToJS(pubKey))
 	if err != nil {
-		if strings.Contains(err.Error(), indexedDb.ErrDoesNotExist) {
+		if strings.Contains(err.Error(), impl.ErrDoesNotExist) {
 			err = w.joinConversation(nickname, pubKey, dmToken, codeset)
 			if err != nil {
 				jww.ERROR.Printf("%+v", err)
@@ -235,10 +230,9 @@ func (w *wasmModel) ReceiveReaction(messageID, _ message.ID, nickname,
 	parentErr := errors.New("failed to ReceiveText")
 
 	// If there is no extant Conversation, create one.
-	_, err := indexedDb.Get(
-		w.db, conversationStoreName, utils.CopyBytesToJS(pubKey))
+	_, err := impl.Get(w.db, conversationStoreName, utils.CopyBytesToJS(pubKey))
 	if err != nil {
-		if strings.Contains(err.Error(), indexedDb.ErrDoesNotExist) {
+		if strings.Contains(err.Error(), impl.ErrDoesNotExist) {
 			err = w.joinConversation(nickname, pubKey, dmToken, codeset)
 			if err != nil {
 				jww.ERROR.Printf("%+v", err)
@@ -288,7 +282,7 @@ func (w *wasmModel) UpdateSentStatus(uuid uint64, messageID message.ID,
 	key := js.ValueOf(uuid)
 
 	// Use the key to get the existing Message
-	currentMsg, err := indexedDb.Get(w.db, messageStoreName, key)
+	currentMsg, err := impl.Get(w.db, messageStoreName, key)
 	if err != nil {
 		jww.ERROR.Printf("%+v", errors.WithMessagef(parentErr,
 			"Unable to get message: %+v", err))
@@ -345,7 +339,7 @@ func (w *wasmModel) receiveHelper(
 	}
 
 	// Store message to database
-	result, err := indexedDb.Put(w.db, messageStoreName, messageObj)
+	result, err := impl.Put(w.db, messageStoreName, messageObj)
 	if err != nil {
 		return 0, errors.Errorf("Unable to put Message: %+v", err)
 	}
@@ -369,7 +363,7 @@ func (w *wasmModel) receiveHelper(
 
 // msgIDLookup gets the UUID of the Message with the given messageID.
 func (w *wasmModel) msgIDLookup(messageID message.ID) (uint64, error) {
-	resultObj, err := indexedDb.GetIndex(w.db, messageStoreName,
+	resultObj, err := impl.GetIndex(w.db, messageStoreName,
 		messageStoreMessageIndex, utils.CopyBytesToJS(messageID.Marshal()))
 	if err != nil {
 		return 0, err
diff --git a/indexedDb/dm/init.go b/indexedDb/impl/dm/init.go
similarity index 78%
rename from indexedDb/dm/init.go
rename to indexedDb/impl/dm/init.go
index 29b80911804004539beb210dc85533afc7c98f31..ec952044166a19f31fd13cec48f270a801bd8013 100644
--- a/indexedDb/dm/init.go
+++ b/indexedDb/impl/dm/init.go
@@ -7,10 +7,11 @@
 
 //go:build js && wasm
 
-package channelEventModel
+package main
 
 import (
 	"crypto/ed25519"
+	"gitlab.com/elixxir/xxdk-wasm/indexedDb/impl"
 	"syscall/js"
 
 	"github.com/hack-pad/go-indexeddb/idb"
@@ -18,8 +19,6 @@ import (
 	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"
 )
 
 const (
@@ -38,25 +37,36 @@ const (
 type MessageReceivedCallback func(
 	uuid uint64, pubKey ed25519.PublicKey, update bool)
 
+// storeDatabaseNameFn matches storage.StoreIndexedDb so that the data can be
+// sent between the worker and main thread.
+type storeDatabaseNameFn func(databaseName string) error
+
+// storeEncryptionStatusFn matches storage.StoreIndexedDbEncryptionStatus so
+// that the data can be sent between the worker and main thread.
+type storeEncryptionStatusFn func(
+	databaseName string, encryptionStatus bool) (bool, error)
+
 // 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.EventModel, error) {
+	cb MessageReceivedCallback, storeDatabaseName storeDatabaseNameFn,
+	storeEncryptionStatus storeEncryptionStatusFn) (dm.EventModel, error) {
 	databaseName := path + databaseSuffix
-	return newWASMModel(databaseName, encryption, cb)
+	return newWASMModel(
+		databaseName, encryption, cb, storeDatabaseName, storeEncryptionStatus)
 }
 
 // newWASMModel creates the given [idb.Database] and returns a wasmModel.
 func newWASMModel(databaseName string, encryption cryptoChannel.Cipher,
-	cb MessageReceivedCallback) (*wasmModel, error) {
+	cb MessageReceivedCallback, storeDatabaseName storeDatabaseNameFn,
+	storeEncryptionStatus storeEncryptionStatusFn) (*wasmModel, error) {
 	// Attempt to open database object
-	ctx, cancel := indexedDb.NewContext()
+	ctx, cancel := impl.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)
+				jww.INFO.Printf("IndexDb version is current: v%d", newVersion)
 				return nil
 			}
 
@@ -84,10 +94,17 @@ func newWASMModel(databaseName string, encryption cryptoChannel.Cipher,
 		return nil, err
 	}
 
+	// Get the database name and save it to storage
+	if dbName, err2 := db.Name(); err2 != nil {
+		return nil, err2
+	} else if err = storeDatabaseName(dbName); err != nil {
+		return nil, err
+	}
+
 	// Save the encryption status to storage
 	encryptionStatus := encryption != nil
-	loadedEncryptionStatus, err := storage.StoreIndexedDbEncryptionStatus(
-		databaseName, encryptionStatus)
+	loadedEncryptionStatus, err :=
+		storeEncryptionStatus(databaseName, encryptionStatus)
 	if err != nil {
 		return nil, err
 	}
@@ -100,19 +117,6 @@ func newWASMModel(databaseName string, encryption cryptoChannel.Cipher,
 		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
@@ -172,12 +176,5 @@ func v1Upgrade(db *idb.Database) error {
 		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/impl/dm/main.go b/indexedDb/impl/dm/main.go
new file mode 100644
index 0000000000000000000000000000000000000000..20b20a0856c78ac753798c9fd4a692e4a88e4852
--- /dev/null
+++ b/indexedDb/impl/dm/main.go
@@ -0,0 +1,42 @@
+////////////////////////////////////////////////////////////////////////////////
+// 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 main
+
+import (
+	"fmt"
+	jww "github.com/spf13/jwalterweatherman"
+	"gitlab.com/elixxir/xxdk-wasm/logging"
+	"gitlab.com/elixxir/xxdk-wasm/wasm"
+	"gitlab.com/elixxir/xxdk-wasm/worker"
+	"syscall/js"
+)
+
+// SEMVER is the current semantic version of the xxDK DM web worker.
+const SEMVER = "0.1.0"
+
+func init() {
+	// Set up Javascript console listener set at level INFO
+	ll := logging.NewJsConsoleLogListener(jww.LevelInfo)
+	logging.AddLogListener(ll.Listen)
+	jww.SetStdoutThreshold(jww.LevelFatal + 1)
+	jww.INFO.Printf("xxDK DM web worker version: v%s", SEMVER)
+}
+
+func main() {
+	jww.INFO.Print("[WW] Starting xxDK WebAssembly DM Database Worker.")
+
+	js.Global().Set("LogLevel", js.FuncOf(wasm.LogLevel))
+
+	m := &manager{mh: worker.NewThreadManager("DmIndexedDbWorker", true)}
+	m.registerCallbacks()
+	m.mh.SignalReady()
+	<-make(chan bool)
+	fmt.Println("[WW] Closing xxDK WebAssembly Channels Database Worker.")
+}
diff --git a/indexedDb/dm/model.go b/indexedDb/impl/dm/model.go
similarity index 98%
rename from indexedDb/dm/model.go
rename to indexedDb/impl/dm/model.go
index bb6f34588aa2976c1689b629c6df31219de6b585..9b0b99e85bd5c97c389a1ac015819026b1d46b94 100644
--- a/indexedDb/dm/model.go
+++ b/indexedDb/impl/dm/model.go
@@ -7,7 +7,7 @@
 
 //go:build js && wasm
 
-package channelEventModel
+package main
 
 import (
 	"time"
diff --git a/indexedDb/utils.go b/indexedDb/impl/utils.go
similarity index 99%
rename from indexedDb/utils.go
rename to indexedDb/impl/utils.go
index a5bef78685ef34452aca396e1aea3f5206cf4ab6..b68746ebd6eb101d8f077b6179f1f6d41bfb04a6 100644
--- a/indexedDb/utils.go
+++ b/indexedDb/impl/utils.go
@@ -10,7 +10,7 @@
 // This file contains several generic IndexedDB helper functions that
 // may be useful for any IndexedDB implementations.
 
-package indexedDb
+package impl
 
 import (
 	"context"
@@ -26,6 +26,7 @@ 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"
 )
diff --git a/indexedDb/utils_test.go b/indexedDb/impl/utils_test.go
similarity index 99%
rename from indexedDb/utils_test.go
rename to indexedDb/impl/utils_test.go
index 1c657ebd5dbb4607c977323dc8883042989f05f9..6b6da6ff36ee8be5412146443093ee7d85c07c4f 100644
--- a/indexedDb/utils_test.go
+++ b/indexedDb/impl/utils_test.go
@@ -7,7 +7,7 @@
 
 //go:build js && wasm
 
-package indexedDb
+package impl
 
 import (
 	"github.com/hack-pad/go-indexeddb/idb"
diff --git a/indexedDb/worker/channels/implementation.go b/indexedDb/worker/channels/implementation.go
new file mode 100644
index 0000000000000000000000000000000000000000..4d51b97e6a2941e07849c0e2e405539ccc86a8f7
--- /dev/null
+++ b/indexedDb/worker/channels/implementation.go
@@ -0,0 +1,451 @@
+////////////////////////////////////////////////////////////////////////////////
+// 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 (
+	"crypto/ed25519"
+	"encoding/json"
+	"time"
+
+	"github.com/pkg/errors"
+	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/message"
+	"gitlab.com/elixxir/xxdk-wasm/worker"
+	"gitlab.com/xx_network/primitives/id"
+)
+
+// wasmModel implements [channels.EventModel] interface, which uses the channels
+// system passed an object that adheres to in order to get events on the
+// channel.
+type wasmModel struct {
+	wm *worker.Manager
+}
+
+// JoinChannel is called whenever a channel is joined locally.
+func (w *wasmModel) JoinChannel(channel *cryptoBroadcast.Channel) {
+	data, err := json.Marshal(channel)
+	if err != nil {
+		jww.ERROR.Printf(
+			"[CH] Could not JSON marshal broadcast.Channel: %+v", err)
+		return
+	}
+
+	w.wm.SendMessage(JoinChannelTag, data, nil)
+}
+
+// LeaveChannel is called whenever a channel is left locally.
+func (w *wasmModel) LeaveChannel(channelID *id.ID) {
+	w.wm.SendMessage(LeaveChannelTag, channelID.Marshal(), nil)
+}
+
+// ReceiveMessage is called whenever a message is received on a given channel.
+//
+// It may be called multiple times on the same message; it is incumbent on the
+// user of the API to filter such called by message ID.
+func (w *wasmModel) ReceiveMessage(channelID *id.ID, messageID message.ID,
+	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, hidden bool) uint64 {
+	msg := channels.ModelMessage{
+		Nickname:       nickname,
+		MessageID:      messageID,
+		ChannelID:      channelID,
+		Timestamp:      timestamp,
+		Lease:          lease,
+		Status:         status,
+		Hidden:         hidden,
+		Pinned:         false,
+		Content:        []byte(text),
+		Type:           mType,
+		Round:          round.ID,
+		PubKey:         pubKey,
+		CodesetVersion: codeset,
+		DmToken:        dmToken,
+	}
+
+	data, err := json.Marshal(msg)
+	if err != nil {
+		jww.ERROR.Printf(
+			"[CH] Could not JSON marshal payload for ReceiveMessage: %+v", err)
+		return 0
+	}
+
+	uuidChan := make(chan uint64)
+	w.wm.SendMessage(ReceiveMessageTag, data, func(data []byte) {
+		var uuid uint64
+		err = json.Unmarshal(data, &uuid)
+		if err != nil {
+			jww.ERROR.Printf("[CH] Could not JSON unmarshal response to "+
+				"ReceiveMessage: %+v", err)
+			uuidChan <- 0
+		}
+		uuidChan <- uuid
+	})
+
+	select {
+	case uuid := <-uuidChan:
+		return uuid
+	case <-time.After(worker.ResponseTimeout):
+		jww.ERROR.Printf("[CH] Timed out after %s waiting for response from "+
+			"the worker about ReceiveMessage", worker.ResponseTimeout)
+	}
+
+	return 0
+}
+
+// ReceiveReplyMessage is JSON marshalled and sent to the worker for
+// [wasmModel.ReceiveReply].
+type ReceiveReplyMessage struct {
+	ReactionTo            message.ID `json:"replyTo"`
+	channels.ModelMessage `json:"message"`
+}
+
+// ReceiveReply is called whenever a message is received that is a reply on a
+// given channel. It may be called multiple times on the same message; it is
+// incumbent on the user of the API to filter such called by message ID.
+//
+// Messages may arrive our of order, so a reply, in theory, can arrive before
+// the initial message. As a result, it may be important to buffer replies.
+func (w *wasmModel) ReceiveReply(channelID *id.ID, messageID,
+	replyTo message.ID, 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,
+	hidden bool) uint64 {
+	msg := ReceiveReplyMessage{
+		ReactionTo: replyTo,
+		ModelMessage: channels.ModelMessage{
+			Nickname:       nickname,
+			MessageID:      messageID,
+			ChannelID:      channelID,
+			Timestamp:      timestamp,
+			Lease:          lease,
+			Status:         status,
+			Hidden:         hidden,
+			Pinned:         false,
+			Content:        []byte(text),
+			Type:           mType,
+			Round:          round.ID,
+			PubKey:         pubKey,
+			CodesetVersion: codeset,
+			DmToken:        dmToken,
+		},
+	}
+
+	data, err := json.Marshal(msg)
+	if err != nil {
+		jww.ERROR.Printf(
+			"[CH] Could not JSON marshal payload for ReceiveReply: %+v", err)
+		return 0
+	}
+
+	uuidChan := make(chan uint64)
+	w.wm.SendMessage(ReceiveReplyTag, data, func(data []byte) {
+		var uuid uint64
+		err = json.Unmarshal(data, &uuid)
+		if err != nil {
+			jww.ERROR.Printf("[CH] Could not JSON unmarshal response to "+
+				"ReceiveReply: %+v", err)
+			uuidChan <- 0
+		}
+		uuidChan <- uuid
+	})
+
+	select {
+	case uuid := <-uuidChan:
+		return uuid
+	case <-time.After(worker.ResponseTimeout):
+		jww.ERROR.Printf("[CH] Timed out after %s waiting for response from "+
+			"the worker about ReceiveReply", worker.ResponseTimeout)
+	}
+
+	return 0
+}
+
+// ReceiveReaction is called whenever a reaction to a message is received on a
+// given channel. It may be called multiple times on the same reaction; it is
+// incumbent on the user of the API to filter such called by message ID.
+//
+// Messages may arrive our of order, so a reply, in theory, can arrive before
+// the initial message. As a result, it may be important to buffer reactions.
+func (w *wasmModel) ReceiveReaction(channelID *id.ID, messageID,
+	reactionTo message.ID, 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,
+	hidden bool) uint64 {
+
+	msg := ReceiveReplyMessage{
+		ReactionTo: reactionTo,
+		ModelMessage: channels.ModelMessage{
+			Nickname:       nickname,
+			MessageID:      messageID,
+			ChannelID:      channelID,
+			Timestamp:      timestamp,
+			Lease:          lease,
+			Status:         status,
+			Hidden:         hidden,
+			Pinned:         false,
+			Content:        []byte(reaction),
+			Type:           mType,
+			Round:          round.ID,
+			PubKey:         pubKey,
+			CodesetVersion: codeset,
+			DmToken:        dmToken,
+		},
+	}
+
+	data, err := json.Marshal(msg)
+	if err != nil {
+		jww.ERROR.Printf(
+			"[CH] Could not JSON marshal payload for ReceiveReaction: %+v", err)
+		return 0
+	}
+
+	uuidChan := make(chan uint64)
+	w.wm.SendMessage(ReceiveReactionTag, data, func(data []byte) {
+		var uuid uint64
+		err = json.Unmarshal(data, &uuid)
+		if err != nil {
+			jww.ERROR.Printf("[CH] Could not JSON unmarshal response to "+
+				"ReceiveReaction: %+v", err)
+			uuidChan <- 0
+		}
+		uuidChan <- uuid
+	})
+
+	select {
+	case uuid := <-uuidChan:
+		return uuid
+	case <-time.After(worker.ResponseTimeout):
+		jww.ERROR.Printf("[CH] Timed out after %s waiting for response from "+
+			"the worker about ReceiveReply", worker.ResponseTimeout)
+	}
+
+	return 0
+}
+
+// MessageUpdateInfo is JSON marshalled and sent to the worker for
+// [wasmModel.UpdateFromMessageID] and [wasmModel.UpdateFromUUID].
+type MessageUpdateInfo struct {
+	UUID uint64 `json:"uuid"`
+
+	MessageID    message.ID `json:"messageID"`
+	MessageIDSet bool       `json:"messageIDSet"`
+
+	Timestamp    time.Time `json:"timestamp"`
+	TimestampSet bool      `json:"timestampSet"`
+
+	RoundID    id.Round `json:"round"`
+	RoundIDSet bool     `json:"roundIDSet"`
+
+	Pinned    bool `json:"pinned"`
+	PinnedSet bool `json:"pinnedSet"`
+
+	Hidden    bool `json:"hidden"`
+	HiddenSet bool `json:"hiddenSet"`
+
+	Status    channels.SentStatus `json:"status"`
+	StatusSet bool                `json:"statusSet"`
+}
+
+// UpdateFromUUID is called whenever a message at the UUID is modified.
+//
+// messageID, timestamp, round, pinned, and hidden are all nillable and may be
+// updated based upon the UUID at a later date. If a nil value is passed, then
+// make no update.
+func (w *wasmModel) UpdateFromUUID(uuid uint64, messageID *message.ID,
+	timestamp *time.Time, round *rounds.Round, pinned, hidden *bool,
+	status *channels.SentStatus) {
+	msg := MessageUpdateInfo{UUID: uuid}
+	if messageID != nil {
+		msg.MessageID = *messageID
+		msg.MessageIDSet = true
+	}
+	if timestamp != nil {
+		msg.Timestamp = *timestamp
+		msg.TimestampSet = true
+	}
+	if round != nil {
+		msg.RoundID = round.ID
+		msg.RoundIDSet = true
+	}
+	if pinned != nil {
+		msg.Pinned = *pinned
+		msg.PinnedSet = true
+	}
+	if hidden != nil {
+		msg.Hidden = *hidden
+		msg.HiddenSet = true
+	}
+	if status != nil {
+		msg.Status = *status
+		msg.StatusSet = true
+	}
+
+	data, err := json.Marshal(msg)
+	if err != nil {
+		jww.ERROR.Printf(
+			"[CH] Could not JSON marshal payload for UpdateFromUUID: %+v", err)
+		return
+	}
+
+	w.wm.SendMessage(UpdateFromUUIDTag, data, nil)
+}
+
+// UpdateFromMessageID is called whenever a message with the message ID is
+// modified.
+//
+// The API needs to return the UUID of the modified message that can be
+// referenced at a later time.
+//
+// timestamp, round, pinned, and hidden are all nillable and may be updated
+// based upon the UUID at a later date. If a nil value is passed, then make
+// no update.
+func (w *wasmModel) UpdateFromMessageID(messageID message.ID,
+	timestamp *time.Time, round *rounds.Round, pinned, hidden *bool,
+	status *channels.SentStatus) uint64 {
+
+	msg := MessageUpdateInfo{MessageID: messageID, MessageIDSet: true}
+	if timestamp != nil {
+		msg.Timestamp = *timestamp
+		msg.TimestampSet = true
+	}
+	if round != nil {
+		msg.RoundID = round.ID
+		msg.RoundIDSet = true
+	}
+	if pinned != nil {
+		msg.Pinned = *pinned
+		msg.PinnedSet = true
+	}
+	if hidden != nil {
+		msg.Hidden = *hidden
+		msg.HiddenSet = true
+	}
+	if status != nil {
+		msg.Status = *status
+		msg.StatusSet = true
+	}
+
+	data, err := json.Marshal(msg)
+	if err != nil {
+		jww.ERROR.Printf("[CH] Could not JSON marshal payload for "+
+			"UpdateFromMessageID: %+v", err)
+		return 0
+	}
+
+	uuidChan := make(chan uint64)
+	w.wm.SendMessage(UpdateFromMessageIDTag, data,
+		func(data []byte) {
+			var uuid uint64
+			err = json.Unmarshal(data, &uuid)
+			if err != nil {
+				jww.ERROR.Printf("[CH] Could not JSON unmarshal response to "+
+					"UpdateFromMessageID: %+v", err)
+				uuidChan <- 0
+			}
+			uuidChan <- uuid
+		})
+
+	select {
+	case uuid := <-uuidChan:
+		return uuid
+	case <-time.After(worker.ResponseTimeout):
+		jww.ERROR.Printf("[CH] Timed out after %s waiting for response from "+
+			"the worker about UpdateFromMessageID", worker.ResponseTimeout)
+	}
+
+	return 0
+}
+
+// GetMessageMessage is JSON marshalled and sent to the worker for
+// [wasmModel.GetMessage].
+type GetMessageMessage struct {
+	Message channels.ModelMessage `json:"message"`
+	Error   string                `json:"error"`
+}
+
+// GetMessage returns the message with the given [channel.MessageID].
+func (w *wasmModel) GetMessage(
+	messageID message.ID) (channels.ModelMessage, error) {
+	msgChan := make(chan GetMessageMessage)
+	w.wm.SendMessage(GetMessageTag, messageID.Marshal(),
+		func(data []byte) {
+			var msg GetMessageMessage
+			err := json.Unmarshal(data, &msg)
+			if err != nil {
+				jww.ERROR.Printf("[CH] Could not JSON unmarshal response to "+
+					"GetMessage: %+v", err)
+			}
+			msgChan <- msg
+		})
+
+	select {
+	case msg := <-msgChan:
+		if msg.Error != "" {
+			return channels.ModelMessage{}, errors.New(msg.Error)
+		}
+		return msg.Message, nil
+	case <-time.After(worker.ResponseTimeout):
+		return channels.ModelMessage{}, errors.Errorf("timed out after %s "+
+			"waiting for response from the worker about GetMessage",
+			worker.ResponseTimeout)
+	}
+}
+
+// DeleteMessage removes a message with the given messageID from storage.
+func (w *wasmModel) DeleteMessage(messageID message.ID) error {
+	errChan := make(chan error)
+	w.wm.SendMessage(DeleteMessageTag, messageID.Marshal(),
+		func(data []byte) {
+			if data != nil {
+				errChan <- errors.New(string(data))
+			} else {
+				errChan <- nil
+			}
+		})
+
+	select {
+	case err := <-errChan:
+		return err
+	case <-time.After(worker.ResponseTimeout):
+		return errors.Errorf("timed out after %s waiting for response from "+
+			"the worker about DeleteMessage", worker.ResponseTimeout)
+	}
+}
+
+// MuteUserMessage is JSON marshalled and sent to the worker for
+// [wasmModel.MuteUser].
+type MuteUserMessage struct {
+	ChannelID *id.ID            `json:"channelID"`
+	PubKey    ed25519.PublicKey `json:"pubKey"`
+	Unmute    bool              `json:"unmute"`
+}
+
+// MuteUser is called whenever a user is muted or unmuted.
+func (w *wasmModel) MuteUser(
+	channelID *id.ID, pubKey ed25519.PublicKey, unmute bool) {
+	msg := MuteUserMessage{
+		ChannelID: channelID,
+		PubKey:    pubKey,
+		Unmute:    unmute,
+	}
+
+	data, err := json.Marshal(msg)
+	if err != nil {
+		jww.ERROR.Printf("[CH] Could not marshal MuteUserMessage: %+v", err)
+		return
+	}
+
+	w.wm.SendMessage(MuteUserTag, data, nil)
+}
diff --git a/indexedDb/worker/channels/init.go b/indexedDb/worker/channels/init.go
new file mode 100644
index 0000000000000000000000000000000000000000..04b7a8f4392ac78051bd9cce001be45a98477dca
--- /dev/null
+++ b/indexedDb/worker/channels/init.go
@@ -0,0 +1,242 @@
+////////////////////////////////////////////////////////////////////////////////
+// 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 (
+	"crypto/ed25519"
+	"encoding/json"
+	"github.com/pkg/errors"
+	"time"
+
+	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/storage"
+	"gitlab.com/elixxir/xxdk-wasm/worker"
+	"gitlab.com/xx_network/primitives/id"
+)
+
+// 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, 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(wasmJsPath string,
+	encryption cryptoChannel.Cipher, messageReceivedCB MessageReceivedCallback,
+	deletedMessageCB DeletedMessageCallback,
+	mutedUserCB MutedUserCallback) channels.EventModelBuilder {
+	fn := func(path string) (channels.EventModel, error) {
+		return NewWASMEventModel(path, wasmJsPath, encryption,
+			messageReceivedCB, deletedMessageCB, mutedUserCB)
+	}
+	return fn
+}
+
+// NewWASMEventModelMessage is JSON marshalled and sent to the worker for
+// [NewWASMEventModel].
+type NewWASMEventModelMessage struct {
+	Path           string `json:"path"`
+	EncryptionJSON string `json:"encryptionJSON"`
+}
+
+// NewWASMEventModel returns a [channels.EventModel] backed by a wasmModel.
+// The name should be a base64 encoding of the users public key.
+func NewWASMEventModel(path, wasmJsPath string, encryption cryptoChannel.Cipher,
+	messageReceivedCB MessageReceivedCallback,
+	deletedMessageCB DeletedMessageCallback, mutedUserCB MutedUserCallback) (
+	channels.EventModel, error) {
+
+	wm, err := worker.NewManager(wasmJsPath, "channelsIndexedDb", true)
+	if err != nil {
+		return nil, err
+	}
+
+	// Register handler to manage messages for the MessageReceivedCallback
+	wm.RegisterCallback(MessageReceivedCallbackTag,
+		messageReceivedCallbackHandler(messageReceivedCB))
+
+	// Register handler to manage messages for the DeletedMessageCallback
+	wm.RegisterCallback(DeletedMessageCallbackTag,
+		deletedMessageCallbackHandler(deletedMessageCB))
+
+	// Register handler to manage messages for the MutedUserCallback
+	wm.RegisterCallback(MutedUserCallbackTag,
+		mutedUserCallbackHandler(mutedUserCB))
+
+	// Register handler to manage checking encryption status from local storage
+	wm.RegisterCallback(EncryptionStatusTag, checkDbEncryptionStatusHandler(wm))
+
+	// Register handler to manage the storage of the database name
+	wm.RegisterCallback(StoreDatabaseNameTag, storeDatabaseNameHandler(wm))
+
+	encryptionJSON, err := json.Marshal(encryption)
+	if err != nil {
+		return nil, err
+	}
+
+	msg := NewWASMEventModelMessage{
+		Path:           path,
+		EncryptionJSON: string(encryptionJSON),
+	}
+
+	payload, err := json.Marshal(msg)
+	if err != nil {
+		return nil, err
+	}
+
+	errChan := make(chan string)
+	wm.SendMessage(NewWASMEventModelTag, payload,
+		func(data []byte) { errChan <- string(data) })
+
+	select {
+	case workerErr := <-errChan:
+		if workerErr != "" {
+			return nil, errors.New(workerErr)
+		}
+	case <-time.After(worker.ResponseTimeout):
+		return nil, errors.Errorf("timed out after %s waiting for indexedDB "+
+			"database in worker to initialize", worker.ResponseTimeout)
+	}
+
+	return &wasmModel{wm}, nil
+}
+
+// MessageReceivedCallbackMessage is JSON marshalled and received from the
+// worker for the [MessageReceivedCallback] callback.
+type MessageReceivedCallbackMessage struct {
+	UUID      uint64 `json:"uuid"`
+	ChannelID *id.ID `json:"channelID"`
+	Update    bool   `json:"update"`
+}
+
+// messageReceivedCallbackHandler returns a handler to manage messages for the
+// MessageReceivedCallback.
+func messageReceivedCallbackHandler(cb MessageReceivedCallback) func(data []byte) {
+	return func(data []byte) {
+		var msg MessageReceivedCallbackMessage
+		err := json.Unmarshal(data, &msg)
+		if err != nil {
+			jww.ERROR.Printf(
+				"Failed to JSON unmarshal %T from worker: %+v", msg, err)
+			return
+		}
+
+		cb(msg.UUID, msg.ChannelID, msg.Update)
+	}
+}
+
+// deletedMessageCallbackHandler returns a handler to manage messages for the
+// DeletedMessageCallback.
+func deletedMessageCallbackHandler(cb DeletedMessageCallback) func(data []byte) {
+	return func(data []byte) {
+		messageID, err := message.UnmarshalID(data)
+		if err != nil {
+			jww.ERROR.Printf(
+				"Failed to JSON unmarshal message ID from worker: %+v", err)
+		}
+
+		cb(messageID)
+	}
+}
+
+// mutedUserCallbackHandler returns a handler to manage messages for the
+// MutedUserCallback.
+func mutedUserCallbackHandler(cb MutedUserCallback) func(data []byte) {
+	return func(data []byte) {
+		var msg MuteUserMessage
+		err := json.Unmarshal(data, &msg)
+		if err != nil {
+			jww.ERROR.Printf(
+				"Failed to JSON unmarshal %T from worker: %+v", msg, err)
+			return
+		}
+
+		cb(msg.ChannelID, msg.PubKey, msg.Unmute)
+	}
+}
+
+// EncryptionStatusMessage is JSON marshalled and received from the worker when
+// the database checks if it is encrypted.
+type EncryptionStatusMessage struct {
+	DatabaseName     string `json:"databaseName"`
+	EncryptionStatus bool   `json:"encryptionStatus"`
+}
+
+// EncryptionStatusReply is JSON marshalled and sent to the worker is response
+// to the [EncryptionStatusMessage].
+type EncryptionStatusReply struct {
+	EncryptionStatus bool   `json:"encryptionStatus"`
+	Error            string `json:"error"`
+}
+
+// checkDbEncryptionStatusHandler returns a handler to manage checking
+// encryption status from local storage.
+func checkDbEncryptionStatusHandler(
+	wh *worker.Manager) func(data []byte) {
+	return func(data []byte) {
+		// Unmarshal received message
+		var msg EncryptionStatusMessage
+		err := json.Unmarshal(data, &msg)
+		if err != nil {
+			jww.ERROR.Printf("Failed to JSON unmarshal "+
+				"EncryptionStatusMessage message from worker: %+v", err)
+			return
+		}
+
+		// Pass message values to storage
+		loadedEncryptionStatus, err := storage.StoreIndexedDbEncryptionStatus(
+			msg.DatabaseName, msg.EncryptionStatus)
+		var reply EncryptionStatusReply
+		if err != nil {
+			reply.Error = err.Error()
+		} else {
+			reply.EncryptionStatus = loadedEncryptionStatus
+		}
+
+		// Return response
+		statusData, err := json.Marshal(reply)
+		if err != nil {
+			jww.ERROR.Printf(
+				"Failed to JSON marshal EncryptionStatusReply: %+v", err)
+			return
+		}
+
+		wh.SendMessage(EncryptionStatusTag, statusData, nil)
+	}
+}
+
+// storeDatabaseNameHandler returns a handler that stores the database name to
+// storage when it is received from the worker.
+func storeDatabaseNameHandler(
+	wh *worker.Manager) func(data []byte) {
+	return func(data []byte) {
+		var returnData []byte
+
+		// Get the database name and save it to storage
+		if err := storage.StoreIndexedDb(string(data)); err != nil {
+			returnData = []byte(err.Error())
+		}
+
+		wh.SendMessage(StoreDatabaseNameTag, returnData, nil)
+	}
+}
diff --git a/indexedDb/worker/channels/tags.go b/indexedDb/worker/channels/tags.go
new file mode 100644
index 0000000000000000000000000000000000000000..d2ae61a9f6b65a9bf2a02483806e89fbc14edb9d
--- /dev/null
+++ b/indexedDb/worker/channels/tags.go
@@ -0,0 +1,34 @@
+////////////////////////////////////////////////////////////////////////////////
+// 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 "gitlab.com/elixxir/xxdk-wasm/worker"
+
+// List of tags that can be used when sending a message or registering a handler
+// to receive a message.
+const (
+	NewWASMEventModelTag       worker.Tag = "NewWASMEventModel"
+	MessageReceivedCallbackTag worker.Tag = "MessageReceivedCallback"
+	DeletedMessageCallbackTag  worker.Tag = "DeletedMessageCallback"
+	MutedUserCallbackTag       worker.Tag = "MutedUserCallback"
+	EncryptionStatusTag        worker.Tag = "EncryptionStatus"
+	StoreDatabaseNameTag       worker.Tag = "StoreDatabaseName"
+
+	JoinChannelTag         worker.Tag = "JoinChannel"
+	LeaveChannelTag        worker.Tag = "LeaveChannel"
+	ReceiveMessageTag      worker.Tag = "ReceiveMessage"
+	ReceiveReplyTag        worker.Tag = "ReceiveReply"
+	ReceiveReactionTag     worker.Tag = "ReceiveReaction"
+	UpdateFromUUIDTag      worker.Tag = "UpdateFromUUID"
+	UpdateFromMessageIDTag worker.Tag = "UpdateFromMessageID"
+	GetMessageTag          worker.Tag = "GetMessage"
+	DeleteMessageTag       worker.Tag = "DeleteMessage"
+	MuteUserTag            worker.Tag = "MuteUser"
+)
diff --git a/indexedDb/worker/dm/implementation.go b/indexedDb/worker/dm/implementation.go
new file mode 100644
index 0000000000000000000000000000000000000000..2b37cef9c459769ba1ab08a8529781cf87e46740
--- /dev/null
+++ b/indexedDb/worker/dm/implementation.go
@@ -0,0 +1,246 @@
+////////////////////////////////////////////////////////////////////////////////
+// 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"
+	"time"
+
+	jww "github.com/spf13/jwalterweatherman"
+	"gitlab.com/elixxir/client/v4/cmix/rounds"
+	"gitlab.com/elixxir/client/v4/dm"
+	"gitlab.com/elixxir/crypto/message"
+	"gitlab.com/elixxir/xxdk-wasm/worker"
+)
+
+// wasmModel implements dm.EventModel interface, which uses the channels system
+// passed an object that adheres to in order to get events on the channel.
+type wasmModel struct {
+	wh *worker.Manager
+}
+
+// TransferMessage is JSON marshalled and sent to the worker.
+type TransferMessage struct {
+	UUID       uint64            `json:"uuid"`
+	MessageID  message.ID        `json:"messageID"`
+	ReactionTo message.ID        `json:"reactionTo"`
+	Nickname   string            `json:"nickname"`
+	Text       []byte            `json:"text"`
+	PubKey     ed25519.PublicKey `json:"pubKey"`
+	DmToken    uint32            `json:"dmToken"`
+	Codeset    uint8             `json:"codeset"`
+	Timestamp  time.Time         `json:"timestamp"`
+	Round      rounds.Round      `json:"round"`
+	MType      dm.MessageType    `json:"mType"`
+	Status     dm.Status         `json:"status"`
+}
+
+func (w *wasmModel) Receive(messageID message.ID, nickname string, text []byte,
+	pubKey ed25519.PublicKey, dmToken uint32, codeset uint8, timestamp time.Time,
+	round rounds.Round, mType dm.MessageType, status dm.Status) uint64 {
+	msg := TransferMessage{
+		MessageID: messageID,
+		Nickname:  nickname,
+		Text:      text,
+		PubKey:    pubKey,
+		DmToken:   dmToken,
+		Codeset:   codeset,
+		Timestamp: timestamp,
+		Round:     round,
+		MType:     mType,
+		Status:    status,
+	}
+
+	data, err := json.Marshal(msg)
+	if err != nil {
+		jww.ERROR.Printf(
+			"Could not JSON marshal payload for TransferMessage: %+v", err)
+		return 0
+	}
+
+	uuidChan := make(chan uint64)
+	w.wh.SendMessage(ReceiveTag, data, func(data []byte) {
+		var uuid uint64
+		err = json.Unmarshal(data, &uuid)
+		if err != nil {
+			jww.ERROR.Printf(
+				"Could not JSON unmarshal response to Receive: %+v", err)
+			uuidChan <- 0
+		}
+		uuidChan <- uuid
+	})
+
+	select {
+	case uuid := <-uuidChan:
+		return uuid
+	case <-time.After(worker.ResponseTimeout):
+		jww.ERROR.Printf("Timed out after %s waiting for response from the "+
+			"worker about Receive", worker.ResponseTimeout)
+	}
+
+	return 0
+}
+
+func (w *wasmModel) ReceiveText(messageID message.ID, nickname, text string,
+	pubKey ed25519.PublicKey, dmToken uint32, codeset uint8,
+	timestamp time.Time, round rounds.Round, status dm.Status) uint64 {
+	msg := TransferMessage{
+		MessageID: messageID,
+		Nickname:  nickname,
+		Text:      []byte(text),
+		PubKey:    pubKey,
+		DmToken:   dmToken,
+		Codeset:   codeset,
+		Timestamp: timestamp,
+		Round:     round,
+		Status:    status,
+	}
+
+	data, err := json.Marshal(msg)
+	if err != nil {
+		jww.ERROR.Printf(
+			"Could not JSON marshal payload for TransferMessage: %+v", err)
+		return 0
+	}
+
+	uuidChan := make(chan uint64)
+	w.wh.SendMessage(ReceiveTextTag, data, func(data []byte) {
+		var uuid uint64
+		err = json.Unmarshal(data, &uuid)
+		if err != nil {
+			jww.ERROR.Printf(
+				"Could not JSON unmarshal response to ReceiveText: %+v", err)
+			uuidChan <- 0
+		}
+		uuidChan <- uuid
+	})
+
+	select {
+	case uuid := <-uuidChan:
+		return uuid
+	case <-time.After(worker.ResponseTimeout):
+		jww.ERROR.Printf("Timed out after %s waiting for response from the "+
+			"worker about ReceiveText", worker.ResponseTimeout)
+	}
+
+	return 0
+}
+
+func (w *wasmModel) ReceiveReply(messageID, reactionTo message.ID, nickname,
+	text string, pubKey ed25519.PublicKey, dmToken uint32, codeset uint8,
+	timestamp time.Time, round rounds.Round, status dm.Status) uint64 {
+	msg := TransferMessage{
+		MessageID:  messageID,
+		ReactionTo: reactionTo,
+		Nickname:   nickname,
+		Text:       []byte(text),
+		PubKey:     pubKey,
+		DmToken:    dmToken,
+		Codeset:    codeset,
+		Timestamp:  timestamp,
+		Round:      round,
+		Status:     status,
+	}
+
+	data, err := json.Marshal(msg)
+	if err != nil {
+		jww.ERROR.Printf(
+			"Could not JSON marshal payload for TransferMessage: %+v", err)
+		return 0
+	}
+
+	uuidChan := make(chan uint64)
+	w.wh.SendMessage(ReceiveReplyTag, data, func(data []byte) {
+		var uuid uint64
+		err = json.Unmarshal(data, &uuid)
+		if err != nil {
+			jww.ERROR.Printf(
+				"Could not JSON unmarshal response to ReceiveReply: %+v", err)
+			uuidChan <- 0
+		}
+		uuidChan <- uuid
+	})
+
+	select {
+	case uuid := <-uuidChan:
+		return uuid
+	case <-time.After(worker.ResponseTimeout):
+		jww.ERROR.Printf("Timed out after %s waiting for response from the "+
+			"worker about ReceiveReply", worker.ResponseTimeout)
+	}
+
+	return 0
+}
+
+func (w *wasmModel) ReceiveReaction(messageID, reactionTo message.ID, nickname,
+	reaction string, pubKey ed25519.PublicKey, dmToken uint32, codeset uint8,
+	timestamp time.Time, round rounds.Round, status dm.Status) uint64 {
+	msg := TransferMessage{
+		MessageID:  messageID,
+		ReactionTo: reactionTo,
+		Nickname:   nickname,
+		Text:       []byte(reaction),
+		PubKey:     pubKey,
+		DmToken:    dmToken,
+		Codeset:    codeset,
+		Timestamp:  timestamp,
+		Round:      round,
+		Status:     status,
+	}
+
+	data, err := json.Marshal(msg)
+	if err != nil {
+		jww.ERROR.Printf(
+			"Could not JSON marshal payload for TransferMessage: %+v", err)
+		return 0
+	}
+
+	uuidChan := make(chan uint64)
+	w.wh.SendMessage(ReceiveReactionTag, data, func(data []byte) {
+		var uuid uint64
+		err = json.Unmarshal(data, &uuid)
+		if err != nil {
+			jww.ERROR.Printf(
+				"Could not JSON unmarshal response to ReceiveReaction: %+v", err)
+			uuidChan <- 0
+		}
+		uuidChan <- uuid
+	})
+
+	select {
+	case uuid := <-uuidChan:
+		return uuid
+	case <-time.After(worker.ResponseTimeout):
+		jww.ERROR.Printf("Timed out after %s waiting for response from the "+
+			"worker about ReceiveReaction", worker.ResponseTimeout)
+	}
+
+	return 0
+}
+
+func (w *wasmModel) UpdateSentStatus(uuid uint64, messageID message.ID,
+	timestamp time.Time, round rounds.Round, status dm.Status) {
+	msg := TransferMessage{
+		UUID:      uuid,
+		MessageID: messageID,
+		Timestamp: timestamp,
+		Round:     round,
+		Status:    status,
+	}
+
+	data, err := json.Marshal(msg)
+	if err != nil {
+		jww.ERROR.Printf(
+			"Could not JSON marshal payload for TransferMessage: %+v", err)
+	}
+
+	w.wh.SendMessage(UpdateSentStatusTag, data, nil)
+}
diff --git a/indexedDb/worker/dm/init.go b/indexedDb/worker/dm/init.go
new file mode 100644
index 0000000000000000000000000000000000000000..145edf935ba9804dd315c2196d301bb60581ecfb
--- /dev/null
+++ b/indexedDb/worker/dm/init.go
@@ -0,0 +1,176 @@
+////////////////////////////////////////////////////////////////////////////////
+// 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"
+	"time"
+
+	"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/storage"
+	"gitlab.com/elixxir/xxdk-wasm/worker"
+)
+
+// 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)
+
+// NewWASMEventModelMessage is JSON marshalled and sent to the worker for
+// [NewWASMEventModel].
+type NewWASMEventModelMessage struct {
+	Path           string `json:"path"`
+	EncryptionJSON string `json:"encryptionJSON"`
+}
+
+// NewWASMEventModel returns a [channels.EventModel] backed by a wasmModel.
+// The name should be a base64 encoding of the users public key.
+func NewWASMEventModel(path, wasmJsPath string, encryption cryptoChannel.Cipher,
+	cb MessageReceivedCallback) (dm.EventModel, error) {
+
+	wh, err := worker.NewManager(wasmJsPath, "dmIndexedDb", true)
+	if err != nil {
+		return nil, err
+	}
+
+	// Register handler to manage messages for the MessageReceivedCallback
+	wh.RegisterCallback(
+		MessageReceivedCallbackTag, messageReceivedCallbackHandler(cb))
+
+	// Register handler to manage checking encryption status from local storage
+	wh.RegisterCallback(EncryptionStatusTag, checkDbEncryptionStatusHandler(wh))
+
+	// Register handler to manage the storage of the database name
+	wh.RegisterCallback(StoreDatabaseNameTag, storeDatabaseNameHandler(wh))
+
+	encryptionJSON, err := json.Marshal(encryption)
+	if err != nil {
+		return nil, err
+	}
+
+	msg := NewWASMEventModelMessage{
+		Path:           path,
+		EncryptionJSON: string(encryptionJSON),
+	}
+
+	payload, err := json.Marshal(msg)
+	if err != nil {
+		return nil, err
+	}
+
+	errChan := make(chan string)
+	wh.SendMessage(NewWASMEventModelTag, payload,
+		func(data []byte) { errChan <- string(data) })
+
+	select {
+	case workerErr := <-errChan:
+		if workerErr != "" {
+			return nil, errors.New(workerErr)
+		}
+	case <-time.After(worker.ResponseTimeout):
+		return nil, errors.Errorf("timed out after %s waiting for indexedDB "+
+			"database in worker to initialize", worker.ResponseTimeout)
+	}
+
+	return &wasmModel{wh}, nil
+}
+
+// MessageReceivedCallbackMessage is JSON marshalled and received from the
+// worker for the [MessageReceivedCallback] callback.
+type MessageReceivedCallbackMessage struct {
+	UUID   uint64            `json:"uuid"`
+	PubKey ed25519.PublicKey `json:"pubKey"`
+	Update bool              `json:"update"`
+}
+
+// messageReceivedCallbackHandler returns a handler to manage messages for the
+// MessageReceivedCallback.
+func messageReceivedCallbackHandler(cb MessageReceivedCallback) func(data []byte) {
+	return func(data []byte) {
+		var msg MessageReceivedCallbackMessage
+		err := json.Unmarshal(data, &msg)
+		if err != nil {
+			jww.ERROR.Printf("Failed to JSON unmarshal "+
+				"MessageReceivedCallback message from worker: %+v", err)
+			return
+		}
+		cb(msg.UUID, msg.PubKey, msg.Update)
+	}
+}
+
+// EncryptionStatusMessage is JSON marshalled and received from the worker when
+// the database checks if it is encrypted.
+type EncryptionStatusMessage struct {
+	DatabaseName     string `json:"databaseName"`
+	EncryptionStatus bool   `json:"encryptionStatus"`
+}
+
+// EncryptionStatusReply is JSON marshalled and sent to the worker is response
+// to the [EncryptionStatusMessage].
+type EncryptionStatusReply struct {
+	EncryptionStatus bool   `json:"encryptionStatus"`
+	Error            string `json:"error"`
+}
+
+// checkDbEncryptionStatusHandler returns a handler to manage checking
+// encryption status from local storage.
+func checkDbEncryptionStatusHandler(wh *worker.Manager) func(data []byte) {
+	return func(data []byte) {
+		// Unmarshal received message
+		var msg EncryptionStatusMessage
+		err := json.Unmarshal(data, &msg)
+		if err != nil {
+			jww.ERROR.Printf("Failed to JSON unmarshal "+
+				"EncryptionStatusMessage message from worker: %+v", err)
+			return
+		}
+
+		// Pass message values to storage
+		loadedEncryptionStatus, err := storage.StoreIndexedDbEncryptionStatus(
+			msg.DatabaseName, msg.EncryptionStatus)
+		var reply EncryptionStatusReply
+		if err != nil {
+			reply.Error = err.Error()
+		} else {
+			reply.EncryptionStatus = loadedEncryptionStatus
+		}
+
+		// Return response
+		statusData, err := json.Marshal(reply)
+		if err != nil {
+			jww.ERROR.Printf(
+				"Failed to JSON marshal EncryptionStatusReply: %+v", err)
+			return
+		}
+
+		wh.SendMessage(EncryptionStatusTag, statusData, nil)
+	}
+}
+
+// storeDatabaseNameHandler returns a handler that stores the database name to
+// storage when it is received from the worker.
+func storeDatabaseNameHandler(wh *worker.Manager) func(data []byte) {
+	return func(data []byte) {
+		var returnData []byte
+
+		// Get the database name and save it to storage
+		if err := storage.StoreIndexedDb(string(data)); err != nil {
+			returnData = []byte(err.Error())
+		}
+
+		wh.SendMessage(StoreDatabaseNameTag, returnData, nil)
+	}
+}
diff --git a/indexedDb/worker/dm/tags.go b/indexedDb/worker/dm/tags.go
new file mode 100644
index 0000000000000000000000000000000000000000..afe29a33f3acc01470a35f84be495f2878bb194f
--- /dev/null
+++ b/indexedDb/worker/dm/tags.go
@@ -0,0 +1,27 @@
+////////////////////////////////////////////////////////////////////////////////
+// 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 "gitlab.com/elixxir/xxdk-wasm/worker"
+
+// List of tags that can be used when sending a message or registering a handler
+// to receive a message.
+const (
+	NewWASMEventModelTag       worker.Tag = "NewWASMEventModel"
+	MessageReceivedCallbackTag worker.Tag = "MessageReceivedCallback"
+	EncryptionStatusTag        worker.Tag = "EncryptionStatus"
+	StoreDatabaseNameTag       worker.Tag = "StoreDatabaseName"
+
+	ReceiveReplyTag     worker.Tag = "ReceiveReply"
+	ReceiveReactionTag  worker.Tag = "ReceiveReaction"
+	ReceiveTag          worker.Tag = "Receive"
+	ReceiveTextTag      worker.Tag = "ReceiveText"
+	UpdateSentStatusTag worker.Tag = "UpdateSentStatusTag"
+)
diff --git a/logging/console.go b/logging/console.go
new file mode 100644
index 0000000000000000000000000000000000000000..8c2edd821210d338f73ff18bfabedfc2f319d7d9
--- /dev/null
+++ b/logging/console.go
@@ -0,0 +1,97 @@
+////////////////////////////////////////////////////////////////////////////////
+// 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 logging
+
+import (
+	jww "github.com/spf13/jwalterweatherman"
+	"io"
+	"syscall/js"
+)
+
+var consoleObj = js.Global().Get("console")
+
+// Console contains the Javascript console object, which provides access to the
+// browser's debugging console. This structure is defined for only a single
+// method on the console object. For example, if the method is set to debug,
+// then all calls to console.Write will print a debug message to the Javascript
+// console.
+//
+// Doc: https://developer.mozilla.org/en-US/docs/Web/API/console
+type Console struct {
+	method string
+	js.Value
+}
+
+// Write writes the data to the Javascript console with preset method. Returns
+// the number of bytes written.
+func (c *Console) Write(p []byte) (n int, err error) {
+	c.Call(c.method, string(p))
+	return len(p), nil
+}
+
+// JsConsoleLogListener redirects log output to the Javascript console using the
+// correct console method.
+type JsConsoleLogListener struct {
+	jww.Threshold
+	js.Value
+
+	trace    *Console
+	debug    *Console
+	info     *Console
+	error    *Console
+	warn     *Console
+	critical *Console
+	fatal    *Console
+	def      *Console
+}
+
+// NewJsConsoleLogListener initialises a new log listener that listener for the
+// specific threshold and prints the logs to the Javascript console.
+func NewJsConsoleLogListener(threshold jww.Threshold) *JsConsoleLogListener {
+	return &JsConsoleLogListener{
+		Threshold: threshold,
+		Value:     consoleObj,
+		trace:     &Console{"debug", consoleObj},
+		debug:     &Console{"log", consoleObj},
+		info:      &Console{"info", consoleObj},
+		warn:      &Console{"warn", consoleObj},
+		error:     &Console{"error", consoleObj},
+		critical:  &Console{"error", consoleObj},
+		fatal:     &Console{"error", consoleObj},
+		def:       &Console{"log", consoleObj},
+	}
+}
+
+// Listen is called for every logging event. This function adheres to the
+// [jwalterweatherman.LogListener] type.
+func (ll *JsConsoleLogListener) Listen(t jww.Threshold) io.Writer {
+	if t < ll.Threshold {
+		return nil
+	}
+
+	switch t {
+	case jww.LevelTrace:
+		return ll.trace
+	case jww.LevelDebug:
+		return ll.debug
+	case jww.LevelInfo:
+		return ll.info
+	case jww.LevelWarn:
+		return ll.warn
+	case jww.LevelError:
+		return ll.error
+	case jww.LevelCritical:
+		return ll.critical
+	case jww.LevelFatal:
+		return ll.fatal
+	default:
+		return ll.def
+	}
+}
diff --git a/logging/jwwListeners.go b/logging/jwwListeners.go
new file mode 100644
index 0000000000000000000000000000000000000000..7d83f3d5bcc69190c505dfd0d0676fbe6104d1d0
--- /dev/null
+++ b/logging/jwwListeners.go
@@ -0,0 +1,78 @@
+////////////////////////////////////////////////////////////////////////////////
+// Copyright © 2022 xx foundation                                             //
+//                                                                            //
+// Use of this source code is governed by a license that can be found in the  //
+// LICENSE file.                                                              //
+////////////////////////////////////////////////////////////////////////////////
+
+package logging
+
+import (
+	jww "github.com/spf13/jwalterweatherman"
+	"sync"
+)
+
+// logListeners contains a map of all registered log listeners keyed on a unique
+// ID that can be used to remove the listener once it has been added. This
+// global keeps track of all listeners that are registered to jwalterweatherman
+// logging.
+var logListeners = newLogListenerList()
+
+type logListenerList struct {
+	listeners map[uint64]jww.LogListener
+	currentID uint64
+	sync.Mutex
+}
+
+func newLogListenerList() logListenerList {
+	return logListenerList{
+		listeners: make(map[uint64]jww.LogListener),
+		currentID: 0,
+	}
+}
+
+// AddLogListener registers the log listener with jwalterweatherman. Returns a
+// unique ID that can be used to remove the listener.
+func AddLogListener(ll jww.LogListener) uint64 {
+	logListeners.Lock()
+	defer logListeners.Unlock()
+
+	id := logListeners.addLogListener(ll)
+	jww.SetLogListeners(logListeners.mapToSlice()...)
+	return id
+}
+
+// RemoveLogListener unregisters the log listener with the ID from
+// jwalterweatherman.
+func RemoveLogListener(id uint64) {
+	logListeners.Lock()
+	defer logListeners.Unlock()
+
+	logListeners.removeLogListener(id)
+	jww.SetLogListeners(logListeners.mapToSlice()...)
+
+}
+
+// addLogListener adds the listener to the list and returns its unique ID.
+func (lll *logListenerList) addLogListener(ll jww.LogListener) uint64 {
+	id := lll.currentID
+	lll.currentID++
+	lll.listeners[id] = ll
+
+	return id
+}
+
+// removeLogListener removes the listener with the specified ID from the list.
+func (lll *logListenerList) removeLogListener(id uint64) {
+	delete(lll.listeners, id)
+}
+
+// mapToSlice converts the map of listeners to a slice of listeners so that it
+// can be registered with jwalterweatherman.SetLogListeners.
+func (lll *logListenerList) mapToSlice() []jww.LogListener {
+	listeners := make([]jww.LogListener, 0, len(lll.listeners))
+	for _, l := range lll.listeners {
+		listeners = append(listeners, l)
+	}
+	return listeners
+}
diff --git a/logging/logLevel.go b/logging/logLevel.go
new file mode 100644
index 0000000000000000000000000000000000000000..895857475c4c647d7625b43644e6c4f32be98155
--- /dev/null
+++ b/logging/logLevel.go
@@ -0,0 +1,89 @@
+////////////////////////////////////////////////////////////////////////////////
+// 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 logging
+
+import (
+	"fmt"
+	"github.com/pkg/errors"
+	jww "github.com/spf13/jwalterweatherman"
+	"gitlab.com/elixxir/xxdk-wasm/utils"
+	"log"
+	"syscall/js"
+)
+
+// LogLevel sets level of logging. All logs at the set level and below will be
+// displayed (e.g., when log level is ERROR, only ERROR, CRITICAL, and FATAL
+// messages will be printed).
+//
+// The default log level without updates is INFO.
+func LogLevel(threshold jww.Threshold) error {
+	if threshold < jww.LevelTrace || threshold > jww.LevelFatal {
+		return errors.Errorf("log level is not valid: log level: %d", threshold)
+	}
+
+	jww.SetLogThreshold(threshold)
+	jww.SetFlags(log.LstdFlags | log.Lmicroseconds)
+
+	ll := NewJsConsoleLogListener(threshold)
+	AddLogListener(ll.Listen)
+	jww.SetStdoutThreshold(jww.LevelFatal + 1)
+
+	msg := fmt.Sprintf("Log level set to: %s", threshold)
+	switch threshold {
+	case jww.LevelTrace:
+		fallthrough
+	case jww.LevelDebug:
+		fallthrough
+	case jww.LevelInfo:
+		jww.INFO.Print(msg)
+	case jww.LevelWarn:
+		jww.WARN.Print(msg)
+	case jww.LevelError:
+		jww.ERROR.Print(msg)
+	case jww.LevelCritical:
+		jww.CRITICAL.Print(msg)
+	case jww.LevelFatal:
+		jww.FATAL.Print(msg)
+	}
+
+	return nil
+}
+
+// LogLevelJS sets level of logging. All logs at the set level and below will be
+// displayed (e.g., when log level is ERROR, only ERROR, CRITICAL, and FATAL
+// messages will be printed).
+//
+// Log level options:
+//
+//	TRACE    - 0
+//	DEBUG    - 1
+//	INFO     - 2
+//	WARN     - 3
+//	ERROR    - 4
+//	CRITICAL - 5
+//	FATAL    - 6
+//
+// The default log level without updates is INFO.
+//
+// Parameters:
+//   - args[0] - Log level (int).
+//
+// Returns:
+//   - Throws TypeError if the log level is invalid.
+func LogLevelJS(_ js.Value, args []js.Value) any {
+	threshold := jww.Threshold(args[0].Int())
+	err := LogLevel(threshold)
+	if err != nil {
+		utils.Throw(utils.TypeError, err)
+		return nil
+	}
+
+	return nil
+}
diff --git a/logging/logger.go b/logging/logger.go
new file mode 100644
index 0000000000000000000000000000000000000000..7da185f34ff81c31475dec3531c407bd53344114
--- /dev/null
+++ b/logging/logger.go
@@ -0,0 +1,537 @@
+////////////////////////////////////////////////////////////////////////////////
+// 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 logging
+
+import (
+	"encoding/binary"
+	"encoding/json"
+	"fmt"
+	"github.com/armon/circbuf"
+	"github.com/pkg/errors"
+	jww "github.com/spf13/jwalterweatherman"
+	"gitlab.com/elixxir/xxdk-wasm/utils"
+	"gitlab.com/elixxir/xxdk-wasm/worker"
+	"io"
+	"strconv"
+	"sync/atomic"
+	"syscall/js"
+	"time"
+)
+
+const (
+	// DefaultInitThreshold is the log threshold used for the initial log before
+	// any logging options is set.
+	DefaultInitThreshold = jww.LevelTrace
+
+	// logListenerChanSize is the size of the listener channel that stores log
+	// messages before they are written.
+	logListenerChanSize = 1500
+)
+
+// List of tags that can be used when sending a message or registering a handler
+// to receive a message.
+const (
+	NewLogFileTag worker.Tag = "NewLogFile"
+	WriteLogTag   worker.Tag = "WriteLog"
+	GetFileTag    worker.Tag = "GetFile"
+	GetFileExtTag worker.Tag = "GetFileExt"
+	SizeTag       worker.Tag = "Size"
+)
+
+// logger is the global that all jwalterweatherman logging is sent to.
+var logger *Logger
+
+// Logger manages the recording of jwalterweatherman logs. It can write logs to
+// a local, in-memory buffer or to an external worker.
+type Logger struct {
+	threshold      jww.Threshold
+	maxLogFileSize int
+	logListenerID  uint64
+
+	listenChan  chan []byte
+	mode        atomic.Uint32
+	processQuit chan struct{}
+
+	cb *circbuf.Buffer
+	wm *worker.Manager
+}
+
+// InitLogger initializes the logger. Include this in the init function in main.
+func InitLogger() *Logger {
+	logger = NewLogger()
+	return logger
+}
+
+// GetLogger returns the Logger object, used to manager where logging is
+// recorded.
+func GetLogger() *Logger {
+	return logger
+}
+
+// NewLogger creates a new Logger that begins storing the first
+// DefaultInitThreshold log entries. If either the log file or log worker is
+// enabled, then these logs are redirected to the set destination. If the
+// channel fills up with no log recorder enabled, then the listener is disabled.
+func NewLogger() *Logger {
+	lf := newLogger()
+
+	// Add the log listener
+	lf.logListenerID = AddLogListener(lf.Listen)
+
+	jww.INFO.Printf("[LOG] Enabled initial log file listener in %s with ID %d "+
+		"at threshold %s that can store %d entries",
+		lf.getMode(), lf.logListenerID, lf.Threshold(), cap(lf.listenChan))
+
+	return lf
+}
+
+// newLogger initialises a Logger without adding it as a log listener.
+func newLogger() *Logger {
+	lf := &Logger{
+		threshold:   DefaultInitThreshold,
+		listenChan:  make(chan []byte, logListenerChanSize),
+		mode:        atomic.Uint32{},
+		processQuit: make(chan struct{}),
+	}
+	lf.setMode(initMode)
+
+	return lf
+}
+
+// LogToFile starts logging to a local, in-memory log file.
+func (l *Logger) LogToFile(threshold jww.Threshold, maxLogFileSize int) error {
+	err := l.prepare(threshold, maxLogFileSize, fileMode)
+	if err != nil {
+		return err
+	}
+
+	b, err := circbuf.NewBuffer(int64(maxLogFileSize))
+	if err != nil {
+		return err
+	}
+	l.cb = b
+
+	sendLog := func(p []byte) {
+		if n, err2 := l.cb.Write(p); err2 != nil {
+			jww.ERROR.Printf(
+				"[LOG] Error writing log to circular buffer: %+v", err2)
+		} else if n != len(p) {
+			jww.ERROR.Printf(
+				"[LOG] Wrote %d bytes when %d bytes expected", n, len(p))
+		}
+	}
+	go l.processLog(workerMode, sendLog, l.processQuit)
+
+	return nil
+}
+
+// LogToFileWorker starts a new worker that begins listening for logs and
+// writing them to file. This function blocks until the worker has started.
+func (l *Logger) LogToFileWorker(threshold jww.Threshold, maxLogFileSize int,
+	wasmJsPath, workerName string) error {
+	err := l.prepare(threshold, maxLogFileSize, workerMode)
+	if err != nil {
+		return err
+	}
+
+	// Create new worker manager, which will start the worker and wait until
+	// communication has been established
+	wm, err := worker.NewManager(wasmJsPath, workerName, false)
+	if err != nil {
+		return err
+	}
+	l.wm = wm
+
+	// Register the callback used by the Javascript to request the log file.
+	// This prevents an error print when GetFileExtTag is not registered.
+	l.wm.RegisterCallback(GetFileExtTag, func([]byte) {
+		jww.DEBUG.Print("[LOG] Received file requested from external " +
+			"Javascript. Ignoring file.")
+	})
+
+	data, err := json.Marshal(l.maxLogFileSize)
+	if err != nil {
+		return err
+	}
+
+	// Send message to initialize the log file listener
+	errChan := make(chan error)
+	l.wm.SendMessage(NewLogFileTag, data, func(data []byte) {
+		if len(data) > 0 {
+			errChan <- errors.New(string(data))
+		} else {
+			errChan <- nil
+		}
+	})
+
+	// Wait for worker to respond
+	select {
+	case err = <-errChan:
+		if err != nil {
+			return err
+		}
+	case <-time.After(worker.ResponseTimeout):
+		return errors.Errorf("timed out after %s waiting for new log "+
+			"file in worker to initialize", worker.ResponseTimeout)
+	}
+
+	jww.INFO.Printf("[LOG] Initialized log to file web worker %s.", workerName)
+
+	sendLog := func(p []byte) { l.wm.SendMessage(WriteLogTag, p, nil) }
+	go l.processLog(workerMode, sendLog, l.processQuit)
+
+	return nil
+}
+
+// processLog processes the log messages sent to the listener channel and sends
+// them to the appropriate recorder.
+func (l *Logger) processLog(m mode, sendLog func(p []byte), quit chan struct{}) {
+	jww.INFO.Printf("[LOG] Starting log file processing thread in %s.", m)
+
+	for {
+		select {
+		case <-quit:
+			jww.INFO.Printf("[LOG] Stopping log file processing thread.")
+			return
+		case p := <-l.listenChan:
+			go sendLog(p)
+		}
+	}
+}
+
+// prepare sets the threshold, maxLogFileSize, and mode of the logger and
+// prints a log message indicating this information.
+func (l *Logger) prepare(
+	threshold jww.Threshold, maxLogFileSize int, m mode) error {
+	if m := l.getMode(); m != initMode {
+		return errors.Errorf("log already set to %s", m)
+	} else if threshold < jww.LevelTrace || threshold > jww.LevelFatal {
+		return errors.Errorf("log level of %d is invalid", threshold)
+	}
+
+	l.threshold = threshold
+	l.maxLogFileSize = maxLogFileSize
+	l.setMode(m)
+
+	msg := fmt.Sprintf("[LOG] Outputting log to file in %s of max size %d "+
+		"with level %s", m, l.MaxSize(), l.Threshold())
+	switch l.Threshold() {
+	case jww.LevelTrace:
+		fallthrough
+	case jww.LevelDebug:
+		fallthrough
+	case jww.LevelInfo:
+		jww.INFO.Print(msg)
+	case jww.LevelWarn:
+		jww.WARN.Print(msg)
+	case jww.LevelError:
+		jww.ERROR.Print(msg)
+	case jww.LevelCritical:
+		jww.CRITICAL.Print(msg)
+	case jww.LevelFatal:
+		jww.FATAL.Print(msg)
+	}
+
+	return nil
+}
+
+// StopLogging stops the logging of log messages and disables the log listener.
+// If the log worker is running, it is terminated. Once logging is stopped, it
+// cannot be resumed the log file cannot be recovered.
+func (l *Logger) StopLogging() {
+	jww.DEBUG.Printf("[LOG] Removing log listener with ID %d", l.logListenerID)
+	RemoveLogListener(l.logListenerID)
+
+	switch l.getMode() {
+	case workerMode:
+		go l.wm.Terminate()
+		jww.DEBUG.Printf("[LOG] Terminated log worker.")
+	case fileMode:
+		jww.DEBUG.Printf("[LOG] Reset circular buffer.")
+		l.cb.Reset()
+	}
+
+	select {
+	case l.processQuit <- struct{}{}:
+		jww.DEBUG.Printf("[LOG] Sent quit channel to log process.")
+	default:
+		jww.DEBUG.Printf("[LOG] Failed to stop log processes.")
+	}
+}
+
+// GetFile returns the entire log file.
+//
+// If the log file is listening locally, it returns it from the local buffer. If
+// it is listening from the worker, it blocks until the file is returned.
+func (l *Logger) GetFile() []byte {
+	switch l.getMode() {
+	case fileMode:
+		return l.cb.Bytes()
+	case workerMode:
+		fileChan := make(chan []byte)
+		l.wm.SendMessage(GetFileTag, nil, func(data []byte) { fileChan <- data })
+
+		select {
+		case file := <-fileChan:
+			return file
+		case <-time.After(worker.ResponseTimeout):
+			jww.FATAL.Panicf("[LOG] Timed out after %s waiting for log "+
+				"file from worker", worker.ResponseTimeout)
+			return nil
+		}
+	default:
+		return nil
+	}
+}
+
+// Threshold returns the log level threshold used in the file.
+func (l *Logger) Threshold() jww.Threshold {
+	return l.threshold
+}
+
+// MaxSize returns the max size, in bytes, that the log file is allowed to be.
+func (l *Logger) MaxSize() int {
+	return l.maxLogFileSize
+}
+
+// Size returns the current size, in bytes, written to the log file.
+//
+// If the log file is listening locally, it returns it from the local buffer. If
+// it is listening from the worker, it blocks until the size is returned.
+func (l *Logger) Size() int {
+	switch l.getMode() {
+	case fileMode:
+		return int(l.cb.Size())
+	case workerMode:
+		sizeChan := make(chan []byte)
+		l.wm.SendMessage(SizeTag, nil, func(data []byte) { sizeChan <- data })
+
+		select {
+		case data := <-sizeChan:
+			return int(jww.Threshold(binary.LittleEndian.Uint64(data)))
+		case <-time.After(worker.ResponseTimeout):
+			jww.FATAL.Panicf("[LOG] Timed out after %s waiting for log "+
+				"file size from worker", worker.ResponseTimeout)
+			return 0
+		}
+	default:
+		return 0
+	}
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// JWW Listener                                                               //
+////////////////////////////////////////////////////////////////////////////////
+
+// Listen is called for every logging event. This function adheres to the
+// [jwalterweatherman.LogListener] type.
+func (l *Logger) Listen(t jww.Threshold) io.Writer {
+	if t < l.threshold {
+		return nil
+	}
+
+	return l
+}
+
+// Write sends the bytes to the listener channel. It always returns the length
+// of p and a nil error. This function adheres to the io.Writer interface.
+func (l *Logger) Write(p []byte) (n int, err error) {
+	select {
+	case l.listenChan <- append([]byte{}, p...):
+	default:
+		jww.ERROR.Printf(
+			"[LOG] Logger channel filled. Log file recording stopping.")
+		l.StopLogging()
+		return 0, errors.Errorf(
+			"Logger channel filled. Log file recording stopping.")
+	}
+	return len(p), nil
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// Log File Mode                                                              //
+////////////////////////////////////////////////////////////////////////////////
+
+// mode represents the state of the Logger.
+type mode uint32
+
+const (
+	initMode mode = iota
+	fileMode
+	workerMode
+)
+
+func (l *Logger) setMode(m mode) { l.mode.Store(uint32(m)) }
+func (l *Logger) getMode() mode  { return mode(l.mode.Load()) }
+
+// String returns a human-readable representation of the mode for logging and
+// debugging. This function adheres to the fmt.Stringer interface.
+func (m mode) String() string {
+	switch m {
+	case initMode:
+		return "uninitialized mode"
+	case fileMode:
+		return "file mode"
+	case workerMode:
+		return "worker mode"
+	default:
+		return "invalid mode: " + strconv.Itoa(int(m))
+	}
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// Javascript Bindings                                                        //
+////////////////////////////////////////////////////////////////////////////////
+
+// GetLoggerJS returns the Logger object, used to manager where logging is
+// recorded and accessing the log file.
+//
+// Returns:
+//   - A Javascript representation of the [Logger] object.
+func GetLoggerJS(js.Value, []js.Value) any {
+	return newLoggerJS(GetLogger())
+}
+
+// newLoggerJS creates a new Javascript compatible object (map[string]any) that
+// matches the [Logger] structure.
+func newLoggerJS(lfw *Logger) map[string]any {
+	logFileWorker := map[string]any{
+		"LogToFile":       js.FuncOf(lfw.LogToFileJS),
+		"LogToFileWorker": js.FuncOf(lfw.LogToFileWorkerJS),
+		"StopLogging":     js.FuncOf(lfw.StopLoggingJS),
+		"GetFile":         js.FuncOf(lfw.GetFileJS),
+		"Threshold":       js.FuncOf(lfw.ThresholdJS),
+		"MaxSize":         js.FuncOf(lfw.MaxSizeJS),
+		"Size":            js.FuncOf(lfw.SizeJS),
+		"Worker":          js.FuncOf(lfw.WorkerJS),
+	}
+
+	return logFileWorker
+}
+
+// LogToFileJS starts logging to a local, in-memory log file.
+//
+// Parameters:
+//   - args[0] - Log level (int).
+//   - args[1] - Max log file size, in bytes (int).
+//
+// Returns:
+//   - Throws a TypeError if starting the log file fails.
+func (l *Logger) LogToFileJS(_ js.Value, args []js.Value) any {
+	threshold := jww.Threshold(args[0].Int())
+	maxLogFileSize := args[1].Int()
+
+	err := l.LogToFile(threshold, maxLogFileSize)
+	if err != nil {
+		utils.Throw(utils.TypeError, err)
+		return nil
+	}
+
+	return nil
+}
+
+// LogToFileWorkerJS starts a new worker that begins listening for logs and
+// writing them to file. This function blocks until the worker has started.
+//
+// Parameters:
+//   - args[0] - Log level (int).
+//   - args[1] - Max log file size, in bytes (int).
+//   - args[2] - Path to Javascript start file for the worker WASM (string).
+//   - args[3] - Name of the worker (used in logs) (string).
+//
+// Returns a promise:
+//   - Resolves to nothing on success (void).
+//   - Rejected with an error if starting the worker fails.
+func (l *Logger) LogToFileWorkerJS(_ js.Value, args []js.Value) any {
+	threshold := jww.Threshold(args[0].Int())
+	maxLogFileSize := args[1].Int()
+	wasmJsPath := args[2].String()
+	workerName := args[3].String()
+
+	promiseFn := func(resolve, reject func(args ...any) js.Value) {
+		err := l.LogToFileWorker(
+			threshold, maxLogFileSize, wasmJsPath, workerName)
+		if err != nil {
+			reject(utils.JsTrace(err))
+		} else {
+			resolve()
+		}
+	}
+
+	return utils.CreatePromise(promiseFn)
+}
+
+// StopLoggingJS stops the logging of log messages and disables the log
+// listener. If the log worker is running, it is terminated. Once logging is
+// stopped, it cannot be resumed the log file cannot be recovered.
+func (l *Logger) StopLoggingJS(js.Value, []js.Value) any {
+	l.StopLogging()
+
+	return nil
+}
+
+// GetFileJS returns the entire log file.
+//
+// If the log file is listening locally, it returns it from the local buffer. If
+// it is listening from the worker, it blocks until the file is returned.
+//
+// Returns a promise:
+//   - Resolves to the log file contents (string).
+func (l *Logger) GetFileJS(js.Value, []js.Value) any {
+	promiseFn := func(resolve, _ func(args ...any) js.Value) {
+		resolve(string(l.GetFile()))
+	}
+
+	return utils.CreatePromise(promiseFn)
+}
+
+// ThresholdJS returns the log level threshold used in the file.
+//
+// Returns:
+//   - Log level (int).
+func (l *Logger) ThresholdJS(js.Value, []js.Value) any {
+	return int(l.Threshold())
+}
+
+// MaxSizeJS returns the max size, in bytes, that the log file is allowed to be.
+//
+// Returns:
+//   - Max file size (int).
+func (l *Logger) MaxSizeJS(js.Value, []js.Value) any {
+	return l.MaxSize()
+}
+
+// SizeJS returns the current size, in bytes, written to the log file.
+//
+// If the log file is listening locally, it returns it from the local buffer. If
+// it is listening from the worker, it blocks until the size is returned.
+//
+// Returns a promise:
+//   - Resolves to the current file size (int).
+func (l *Logger) SizeJS(js.Value, []js.Value) any {
+	promiseFn := func(resolve, _ func(args ...any) js.Value) {
+		resolve(l.Size())
+	}
+
+	return utils.CreatePromise(promiseFn)
+}
+
+// WorkerJS returns the web worker object.
+//
+// Returns:
+//   - Javascript worker object. If the worker has not been initialized, it
+//     returns null.
+func (l *Logger) WorkerJS(js.Value, []js.Value) any {
+	if l.getMode() == workerMode {
+		return l.wm.GetWorker()
+	}
+
+	return js.Null()
+}
diff --git a/logging/logger_test.go b/logging/logger_test.go
new file mode 100644
index 0000000000000000000000000000000000000000..0b5267be9cabaf995ddc8fcdba4b5d3ec8eea133
--- /dev/null
+++ b/logging/logger_test.go
@@ -0,0 +1,172 @@
+////////////////////////////////////////////////////////////////////////////////
+// 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 logging
+
+import (
+	"bytes"
+	"fmt"
+	jww "github.com/spf13/jwalterweatherman"
+	"testing"
+)
+
+// Tests InitLogger
+func TestInitLogger(t *testing.T) {
+}
+
+// Tests GetLogger
+func TestGetLogger(t *testing.T) {
+}
+
+// Tests NewLogger
+func TestNewLogger(t *testing.T) {
+}
+
+// Tests Logger.LogToFile
+func TestLogger_LogToFile(t *testing.T) {
+	jww.SetStdoutThreshold(jww.LevelTrace)
+	l := NewLogger()
+
+	err := l.LogToFile(jww.LevelTrace, 50000000)
+	if err != nil {
+		t.Fatalf("Failed to LogToFile: %+v", err)
+	}
+
+	jww.INFO.Printf("test")
+
+	file := l.cb.Bytes()
+	fmt.Printf("file:----------------------------\n%s\n---------------------------------\n", file)
+}
+
+// Tests Logger.LogToFileWorker
+func TestLogger_LogToFileWorker(t *testing.T) {
+}
+
+// Tests Logger.processLog
+func TestLogger_processLog(t *testing.T) {
+}
+
+// Tests Logger.prepare
+func TestLogger_prepare(t *testing.T) {
+}
+
+// Tests Logger.StopLogging
+func TestLogger_StopLogging(t *testing.T) {
+}
+
+// Tests Logger.GetFile
+func TestLogger_GetFile(t *testing.T) {
+}
+
+// Tests Logger.Threshold
+func TestLogger_Threshold(t *testing.T) {
+}
+
+// Tests Logger.MaxSize
+func TestLogger_MaxSize(t *testing.T) {
+}
+
+// Tests Logger.Size
+func TestLogger_Size(t *testing.T) {
+}
+
+// Tests Logger.Listen
+func TestLogger_Listen(t *testing.T) {
+
+	// l := newLogger()
+
+}
+
+// Tests that Logger.Write can fill the listenChan channel completely and that
+// all messages are received in the order they were added.
+func TestLogger_Write(t *testing.T) {
+	l := newLogger()
+	expectedLogs := make([][]byte, logListenerChanSize)
+
+	for i := range expectedLogs {
+		p := []byte(
+			fmt.Sprintf("Log message %d of %d.", i+1, logListenerChanSize))
+		expectedLogs[i] = p
+		n, err := l.Listen(jww.LevelError).Write(p)
+		if err != nil {
+			t.Errorf("Received impossible error (%d): %+v", i, err)
+		} else if n != len(p) {
+			t.Errorf("Received incorrect bytes written (%d)."+
+				"\nexpected: %d\nreceived: %d", i, len(p), n)
+		}
+	}
+
+	for i, expected := range expectedLogs {
+		select {
+		case received := <-l.listenChan:
+			if !bytes.Equal(expected, received) {
+				t.Errorf("Received unexpected meessage (%d)."+
+					"\nexpected: %q\nreceived: %q", i, expected, received)
+			}
+		default:
+			t.Errorf("Failed to read from channel.")
+		}
+	}
+}
+
+// Error path: Tests that Logger.Write returns an error when the listener
+// channel is full.
+func TestLogger_Write_ChannelFilledError(t *testing.T) {
+	l := newLogger()
+	expectedLogs := make([][]byte, logListenerChanSize)
+
+	for i := range expectedLogs {
+		p := []byte(
+			fmt.Sprintf("Log message %d of %d.", i+1, logListenerChanSize))
+		expectedLogs[i] = p
+		n, err := l.Listen(jww.LevelError).Write(p)
+		if err != nil {
+			t.Errorf("Received impossible error (%d): %+v", i, err)
+		} else if n != len(p) {
+			t.Errorf("Received incorrect bytes written (%d)."+
+				"\nexpected: %d\nreceived: %d", i, len(p), n)
+		}
+	}
+
+	_, err := l.Write([]byte("test"))
+	if err == nil {
+		t.Error("Failed to receive error when the chanel should be full.")
+	}
+}
+
+// Tests that Logger.getMode gets the same value set with Logger.setMode.
+func TestLogger_setMode_getMode(t *testing.T) {
+	l := newLogger()
+
+	for i, m := range []mode{initMode, fileMode, workerMode, 12} {
+		l.setMode(m)
+		received := l.getMode()
+		if m != received {
+			t.Errorf("Received wrong mode (%d).\nexpected: %s\nreceived: %s",
+				i, m, received)
+		}
+	}
+
+}
+
+// Unit test of mode.String.
+func Test_mode_String(t *testing.T) {
+	for m, expected := range map[mode]string{
+		initMode:   "uninitialized mode",
+		fileMode:   "file mode",
+		workerMode: "worker mode",
+		12:         "invalid mode: 12",
+	} {
+		s := m.String()
+		if s != expected {
+			t.Errorf("Wrong string for mode %d.\nexpected: %s\nreceived: %s",
+				m, expected, s)
+		}
+	}
+}
diff --git a/logging/workerThread/logFileWorker.js b/logging/workerThread/logFileWorker.js
new file mode 100644
index 0000000000000000000000000000000000000000..159bfaa0d919a4f0cb2758af48d80c65891e7820
--- /dev/null
+++ b/logging/workerThread/logFileWorker.js
@@ -0,0 +1,17 @@
+////////////////////////////////////////////////////////////////////////////////
+// Copyright © 2022 xx foundation                                             //
+//                                                                            //
+// Use of this source code is governed by a license that can be found in the  //
+// LICENSE file.                                                              //
+////////////////////////////////////////////////////////////////////////////////
+
+importScripts('wasm_exec.js');
+
+const go = new Go();
+const binPath = 'xxdk-logFileWorker.wasm'
+WebAssembly.instantiateStreaming(fetch(binPath), go.importObject).then((result) => {
+    go.run(result.instance);
+    LogLevel(1);
+}).catch((err) => {
+    console.error(err);
+});
\ No newline at end of file
diff --git a/logging/workerThread/main.go b/logging/workerThread/main.go
new file mode 100644
index 0000000000000000000000000000000000000000..91b059f3da1195b0ff244027608a0f8a98482fed
--- /dev/null
+++ b/logging/workerThread/main.go
@@ -0,0 +1,110 @@
+////////////////////////////////////////////////////////////////////////////////
+// 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 main
+
+import (
+	"encoding/binary"
+	"encoding/json"
+	"fmt"
+	"github.com/armon/circbuf"
+	"github.com/pkg/errors"
+	jww "github.com/spf13/jwalterweatherman"
+	"gitlab.com/elixxir/xxdk-wasm/logging"
+	"gitlab.com/elixxir/xxdk-wasm/worker"
+	"syscall/js"
+)
+
+// SEMVER is the current semantic version of the xxDK Logger web worker.
+const SEMVER = "0.1.0"
+
+func init() {
+	// Set up Javascript console listener set at level INFO
+	ll := logging.NewJsConsoleLogListener(jww.LevelDebug)
+	logging.AddLogListener(ll.Listen)
+	jww.SetStdoutThreshold(jww.LevelFatal + 1)
+	jww.INFO.Printf("xxDK Logger web worker version: v%s", SEMVER)
+}
+
+// workerLogFile manages communication with the main thread and writing incoming
+// logging messages to the log file.
+type workerLogFile struct {
+	wtm *worker.ThreadManager
+	b   *circbuf.Buffer
+}
+
+func main() {
+	jww.INFO.Print("[LOG] Starting xxDK WebAssembly Logger Worker.")
+
+	js.Global().Set("LogLevel", js.FuncOf(logging.LogLevelJS))
+
+	wlf := workerLogFile{wtm: worker.NewThreadManager("Logger", false)}
+
+	wlf.registerCallbacks()
+
+	wlf.wtm.SignalReady()
+	<-make(chan bool)
+	fmt.Println("[WW] Closing xxDK WebAssembly Log Worker.")
+}
+
+// registerCallbacks registers all the necessary callbacks for the main thread
+// to get the file and file metadata.
+func (wlf *workerLogFile) registerCallbacks() {
+	// Callback for logging.LogToFileWorker
+	wlf.wtm.RegisterCallback(logging.NewLogFileTag,
+		func(data []byte) ([]byte, error) {
+			var maxLogFileSize int64
+			err := json.Unmarshal(data, &maxLogFileSize)
+			if err != nil {
+				return []byte(err.Error()), err
+			}
+
+			wlf.b, err = circbuf.NewBuffer(maxLogFileSize)
+			if err != nil {
+				return []byte(err.Error()), err
+			}
+
+			jww.DEBUG.Printf("[LOG] Created new worker log file of size %d",
+				maxLogFileSize)
+
+			return []byte{}, nil
+		})
+
+	// Callback for Logging.GetFile
+	wlf.wtm.RegisterCallback(logging.WriteLogTag,
+		func(data []byte) ([]byte, error) {
+			n, err := wlf.b.Write(data)
+			if err != nil {
+				return nil, err
+			} else if n != len(data) {
+				return nil, errors.Errorf(
+					"wrote %d bytes; expected %d bytes", n, len(data))
+			}
+
+			return nil, nil
+		},
+	)
+
+	// Callback for Logging.GetFile
+	wlf.wtm.RegisterCallback(logging.GetFileTag, func([]byte) ([]byte, error) {
+		return wlf.b.Bytes(), nil
+	})
+
+	// Callback for Logging.GetFile
+	wlf.wtm.RegisterCallback(logging.GetFileExtTag, func([]byte) ([]byte, error) {
+		return wlf.b.Bytes(), nil
+	})
+
+	// Callback for Logging.Size
+	wlf.wtm.RegisterCallback(logging.SizeTag, func([]byte) ([]byte, error) {
+		b := make([]byte, 8)
+		binary.LittleEndian.PutUint64(b, uint64(wlf.b.TotalWritten()))
+		return b, nil
+	})
+}
diff --git a/main.go b/main.go
index fe7680c76b0dbb14c74200a3cca495e5fa582e15..4755223854f7ef0370086bf0b541964639104037 100644
--- a/main.go
+++ b/main.go
@@ -10,22 +10,24 @@
 package main
 
 import (
-	"fmt"
+	"gitlab.com/elixxir/xxdk-wasm/logging"
 	"os"
 	"syscall/js"
 
 	jww "github.com/spf13/jwalterweatherman"
-	"gitlab.com/elixxir/client/v4/bindings"
 	"gitlab.com/elixxir/xxdk-wasm/storage"
 	"gitlab.com/elixxir/xxdk-wasm/utils"
 	"gitlab.com/elixxir/xxdk-wasm/wasm"
 )
 
 func init() {
+	// Start logger first to capture all logging events
+	logging.InitLogger()
+
 	// Overwrites setting the log level to INFO done in bindings so that the
 	// Javascript console can be used
-	ll := wasm.NewJsConsoleLogListener(jww.LevelInfo)
-	jww.SetLogListeners(ll.Listen)
+	ll := logging.NewJsConsoleLogListener(jww.LevelInfo)
+	logging.AddLogListener(ll.Listen)
 	jww.SetStdoutThreshold(jww.LevelFatal + 1)
 
 	// Check that the WASM binary version is correct
@@ -36,8 +38,10 @@ func init() {
 }
 
 func main() {
-	fmt.Println("Starting xxDK WebAssembly bindings.")
-	fmt.Printf("Client version %s\n", bindings.GetVersion())
+	jww.INFO.Printf("Starting xxDK WebAssembly bindings.")
+
+	// logging/worker.go
+	js.Global().Set("GetLogger", js.FuncOf(logging.GetLoggerJS))
 
 	// storage/password.go
 	js.Global().Set("GetOrInitPassword", js.FuncOf(storage.GetOrInitPassword))
@@ -147,7 +151,6 @@ func main() {
 
 	// wasm/logging.go
 	js.Global().Set("LogLevel", js.FuncOf(wasm.LogLevel))
-	js.Global().Set("LogToFile", js.FuncOf(wasm.LogToFile))
 	js.Global().Set("RegisterLogWriter", js.FuncOf(wasm.RegisterLogWriter))
 	js.Global().Set("EnableGrpcLogs", js.FuncOf(wasm.EnableGrpcLogs))
 
diff --git a/storage/indexedDbEncryptionTrack.go b/storage/indexedDbEncryptionTrack.go
index 6e52a2648d3ea076578e9697fa3e9b591edf043f..d568a63787490ea0979a189191272da70d957e05 100644
--- a/storage/indexedDbEncryptionTrack.go
+++ b/storage/indexedDbEncryptionTrack.go
@@ -18,16 +18,17 @@ import (
 const databaseEncryptionToggleKey = "xxdkWasmDatabaseEncryptionToggle/"
 
 // StoreIndexedDbEncryptionStatus stores the encryption status if it has not
-// been previously saved. If it has, it returns its value.
+// been previously saved. If it has, then it returns its value.
 func StoreIndexedDbEncryptionStatus(
-	databaseName string, encryption bool) (bool, error) {
+	databaseName string, encryptionStatus bool) (
+	loadedEncryptionStatus bool, err error) {
 	data, err := GetLocalStorage().GetItem(
 		databaseEncryptionToggleKey + databaseName)
 	if err != nil {
 		if errors.Is(err, os.ErrNotExist) {
 			GetLocalStorage().SetItem(
 				databaseEncryptionToggleKey+databaseName, []byte{1})
-			return encryption, nil
+			return encryptionStatus, nil
 		} else {
 			return false, err
 		}
diff --git a/storage/indexedDbEncryptionTrack_test.go b/storage/indexedDbEncryptionTrack_test.go
index ec754f87a940e14f6802e347f86f44fa437f4ff9..490d720be4616eba5118af797f0fbdaf1e606daf 100644
--- a/storage/indexedDbEncryptionTrack_test.go
+++ b/storage/indexedDbEncryptionTrack_test.go
@@ -18,23 +18,23 @@ import (
 func TestStoreIndexedDbEncryptionStatus(t *testing.T) {
 	databaseName := "databaseA"
 
-	encrypted, err := StoreIndexedDbEncryptionStatus(databaseName, true)
+	encryptionStatus, err := StoreIndexedDbEncryptionStatus(databaseName, true)
 	if err != nil {
 		t.Errorf("Failed to store/get encryption status: %+v", err)
 	}
 
-	if encrypted != true {
+	if encryptionStatus != true {
 		t.Errorf("Incorrect encryption values.\nexpected: %t\nreceived: %t",
-			true, encrypted)
+			true, encryptionStatus)
 	}
 
-	encrypted, err = StoreIndexedDbEncryptionStatus(databaseName, false)
+	encryptionStatus, err = StoreIndexedDbEncryptionStatus(databaseName, false)
 	if err != nil {
 		t.Errorf("Failed to store/get encryption status: %+v", err)
 	}
 
-	if encrypted != true {
+	if encryptionStatus != true {
 		t.Errorf("Incorrect encryption values.\nexpected: %t\nreceived: %t",
-			true, encrypted)
+			true, encryptionStatus)
 	}
 }
diff --git a/utils/convert.go b/utils/convert.go
index a5c79e309529089ebb918fc77f701f7a633c8d05..b1f2cd10172bca7e88ff6c77931c754ab1b7f1d8 100644
--- a/utils/convert.go
+++ b/utils/convert.go
@@ -49,3 +49,14 @@ func JsonToJS(inputJson []byte) (js.Value, error) {
 
 	return js.ValueOf(jsObj), nil
 }
+
+// JsErrorToJson converts the Javascript error to JSON. This should be used for
+// all Javascript error objects instead of JsonToJS.
+func JsErrorToJson(value js.Value) string {
+	if value.IsUndefined() {
+		return "null"
+	}
+
+	properties := Object.Call("getOwnPropertyNames", value)
+	return JSON.Call("stringify", value, properties).String()
+}
diff --git a/utils/convert_test.go b/utils/convert_test.go
index 74bd9ca6c68e5e95639f413f2da0ab58b5faed31..508c27f783f1365f28b4b7b78b7389fc4d3ff1a6 100644
--- a/utils/convert_test.go
+++ b/utils/convert_test.go
@@ -241,3 +241,65 @@ func TestJsonToJSJsToJson(t *testing.T) {
 			"\nexpected: %s\nreceived: %s", jsonData, jsJson)
 	}
 }
+
+// Tests that JsErrorToJson can convert a Javascript object to JSON that matches
+// the output of json.Marshal on the Go version of the same object.
+func TestJsErrorToJson(t *testing.T) {
+	testObj := map[string]any{
+		"nil":    nil,
+		"bool":   true,
+		"int":    1,
+		"float":  1.5,
+		"string": "I am string",
+		"array":  []any{1, 2, 3},
+		"object": map[string]any{"int": 5},
+	}
+
+	expected, err := json.Marshal(testObj)
+	if err != nil {
+		t.Errorf("Failed to JSON marshal test object: %+v", err)
+	}
+
+	jsJson := JsErrorToJson(js.ValueOf(testObj))
+
+	// Javascript does not return the JSON object fields sorted so the letters
+	// of each Javascript string are sorted and compared
+	er := []rune(string(expected))
+	sort.SliceStable(er, func(i, j int) bool { return er[i] < er[j] })
+	jj := []rune(jsJson)
+	sort.SliceStable(jj, func(i, j int) bool { return jj[i] < jj[j] })
+
+	if string(er) != string(jj) {
+		t.Errorf("Recieved incorrect JSON from Javascript object."+
+			"\nexpected: %s\nreceived: %s", expected, jsJson)
+	}
+}
+
+// Tests that JsErrorToJson return a null object when the Javascript object is
+// undefined.
+func TestJsErrorToJson_Undefined(t *testing.T) {
+	expected, err := json.Marshal(nil)
+	if err != nil {
+		t.Errorf("Failed to JSON marshal test object: %+v", err)
+	}
+
+	jsJson := JsErrorToJson(js.Undefined())
+
+	if string(expected) != jsJson {
+		t.Errorf("Recieved incorrect JSON from Javascript object."+
+			"\nexpected: %s\nreceived: %s", expected, jsJson)
+	}
+}
+
+// Tests that JsErrorToJson returns a JSON object containing the original error
+// string.
+func TestJsErrorToJson_ErrorObject(t *testing.T) {
+	expected := "An error"
+	jsErr := Error.New(expected)
+	jsJson := JsErrorToJson(jsErr)
+
+	if !strings.Contains(jsJson, expected) {
+		t.Errorf("Recieved incorrect JSON from Javascript error."+
+			"\nexpected: %s\nreceived: %s", expected, jsJson)
+	}
+}
diff --git a/wasm/channels.go b/wasm/channels.go
index 2248b1b31d768b84982d893654e7d1b6f959cd5b..43ae72bdf4ad9b01441c20c800ca9bafbdcf7620 100644
--- a/wasm/channels.go
+++ b/wasm/channels.go
@@ -16,7 +16,7 @@ import (
 	"errors"
 	"gitlab.com/elixxir/client/v4/channels"
 	"gitlab.com/elixxir/crypto/message"
-	channelsDb "gitlab.com/elixxir/xxdk-wasm/indexedDb/channels"
+	channelsDb "gitlab.com/elixxir/xxdk-wasm/indexedDb/worker/channels"
 	"gitlab.com/xx_network/primitives/id"
 	"sync"
 	"syscall/js"
@@ -319,9 +319,10 @@ func LoadChannelsManager(_ js.Value, args []js.Value) any {
 // Parameters:
 //   - args[0] - ID of [Cmix] object in tracker (int). This can be retrieved
 //     using [Cmix.GetID].
-//   - args[1] - Bytes of a private identity ([channel.PrivateIdentity]) that is
+//   - args[1] - Path to Javascript file that starts the worker (string).
+//   - args[2] - Bytes of a private identity ([channel.PrivateIdentity]) that is
 //     generated by [GenerateChannelIdentity] (Uint8Array).
-//   - args[2] - The received message callback, which is called everytime a
+//   - args[3] - 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
@@ -329,15 +330,15 @@ func LoadChannelsManager(_ js.Value, args []js.Value) any {
 //     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
+//   - args[4] - 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
+//   - args[5] - 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
+//   - args[6] - ID of [ChannelDbCipher] object in tracker (int). Create this
 //     object with [NewChannelsDatabaseCipher] and get its id with
 //     [ChannelDbCipher.GetID].
 //
@@ -347,18 +348,19 @@ func LoadChannelsManager(_ js.Value, args []js.Value) any {
 //   - Throws a TypeError if the cipher ID does not correspond to a cipher.
 func NewChannelsManagerWithIndexedDb(_ js.Value, args []js.Value) any {
 	cmixID := args[0].Int()
-	privateIdentity := utils.CopyBytesToGo(args[1])
-	messageReceivedCB := args[2]
-	deletedMessageCB := args[3]
-	mutedUserCB := args[4]
-	cipherID := args[5].Int()
+	wasmJsPath := args[1].String()
+	privateIdentity := utils.CopyBytesToGo(args[2])
+	messageReceivedCB := args[3]
+	deletedMessageCB := args[4]
+	mutedUserCB := args[5]
+	cipherID := args[6].Int()
 
 	cipher, err := bindings.GetChannelDbCipherTrackerFromID(cipherID)
 	if err != nil {
 		utils.Throw(utils.TypeError, err)
 	}
 
-	return newChannelsManagerWithIndexedDb(cmixID, privateIdentity,
+	return newChannelsManagerWithIndexedDb(cmixID, wasmJsPath, privateIdentity,
 		messageReceivedCB, deletedMessageCB, mutedUserCB, cipher)
 }
 
@@ -377,9 +379,10 @@ func NewChannelsManagerWithIndexedDb(_ js.Value, args []js.Value) any {
 // Parameters:
 //   - args[0] - ID of [Cmix] object in tracker (int). This can be retrieved
 //     using [Cmix.GetID].
-//   - args[1] - Bytes of a private identity ([channel.PrivateIdentity]) that is
+//   - args[1] - Path to Javascript file that starts the worker (string).
+//   - args[2] - Bytes of a private identity ([channel.PrivateIdentity]) that is
 //     generated by [GenerateChannelIdentity] (Uint8Array).
-//   - args[2] - The received message callback, which is called everytime a
+//   - args[3] - 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
@@ -387,11 +390,11 @@ func NewChannelsManagerWithIndexedDb(_ js.Value, args []js.Value) any {
 //     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
+//   - args[4] - 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
+//   - args[5] - 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.
@@ -399,19 +402,21 @@ func NewChannelsManagerWithIndexedDb(_ js.Value, args []js.Value) any {
 // Returns a promise:
 //   - Resolves to a Javascript representation of the [ChannelsManager] object.
 //   - Rejected with an error if loading indexedDb or the manager fails.
+// FIXME: package names in comments for indexedDb
 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]
+	wasmJsPath := args[1].String()
+	privateIdentity := utils.CopyBytesToGo(args[2])
+	messageReceivedCB := args[3]
+	deletedMessageCB := args[4]
+	mutedUserCB := args[5]
 
-	return newChannelsManagerWithIndexedDb(cmixID, privateIdentity,
+	return newChannelsManagerWithIndexedDb(cmixID, wasmJsPath, privateIdentity,
 		messageReceivedCB, deletedMessageCB, mutedUserCB, nil)
 }
 
-func newChannelsManagerWithIndexedDb(cmixID int, privateIdentity []byte,
-	messageReceivedCB, deletedMessageCB, mutedUserCB js.Value,
+func newChannelsManagerWithIndexedDb(cmixID int, wasmJsPath string,
+	privateIdentity []byte, messageReceivedCB, deletedMessageCB, mutedUserCB js.Value,
 	cipher *bindings.ChannelDbCipher) any {
 
 	messageReceived := func(uuid uint64, channelID *id.ID, update bool) {
@@ -428,7 +433,7 @@ func newChannelsManagerWithIndexedDb(cmixID int, privateIdentity []byte,
 	}
 
 	model := channelsDb.NewWASMEventModelBuilder(
-		cipher, messageReceived, deletedMessage, mutedUser)
+		wasmJsPath, cipher, messageReceived, deletedMessage, mutedUser)
 
 	promiseFn := func(resolve, reject func(args ...any) js.Value) {
 		cm, err := bindings.NewChannelsManagerGoEventModel(
@@ -454,9 +459,10 @@ func newChannelsManagerWithIndexedDb(cmixID int, privateIdentity []byte,
 // Parameters:
 //   - args[0] - ID of [Cmix] object in tracker (int). This can be retrieved
 //     using [Cmix.GetID].
-//   - args[1] - The storage tag associated with the previously created channel
+//   - args[1] - Path to Javascript file that starts the worker (string).
+//   - args[2] - The storage tag associated with the previously created channel
 //     manager and retrieved with [ChannelsManager.GetStorageTag] (string).
-//   - args[2] - The received message callback, which is called everytime a
+//   - args[3] - 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
@@ -464,15 +470,15 @@ func newChannelsManagerWithIndexedDb(cmixID int, privateIdentity []byte,
 //     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
+//   - args[4] - 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
+//   - args[5] - 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
+//   - args[6] - ID of [ChannelDbCipher] object in tracker (int). Create this
 //     object with [NewChannelsDatabaseCipher] and get its id with
 //     [ChannelDbCipher.GetID].
 //
@@ -482,18 +488,19 @@ func newChannelsManagerWithIndexedDb(cmixID int, privateIdentity []byte,
 //   - Throws a TypeError if the cipher ID does not correspond to a cipher.
 func LoadChannelsManagerWithIndexedDb(_ js.Value, args []js.Value) any {
 	cmixID := args[0].Int()
-	storageTag := args[1].String()
-	messageReceivedCB := args[2]
-	deletedMessageCB := args[3]
-	mutedUserCB := args[4]
-	cipherID := args[5].Int()
+	wasmJsPath := args[1].String()
+	storageTag := args[2].String()
+	messageReceivedCB := args[3]
+	deletedMessageCB := args[4]
+	mutedUserCB := args[5]
+	cipherID := args[6].Int()
 
 	cipher, err := bindings.GetChannelDbCipherTrackerFromID(cipherID)
 	if err != nil {
 		utils.Throw(utils.TypeError, err)
 	}
 
-	return loadChannelsManagerWithIndexedDb(cmixID, storageTag,
+	return loadChannelsManagerWithIndexedDb(cmixID, wasmJsPath, storageTag,
 		messageReceivedCB, deletedMessageCB, mutedUserCB, cipher)
 }
 
@@ -510,9 +517,10 @@ func LoadChannelsManagerWithIndexedDb(_ js.Value, args []js.Value) any {
 // Parameters:
 //   - args[0] - ID of [Cmix] object in tracker (int). This can be retrieved
 //     using [Cmix.GetID].
-//   - args[1] - The storage tag associated with the previously created channel
+//   - args[1] - Path to Javascript file that starts the worker (string).
+//   - args[2] - The storage tag associated with the previously created channel
 //     manager and retrieved with [ChannelsManager.GetStorageTag] (string).
-//   - args[2] - The received message callback, which is called everytime a
+//   - args[3] - 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
@@ -520,11 +528,11 @@ func LoadChannelsManagerWithIndexedDb(_ js.Value, args []js.Value) any {
 //     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
+//   - args[4] - 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
+//   - args[5] - 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.
@@ -534,16 +542,17 @@ func LoadChannelsManagerWithIndexedDb(_ js.Value, args []js.Value) any {
 //   - Rejected with an error if loading indexedDb or the manager fails.
 func LoadChannelsManagerWithIndexedDbUnsafe(_ js.Value, args []js.Value) any {
 	cmixID := args[0].Int()
-	storageTag := args[1].String()
-	messageReceivedCB := args[2]
+	wasmJsPath := args[1].String()
+	storageTag := args[2].String()
+	messageReceivedCB := args[3]
 	deletedMessageCB := args[3]
 	mutedUserCB := args[4]
 
-	return loadChannelsManagerWithIndexedDb(cmixID, storageTag,
+	return loadChannelsManagerWithIndexedDb(cmixID, wasmJsPath, storageTag,
 		messageReceivedCB, deletedMessageCB, mutedUserCB, nil)
 }
 
-func loadChannelsManagerWithIndexedDb(cmixID int, storageTag string,
+func loadChannelsManagerWithIndexedDb(cmixID int, wasmJsPath, storageTag string,
 	messageReceivedCB, deletedMessageCB, mutedUserCB js.Value,
 	cipher *bindings.ChannelDbCipher) any {
 
@@ -561,7 +570,7 @@ func loadChannelsManagerWithIndexedDb(cmixID int, storageTag string,
 	}
 
 	model := channelsDb.NewWASMEventModelBuilder(
-		cipher, messageReceived, deletedMessage, mutedUser)
+		wasmJsPath, cipher, messageReceived, deletedMessage, mutedUser)
 
 	promiseFn := func(resolve, reject func(args ...any) js.Value) {
 		cm, err := bindings.LoadChannelsManagerGoEventModel(
diff --git a/wasm/dm.go b/wasm/dm.go
index 678ca74f529f1ccce1a0a8cac63242b5073ed835..04696435bce630ae9148db8bab580301dfdedb5f 100644
--- a/wasm/dm.go
+++ b/wasm/dm.go
@@ -13,7 +13,7 @@ import (
 	"crypto/ed25519"
 	"syscall/js"
 
-	indexDB "gitlab.com/elixxir/xxdk-wasm/indexedDb/dm"
+	indexDB "gitlab.com/elixxir/xxdk-wasm/indexedDb/worker/dm"
 
 	"encoding/base64"
 
@@ -129,7 +129,7 @@ func NewDMClient(_ js.Value, args []js.Value) any {
 }
 
 // NewDMClientWithIndexedDb creates a new [DMClient] from a new
-// private identity ([channel.PrivateIdentity]) and using indexedDb as a backend
+// private identity ([channel.PrivateIdentity]) and using indexedDbWorker as a backend
 // to manage the event model.
 //
 // This is for creating a manager for an identity for the first time. For
@@ -137,33 +137,35 @@ func NewDMClient(_ js.Value, args []js.Value) any {
 // reload this channel manager, use [LoadDMClientWithIndexedDb], passing
 // in the storage tag retrieved by [DMClient.GetStorageTag].
 //
-// This function initialises an indexedDb database.
+// This function initialises an indexedDbWorker database.
 //
 // Parameters:
 //   - args[0] - ID of [Cmix] object in tracker (int). This can be retrieved
 //     using [Cmix.GetID].
-//   - args[1] - Bytes of a private identity ([channel.PrivateIdentity]) that is
+//   - args[1] - Path to Javascript file that starts the worker (string).
+//   - args[2] - 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
+//   - args[3] - Function that takes in the same parameters as
+//     [indexedDbWorker.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[4] - ID of [ChannelDbCipher] object in tracker (int). Create this
 //     object with [NewChannelsDatabaseCipher] and get its id with
 //     [ChannelDbCipher.GetID].
 //
 // Returns a promise:
 //   - Resolves to a Javascript representation of the [DMClient] object.
-//   - Rejected with an error if loading indexedDb or the manager fails.
+//   - Rejected with an error if loading indexedDbWorker or the manager fails.
 //   - Throws a TypeError if the cipher ID does not correspond to a cipher.
 func NewDMClientWithIndexedDb(_ js.Value, args []js.Value) any {
 	cmixID := args[0].Int()
-	privateIdentity := utils.CopyBytesToGo(args[1])
-	messageReceivedCB := args[2]
-	cipherID := args[3].Int()
+	wasmJsPath := args[1].String()
+	privateIdentity := utils.CopyBytesToGo(args[2])
+	messageReceivedCB := args[3]
+	cipherID := args[4].Int()
 
 	cipher, err := bindings.GetChannelDbCipherTrackerFromID(cipherID)
 	if err != nil {
@@ -171,11 +173,11 @@ func NewDMClientWithIndexedDb(_ js.Value, args []js.Value) any {
 	}
 
 	return newDMClientWithIndexedDb(
-		cmixID, privateIdentity, messageReceivedCB, cipher)
+		cmixID, wasmJsPath, privateIdentity, messageReceivedCB, cipher)
 }
 
 // NewDMClientWithIndexedDbUnsafe creates a new [DMClient] from a
-// new private identity ([channel.PrivateIdentity]) and using indexedDb as a
+// new private identity ([channel.PrivateIdentity]) and using indexedDbWorker as a
 // backend to manage the event model. However, the data is written in plain text
 // and not encrypted. It is recommended that you do not use this in production.
 //
@@ -184,15 +186,16 @@ func NewDMClientWithIndexedDb(_ js.Value, args []js.Value) any {
 // reload this channel manager, use [LoadDMClientWithIndexedDbUnsafe],
 // passing in the storage tag retrieved by [DMClient.GetStorageTag].
 //
-// This function initialises an indexedDb database.
+// This function initialises an indexedDbWorker database.
 //
 // Parameters:
 //   - args[0] - ID of [Cmix] object in tracker (int). This can be retrieved
 //     using [Cmix.GetID].
-//   - args[1] - Bytes of a private identity ([channel.PrivateIdentity]) that is
+//   - args[1] - Path to Javascript file that starts the worker (string).
+//   - args[2] - 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
+//   - args[3] - Function that takes in the same parameters as
+//     [indexedDbWorker.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
@@ -201,18 +204,19 @@ func NewDMClientWithIndexedDb(_ js.Value, args []js.Value) any {
 //
 // Returns a promise:
 //   - Resolves to a Javascript representation of the [DMClient] object.
-//   - Rejected with an error if loading indexedDb or the manager fails.
+//   - Rejected with an error if loading indexedDbWorker or the manager fails.
 func NewDMClientWithIndexedDbUnsafe(_ js.Value, args []js.Value) any {
 	cmixID := args[0].Int()
-	privateIdentity := utils.CopyBytesToGo(args[1])
-	messageReceivedCB := args[2]
+	wasmJsPath := args[1].String()
+	privateIdentity := utils.CopyBytesToGo(args[2])
+	messageReceivedCB := args[3]
 
 	return newDMClientWithIndexedDb(
-		cmixID, privateIdentity, messageReceivedCB, nil)
+		cmixID, wasmJsPath, privateIdentity, messageReceivedCB, nil)
 }
 
-func newDMClientWithIndexedDb(cmixID int, privateIdentity []byte,
-	cb js.Value, cipher *bindings.ChannelDbCipher) any {
+func newDMClientWithIndexedDb(cmixID int, wasmJsPath string,
+	privateIdentity []byte, cb js.Value, cipher *bindings.ChannelDbCipher) any {
 
 	messageReceivedCB := func(uuid uint64, pubKey ed25519.PublicKey,
 		update bool) {
@@ -226,8 +230,8 @@ func newDMClientWithIndexedDb(cmixID int, privateIdentity []byte,
 			reject(utils.JsTrace(err))
 		}
 		dmPath := base64.RawStdEncoding.EncodeToString(pi.PubKey[:])
-		model, err := indexDB.NewWASMEventModel(dmPath, cipher,
-			messageReceivedCB)
+		model, err := indexDB.NewWASMEventModel(
+			dmPath, wasmJsPath, cipher, messageReceivedCB)
 		if err != nil {
 			reject(utils.JsTrace(err))
 		}
diff --git a/wasm/logging.go b/wasm/logging.go
index 46358b226f0549e1f05f3bb74c08f66a81a8198e..8199edc02a829a8e8b81b59dbfc73cddb4d46857 100644
--- a/wasm/logging.go
+++ b/wasm/logging.go
@@ -10,21 +10,11 @@
 package wasm
 
 import (
-	"fmt"
-	"github.com/armon/circbuf"
-	"github.com/pkg/errors"
-	jww "github.com/spf13/jwalterweatherman"
 	"gitlab.com/elixxir/client/v4/bindings"
-	"gitlab.com/elixxir/xxdk-wasm/utils"
-	"io"
-	"log"
+	"gitlab.com/elixxir/xxdk-wasm/logging"
 	"syscall/js"
 )
 
-// logListeners is a list of all registered log listeners. This is used to add
-// additional log listener without overwriting previously registered listeners.
-var logListeners []jww.LogListener
-
 // LogLevel sets level of logging. All logs at the set level and below will be
 // displayed (e.g., when log level is ERROR, only ERROR, CRITICAL, and FATAL
 // messages will be printed).
@@ -46,91 +36,8 @@ var logListeners []jww.LogListener
 //
 // Returns:
 //   - Throws TypeError if the log level is invalid.
-func LogLevel(_ js.Value, args []js.Value) any {
-	threshold := jww.Threshold(args[0].Int())
-	if threshold < jww.LevelTrace || threshold > jww.LevelFatal {
-		err := errors.Errorf("log level is not valid: log level: %d", threshold)
-		utils.Throw(utils.TypeError, err)
-		return nil
-	}
-
-	jww.SetLogThreshold(threshold)
-	jww.SetFlags(log.LstdFlags | log.Lmicroseconds)
-
-	ll := NewJsConsoleLogListener(threshold)
-	logListeners = append(logListeners, ll.Listen)
-	jww.SetLogListeners(logListeners...)
-	jww.SetStdoutThreshold(jww.LevelFatal + 1)
-
-	msg := fmt.Sprintf("Log level set to: %s", threshold)
-	switch threshold {
-	case jww.LevelTrace:
-		fallthrough
-	case jww.LevelDebug:
-		fallthrough
-	case jww.LevelInfo:
-		jww.INFO.Print(msg)
-	case jww.LevelWarn:
-		jww.WARN.Print(msg)
-	case jww.LevelError:
-		jww.ERROR.Print(msg)
-	case jww.LevelCritical:
-		jww.CRITICAL.Print(msg)
-	case jww.LevelFatal:
-		jww.FATAL.Print(msg)
-	}
-
-	return nil
-}
-
-// LogToFile enables logging to a file that can be downloaded.
-//
-// Parameters:
-//   - args[0] - Log level (int).
-//   - args[1] - Log file name (string).
-//   - args[2] - Max log file size, in bytes (int).
-//
-// Returns:
-//   - A Javascript representation of the [LogFile] object, which allows
-//     accessing the contents of the log file and other metadata.
-func LogToFile(_ js.Value, args []js.Value) any {
-	threshold := jww.Threshold(args[0].Int())
-	if threshold < jww.LevelTrace || threshold > jww.LevelFatal {
-		err := errors.Errorf("log level is not valid: log level: %d", threshold)
-		utils.Throw(utils.TypeError, err)
-		return nil
-	}
-
-	// Create new log file output
-	ll, err := NewLogFile(args[1].String(), threshold, args[2].Int())
-	if err != nil {
-		utils.Throw(utils.TypeError, err)
-		return nil
-	}
-
-	logListeners = append(logListeners, ll.Listen)
-	jww.SetLogListeners(logListeners...)
-
-	msg := fmt.Sprintf("Outputting log to file %s of max size %d with level %s",
-		ll.name, ll.b.Size(), threshold)
-	switch threshold {
-	case jww.LevelTrace:
-		fallthrough
-	case jww.LevelDebug:
-		fallthrough
-	case jww.LevelInfo:
-		jww.INFO.Print(msg)
-	case jww.LevelWarn:
-		jww.WARN.Print(msg)
-	case jww.LevelError:
-		jww.ERROR.Print(msg)
-	case jww.LevelCritical:
-		jww.CRITICAL.Print(msg)
-	case jww.LevelFatal:
-		jww.FATAL.Print(msg)
-	}
-
-	return newLogFileJS(ll)
+func LogLevel(this js.Value, args []js.Value) any {
+	return logging.LogLevelJS(this, args)
 }
 
 // logWriter wraps Javascript callbacks to adhere to the [bindings.LogWriter]
@@ -164,174 +71,3 @@ func EnableGrpcLogs(_ js.Value, args []js.Value) any {
 	bindings.EnableGrpcLogs(&logWriter{args[0].Invoke})
 	return nil
 }
-
-////////////////////////////////////////////////////////////////////////////////
-// Javascript Console Log Listener                                            //
-////////////////////////////////////////////////////////////////////////////////
-
-// console contains the Javascript console object, which provides access to the
-// browser's debugging console. This structure detects logging types and prints
-// it using the correct logging method.
-type console struct {
-	call string
-	js.Value
-}
-
-// Write writes the data to the Javascript console at the level specified by the
-// call.
-func (c *console) Write(p []byte) (n int, err error) {
-	c.Call(c.call, string(p))
-	return len(p), nil
-}
-
-// JsConsoleLogListener redirects log output to the Javascript console.
-type JsConsoleLogListener struct {
-	jww.Threshold
-	js.Value
-
-	trace    *console
-	debug    *console
-	info     *console
-	error    *console
-	warn     *console
-	critical *console
-	fatal    *console
-	def      *console
-}
-
-// NewJsConsoleLogListener initialises a new log listener that listener for the
-// specific threshold and prints the logs to the Javascript console.
-func NewJsConsoleLogListener(threshold jww.Threshold) *JsConsoleLogListener {
-	consoleObj := js.Global().Get("console")
-	return &JsConsoleLogListener{
-		Threshold: threshold,
-		Value:     consoleObj,
-		trace:     &console{"debug", consoleObj},
-		debug:     &console{"log", consoleObj},
-		info:      &console{"info", consoleObj},
-		warn:      &console{"warn", consoleObj},
-		error:     &console{"error", consoleObj},
-		critical:  &console{"error", consoleObj},
-		fatal:     &console{"error", consoleObj},
-		def:       &console{"log", consoleObj},
-	}
-}
-
-// Listen is called for every logging event. This function adheres to the
-// [jwalterweatherman.LogListener] type.
-func (ll *JsConsoleLogListener) Listen(t jww.Threshold) io.Writer {
-	if t < ll.Threshold {
-		return nil
-	}
-
-	switch t {
-	case jww.LevelTrace:
-		return ll.trace
-	case jww.LevelDebug:
-		return ll.debug
-	case jww.LevelInfo:
-		return ll.info
-	case jww.LevelWarn:
-		return ll.warn
-	case jww.LevelError:
-		return ll.error
-	case jww.LevelCritical:
-		return ll.critical
-	case jww.LevelFatal:
-		return ll.fatal
-	default:
-		return ll.def
-	}
-}
-
-////////////////////////////////////////////////////////////////////////////////
-// Log File Log Listener                                                      //
-////////////////////////////////////////////////////////////////////////////////
-
-// LogFile represents a virtual log file in memory. It contains a circular
-// buffer that limits the log file, overwriting the oldest logs.
-type LogFile struct {
-	name      string
-	threshold jww.Threshold
-	b         *circbuf.Buffer
-}
-
-// NewLogFile initialises a new [LogFile] for log writing.
-func NewLogFile(
-	name string, threshold jww.Threshold, maxSize int) (*LogFile, error) {
-	// Create new buffer of the specified size
-	b, err := circbuf.NewBuffer(int64(maxSize))
-	if err != nil {
-		return nil, err
-	}
-
-	return &LogFile{
-		name:      name,
-		threshold: threshold,
-		b:         b,
-	}, nil
-}
-
-// newLogFileJS creates a new Javascript compatible object (map[string]any) that
-// matches the [LogFile] structure.
-func newLogFileJS(lf *LogFile) map[string]any {
-	logFile := map[string]any{
-		"Name":      js.FuncOf(lf.Name),
-		"Threshold": js.FuncOf(lf.Threshold),
-		"GetFile":   js.FuncOf(lf.GetFile),
-		"MaxSize":   js.FuncOf(lf.MaxSize),
-		"Size":      js.FuncOf(lf.Size),
-	}
-
-	return logFile
-}
-
-// Listen is called for every logging event. This function adheres to the
-// [jwalterweatherman.LogListener] type.
-func (lf *LogFile) Listen(t jww.Threshold) io.Writer {
-	if t < lf.threshold {
-		return nil
-	}
-
-	return lf.b
-}
-
-// Name returns the name of the log file.
-//
-// Returns:
-//   - File name (string).
-func (lf *LogFile) Name(js.Value, []js.Value) any {
-	return lf.name
-}
-
-// Threshold returns the log level threshold used in the file.
-//
-// Returns:
-//   - Log level (string).
-func (lf *LogFile) Threshold(js.Value, []js.Value) any {
-	return lf.threshold.String()
-}
-
-// GetFile returns the entire log file.
-//
-// Returns:
-//   - Log file contents (string).
-func (lf *LogFile) GetFile(js.Value, []js.Value) any {
-	return string(lf.b.Bytes())
-}
-
-// MaxSize returns the max size, in bytes, that the log file is allowed to be.
-//
-// Returns:
-//   - Max file size (int).
-func (lf *LogFile) MaxSize(js.Value, []js.Value) any {
-	return lf.b.Size()
-}
-
-// Size returns the current size, in bytes, written to the log file.
-//
-// Returns:
-//   - Current file size (int).
-func (lf *LogFile) Size(js.Value, []js.Value) any {
-	return lf.b.TotalWritten()
-}
diff --git a/worker/README.md b/worker/README.md
new file mode 100644
index 0000000000000000000000000000000000000000..e5027c3fe7d366d5096bb9b09406ea6ab6edaa2f
--- /dev/null
+++ b/worker/README.md
@@ -0,0 +1,54 @@
+# Web Worker API
+
+This package allows you to create
+a [Javascript Web Worker](https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API)
+from WASM and facilitates communication between the web worker and the main
+thread using a messaging system.
+
+## Creating a Worker
+
+Web workers have two sides. There is the main side that creates the worker
+thread and the thread itself. The thread needs to be compiled into its own WASM
+binary and must have a corresponding Javascript file to launch it.
+
+Example `main.go`:
+
+```go
+package main
+
+import (
+	"fmt"
+	"gitlab.com/elixxir/xxdk-wasm/utils/worker"
+)
+
+func main() {
+	fmt.Println("Starting WebAssembly Worker.")
+	tm := worker.NewThreadManager("exampleWebWorker")
+	tm.SignalReady()
+	<-make(chan bool)
+}
+```
+
+Example WASM start file:
+
+```javascript
+importScripts('wasm_exec.js');
+
+const go = new Go();
+const binPath = 'xxdk-exampleWebWorker.wasm'
+WebAssembly.instantiateStreaming(fetch(binPath), go.importObject).then((result) => {
+    go.run(result.instance);
+}).catch((err) => {
+    console.error(err);
+});
+```
+
+To start the worker, call `worker.NewManager` with the Javascript file to launch
+the worker.
+
+```go
+wm, err := worker.NewManager("workerWasm.js", "exampleWebWorker")
+if err != nil {
+	return nil, err
+}
+```
\ No newline at end of file
diff --git a/worker/manager.go b/worker/manager.go
new file mode 100644
index 0000000000000000000000000000000000000000..1a28ac2f99e803b902cdac1e1696ec031d42ca48
--- /dev/null
+++ b/worker/manager.go
@@ -0,0 +1,340 @@
+////////////////////////////////////////////////////////////////////////////////
+// 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 worker
+
+import (
+	"encoding/json"
+	"github.com/pkg/errors"
+	jww "github.com/spf13/jwalterweatherman"
+	"gitlab.com/elixxir/xxdk-wasm/utils"
+	"sync"
+	"syscall/js"
+	"time"
+)
+
+// initID is the ID for the first item in the callback list. If the list only
+// contains one callback, then this is the ID of that callback. If the list has
+// autogenerated unique IDs, this is the initial ID to start at.
+const initID = uint64(0)
+
+// Response timeouts.
+const (
+	// workerInitialConnectionTimeout is the time to wait to receive initial
+	// contact from a new worker before timing out.
+	workerInitialConnectionTimeout = 90 * time.Second
+
+	// ResponseTimeout is the general time to wait after sending a message to
+	// receive a response before timing out.
+	ResponseTimeout = 30 * time.Second
+)
+
+// ReceptionCallback is the function that handles incoming data from the worker.
+type ReceptionCallback func(data []byte)
+
+// Manager manages the handling of messages received from the worker.
+type Manager struct {
+	// worker is the Worker Javascript object.
+	// Doc: https://developer.mozilla.org/en-US/docs/Web/API/Worker
+	worker js.Value
+
+	// callbacks are a list of ReceptionCallback that handle a specific message
+	// received from the worker. Each callback is keyed on a tag specifying how
+	// the received message should be handled. If the message is a reply to a
+	// message sent to the worker, then the callback is also keyed on a unique
+	// ID. If the message is not a reply, then it appears on initID.
+	callbacks map[Tag]map[uint64]ReceptionCallback
+
+	// responseIDs is a list of the newest ID to assign to each callback when
+	// registered. The IDs are used to connect a reply from the worker to the
+	// original message sent by the main thread.
+	responseIDs map[Tag]uint64
+
+	// name describes the worker. It is used for debugging and logging purposes.
+	name string
+
+	// messageLogging determines if debug message logs should be printed every
+	// time a message is sent/received to/from the worker.
+	messageLogging bool
+
+	mux sync.Mutex
+}
+
+// NewManager generates a new Manager. This functions will only return once
+// communication with the worker has been established.
+func NewManager(aURL, name string, messageLogging bool) (*Manager, error) {
+	// Create new worker options with the given name
+	opts := newWorkerOptions("", "", name)
+
+	m := &Manager{
+		worker:         js.Global().Get("Worker").New(aURL, opts),
+		callbacks:      make(map[Tag]map[uint64]ReceptionCallback),
+		responseIDs:    make(map[Tag]uint64),
+		name:           name,
+		messageLogging: messageLogging,
+	}
+
+	// Register listeners on the Javascript worker object that receive messages
+	// and errors from the worker
+	m.addEventListeners()
+
+	// Register a callback that will receive initial message from worker
+	// indicating that it is ready
+	ready := make(chan struct{})
+	m.RegisterCallback(readyTag, func([]byte) { ready <- struct{}{} })
+
+	// Wait for the ready signal from the worker
+	select {
+	case <-ready:
+	case <-time.After(workerInitialConnectionTimeout):
+		return nil, errors.Errorf("[WW] [%s] timed out after %s waiting for "+
+			"initial message from worker",
+			m.name, workerInitialConnectionTimeout)
+	}
+
+	return m, nil
+}
+
+// SendMessage sends a message to the worker with the given tag. If a reception
+// callback is specified, then the message is given a unique ID to handle the
+// reply. Set receptionCB to nil if no reply is expected.
+func (m *Manager) SendMessage(
+	tag Tag, data []byte, receptionCB ReceptionCallback) {
+	var id uint64
+	if receptionCB != nil {
+		id = m.registerReplyCallback(tag, receptionCB)
+	}
+
+	if m.messageLogging {
+		jww.DEBUG.Printf("[WW] [%s] Main sending message for %q and ID %d "+
+			"with data: %s", m.name, tag, id, data)
+	}
+
+	msg := Message{
+		Tag:  tag,
+		ID:   id,
+		Data: data,
+	}
+	payload, err := json.Marshal(msg)
+	if err != nil {
+		jww.FATAL.Panicf("[WW] [%s] Main failed to marshal %T for %q and "+
+			"ID %d going to worker: %+v", m.name, msg, tag, id, err)
+	}
+
+	go m.postMessage(string(payload))
+}
+
+// receiveMessage is registered with the Javascript event listener and is called
+// every time a new message from the worker is received.
+func (m *Manager) receiveMessage(data []byte) error {
+	var msg Message
+	err := json.Unmarshal(data, &msg)
+	if err != nil {
+		return err
+	}
+
+	if m.messageLogging {
+		jww.DEBUG.Printf("[WW] [%s] Main received message for %q and ID %d "+
+			"with data: %s", m.name, msg.Tag, msg.ID, msg.Data)
+	}
+
+	callback, err := m.getCallback(msg.Tag, msg.ID, msg.DeleteCB)
+	if err != nil {
+		return err
+	}
+
+	go callback(msg.Data)
+
+	return nil
+}
+
+// getCallback returns the callback for the given ID or returns an error if no
+// callback is found. The callback is deleted from the map if specified in the
+// message. This function is thread safe.
+func (m *Manager) getCallback(
+	tag Tag, id uint64, deleteCB bool) (ReceptionCallback, error) {
+	m.mux.Lock()
+	defer m.mux.Unlock()
+	callbacks, exists := m.callbacks[tag]
+	if !exists {
+		return nil, errors.Errorf("no callbacks found for tag %q", tag)
+	}
+
+	callback, exists := callbacks[id]
+	if !exists {
+		return nil, errors.Errorf("no %q callback found for ID %d", tag, id)
+	}
+
+	if deleteCB {
+		delete(m.callbacks[tag], id)
+		if len(m.callbacks[tag]) == 0 {
+			delete(m.callbacks, tag)
+		}
+	}
+
+	return callback, nil
+}
+
+// RegisterCallback registers the reception callback for the given tag. If a
+// previous callback was registered, it is overwritten. This function is thread
+// safe.
+func (m *Manager) RegisterCallback(tag Tag, receptionCB ReceptionCallback) {
+	m.mux.Lock()
+	defer m.mux.Unlock()
+
+	id := initID
+
+	jww.DEBUG.Printf("[WW] [%s] Main registering callback for tag %q and ID %d",
+		m.name, tag, id)
+
+	m.callbacks[tag] = map[uint64]ReceptionCallback{id: receptionCB}
+}
+
+// RegisterCallback registers the reception callback for the given tag and a new
+// unique ID used to associate the reply to the callback. Returns the ID that
+// was registered. If a previous callback was registered, it is overwritten.
+// This function is thread safe.
+func (m *Manager) registerReplyCallback(
+	tag Tag, receptionCB ReceptionCallback) uint64 {
+	m.mux.Lock()
+	defer m.mux.Unlock()
+	id := m.getNextID(tag)
+
+	jww.DEBUG.Printf("[WW] [%s] Main registering callback for tag %q and ID %d",
+		m.name, tag, id)
+
+	if _, exists := m.callbacks[tag]; !exists {
+		m.callbacks[tag] = make(map[uint64]ReceptionCallback)
+	}
+	m.callbacks[tag][id] = receptionCB
+
+	return id
+}
+
+// getNextID returns the next unique ID for the given tag. This function is not
+// thread-safe.
+func (m *Manager) getNextID(tag Tag) uint64 {
+	if _, exists := m.responseIDs[tag]; !exists {
+		m.responseIDs[tag] = initID
+	}
+
+	id := m.responseIDs[tag]
+	m.responseIDs[tag]++
+	return id
+}
+
+// GetWorker returns the web worker object. This returned so the worker object
+// can be returned to the Javascript layer for it to communicate with the worker
+// thread.
+func (m *Manager) GetWorker() js.Value { return m.worker }
+
+// Name returns the name of the web worker object.
+func (m *Manager) Name() string { return m.name }
+
+////////////////////////////////////////////////////////////////////////////////
+// Javascript Call Wrappers                                                   //
+////////////////////////////////////////////////////////////////////////////////
+
+// addEventListeners adds the event listeners needed to receive a message from
+// the worker. Two listeners were added; one to receive messages from the worker
+// and the other to receive errors.
+func (m *Manager) addEventListeners() {
+	// Create a listener for when the message event is fired on the worker. This
+	// occurs when a message is received from the worker.
+	// Doc: https://developer.mozilla.org/en-US/docs/Web/API/Worker/message_event
+	messageEvent := js.FuncOf(func(_ js.Value, args []js.Value) any {
+		err := m.receiveMessage([]byte(args[0].Get("data").String()))
+		if err != nil {
+			jww.ERROR.Printf("[WW] [%s] Failed to receive message from "+
+				"worker: %+v", m.name, err)
+		}
+		return nil
+	})
+
+	// Create listener for when an error event is fired on the worker. This
+	// occurs when an error occurs in the worker.
+	// Doc: https://developer.mozilla.org/en-US/docs/Web/API/Worker/error_event
+	errorEvent := js.FuncOf(func(_ js.Value, args []js.Value) any {
+		event := args[0]
+		jww.ERROR.Printf("[WW] [%s] Main received error event: %s",
+			m.name, utils.JsErrorToJson(event))
+		return nil
+	})
+
+	// Create listener for when a messageerror event is fired on the worker.
+	// This occurs when it receives a message that cannot be deserialized.
+	// Doc: https://developer.mozilla.org/en-US/docs/Web/API/Worker/messageerror_event
+	messageerrorEvent := js.FuncOf(func(_ js.Value, args []js.Value) any {
+		event := args[0]
+		jww.ERROR.Printf("[WW] [%s] Main received message error event: %s",
+			m.name, utils.JsErrorToJson(event))
+		return nil
+	})
+
+	// Register each event listener on the worker using addEventListener
+	// Doc: https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener
+	m.worker.Call("addEventListener", "message", messageEvent)
+	m.worker.Call("addEventListener", "error", errorEvent)
+	m.worker.Call("addEventListener", "messageerror", messageerrorEvent)
+}
+
+// postMessage sends a message to the worker.
+//
+// message is the object to deliver to the worker; this will be in the data
+// field in the event delivered to the worker. It must be a js.Value or a
+// primitive type that can be converted via js.ValueOf. The Javascript object
+// must be "any value or JavaScript object handled by the structured clone
+// algorithm, which includes cyclical references.". See the doc for more
+// information.
+//
+// If the message parameter is not provided, a SyntaxError will be thrown by the
+// parser. If the data to be passed to the worker is unimportant, js.Null or
+// js.Undefined can be passed explicitly.
+//
+// Doc: https://developer.mozilla.org/en-US/docs/Web/API/Worker/postMessage
+func (m *Manager) postMessage(msg any) {
+	m.worker.Call("postMessage", msg)
+}
+
+// Terminate immediately terminates the Worker. This does not offer the worker
+// an opportunity to finish its operations; it is stopped at once.
+//
+// Doc: https://developer.mozilla.org/en-US/docs/Web/API/Worker/terminate
+func (m *Manager) Terminate() {
+	m.worker.Call("terminate")
+}
+
+// newWorkerOptions creates a new Javascript object containing optional
+// properties that can be set when creating a new worker.
+//
+// Each property is optional; leave a property empty to use the defaults (as
+// documented). The available properties are:
+//   - type - The type of worker to create. The value can be either "classic" or
+//     "module". If not specified, the default used is "classic".
+//   - credentials - The type of credentials to use for the worker. The value
+//     can be "omit", "same-origin", or "include". If it is not specified, or if
+//     the type is "classic", then the default used is "omit" (no credentials
+//     are required).
+//   - name - An identifying name for the worker, used mainly for debugging
+//     purposes.
+//
+// Doc: https://developer.mozilla.org/en-US/docs/Web/API/Worker/Worker#options
+func newWorkerOptions(workerType, credentials, name string) js.Value {
+	options := make(map[string]any, 3)
+	if workerType != "" {
+		options["type"] = workerType
+	}
+	if credentials != "" {
+		options["credentials"] = credentials
+	}
+	if name != "" {
+		options["name"] = name
+	}
+	return js.ValueOf(options)
+}
diff --git a/worker/manager_test.go b/worker/manager_test.go
new file mode 100644
index 0000000000000000000000000000000000000000..2eadaff2edeaf66a4071f387f608a602c6967b8d
--- /dev/null
+++ b/worker/manager_test.go
@@ -0,0 +1,212 @@
+////////////////////////////////////////////////////////////////////////////////
+// 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 worker
+
+import (
+	"encoding/json"
+	"reflect"
+	"testing"
+	"time"
+)
+
+// Tests Manager.receiveMessage calls the expected callback.
+func TestManager_receiveMessage(t *testing.T) {
+	m := &Manager{callbacks: make(map[Tag]map[uint64]ReceptionCallback)}
+
+	msg := Message{Tag: readyTag, ID: 5}
+	cbChan := make(chan struct{})
+	cb := func([]byte) { cbChan <- struct{}{} }
+	m.callbacks[msg.Tag] = map[uint64]ReceptionCallback{msg.ID: cb}
+
+	data, err := json.Marshal(msg)
+	if err != nil {
+		t.Fatalf("Failed to JSON marshal Message: %+v", err)
+	}
+
+	go func() {
+		select {
+		case <-cbChan:
+		case <-time.After(10 * time.Millisecond):
+			t.Error("Timed out waiting for callback to be called.")
+		}
+	}()
+
+	err = m.receiveMessage(data)
+	if err != nil {
+		t.Errorf("Failed to receive message: %+v", err)
+	}
+}
+
+// Tests Manager.getCallback returns the expected callback and deletes only the
+// given callback when deleteCB is true.
+func TestManager_getCallback(t *testing.T) {
+	m := &Manager{callbacks: make(map[Tag]map[uint64]ReceptionCallback)}
+
+	// Add new callback and check that it is returned by getCallback
+	tag, id1 := readyTag, uint64(5)
+	cb := func([]byte) {}
+	m.callbacks[tag] = map[uint64]ReceptionCallback{id1: cb}
+
+	received, err := m.getCallback(tag, id1, false)
+	if err != nil {
+		t.Errorf("getCallback error for tag %q and ID %d: %+v", tag, id1, err)
+	}
+
+	if reflect.ValueOf(cb).Pointer() != reflect.ValueOf(received).Pointer() {
+		t.Errorf("Wrong callback.\nexpected: %p\nreceived: %p", cb, received)
+	}
+
+	// Add new callback under the same tag but with deleteCB set to true and
+	// check that it is returned by getCallback and that it was deleted from the
+	// map while id1 was not
+	id2 := uint64(56)
+	cb = func([]byte) {}
+	m.callbacks[tag][id2] = cb
+
+	received, err = m.getCallback(tag, id2, true)
+	if err != nil {
+		t.Errorf("getCallback error for tag %q and ID %d: %+v", tag, id2, err)
+	}
+
+	if reflect.ValueOf(cb).Pointer() != reflect.ValueOf(received).Pointer() {
+		t.Errorf("Wrong callback.\nexpected: %p\nreceived: %p", cb, received)
+	}
+
+	received, err = m.getCallback(tag, id1, false)
+	if err != nil {
+		t.Errorf("getCallback error for tag %q and ID %d: %+v", tag, id1, err)
+	}
+
+	received, err = m.getCallback(tag, id2, true)
+	if err == nil {
+		t.Errorf("getCallback did not get error when trying to get deleted "+
+			"callback for tag %q and ID %d", tag, id2)
+	}
+}
+
+// Tests that Manager.RegisterCallback registers a callback that is then called
+// by Manager.receiveMessage.
+func TestManager_RegisterCallback(t *testing.T) {
+	m := &Manager{callbacks: make(map[Tag]map[uint64]ReceptionCallback)}
+
+	msg := Message{Tag: readyTag, ID: initID}
+	cbChan := make(chan struct{})
+	cb := func([]byte) { cbChan <- struct{}{} }
+	m.RegisterCallback(msg.Tag, cb)
+
+	data, err := json.Marshal(msg)
+	if err != nil {
+		t.Fatalf("Failed to JSON marshal Message: %+v", err)
+	}
+
+	go func() {
+		select {
+		case <-cbChan:
+		case <-time.After(10 * time.Millisecond):
+			t.Error("Timed out waiting for callback to be called.")
+		}
+	}()
+
+	err = m.receiveMessage(data)
+	if err != nil {
+		t.Errorf("Failed to receive message: %+v", err)
+	}
+}
+
+// Tests that Manager.registerReplyCallback registers a callback that is then
+// called by Manager.receiveMessage.
+func TestManager_registerReplyCallback(t *testing.T) {
+	m := &Manager{
+		callbacks:   make(map[Tag]map[uint64]ReceptionCallback),
+		responseIDs: make(map[Tag]uint64),
+	}
+
+	msg := Message{Tag: readyTag, ID: 5}
+	cbChan := make(chan struct{})
+	cb := func([]byte) { cbChan <- struct{}{} }
+	m.registerReplyCallback(msg.Tag, cb)
+	m.callbacks[msg.Tag] = map[uint64]ReceptionCallback{msg.ID: cb}
+
+	data, err := json.Marshal(msg)
+	if err != nil {
+		t.Fatalf("Failed to JSON marshal Message: %+v", err)
+	}
+
+	go func() {
+		select {
+		case <-cbChan:
+		case <-time.After(10 * time.Millisecond):
+			t.Error("Timed out waiting for callback to be called.")
+		}
+	}()
+
+	err = m.receiveMessage(data)
+	if err != nil {
+		t.Errorf("Failed to receive message: %+v", err)
+	}
+}
+
+// Tests that Manager.getNextID returns the expected ID for various Tags.
+func TestManager_getNextID(t *testing.T) {
+	m := &Manager{
+		callbacks:   make(map[Tag]map[uint64]ReceptionCallback),
+		responseIDs: make(map[Tag]uint64),
+	}
+
+	for _, tag := range []Tag{readyTag, "test", "A", "B", "C"} {
+		id := m.getNextID(tag)
+		if id != initID {
+			t.Errorf("ID for new tag %q is not initID."+
+				"\nexpected: %d\nreceived: %d", tag, initID, id)
+		}
+
+		for j := uint64(1); j < 100; j++ {
+			id = m.getNextID(tag)
+			if id != j {
+				t.Errorf("Unexpected ID for tag %q."+
+					"\nexpected: %d\nreceived: %d", tag, j, id)
+			}
+		}
+	}
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// Javascript Call Wrappers                                                   //
+////////////////////////////////////////////////////////////////////////////////
+
+// Tests that newWorkerOptions returns a Javascript object with the expected
+// type, credentials, and name fields.
+func Test_newWorkerOptions(t *testing.T) {
+	for i, workerType := range []string{"classic", "module"} {
+		for j, credentials := range []string{"omit", "same-origin", "include"} {
+			for k, name := range []string{"name1", "name2", "name3"} {
+				opts := newWorkerOptions(workerType, credentials, name)
+
+				v := opts.Get("type").String()
+				if v != workerType {
+					t.Errorf("Unexpected type (%d, %d, %d)."+
+						"\nexpected: %s\nreceived: %s", i, j, k, workerType, v)
+				}
+
+				v = opts.Get("credentials").String()
+				if v != credentials {
+					t.Errorf("Unexpected credentials (%d, %d, %d)."+
+						"\nexpected: %s\nreceived: %s", i, j, k, credentials, v)
+				}
+
+				v = opts.Get("name").String()
+				if v != name {
+					t.Errorf("Unexpected name (%d, %d, %d)."+
+						"\nexpected: %s\nreceived: %s", i, j, k, name, v)
+				}
+			}
+		}
+	}
+}
diff --git a/worker/message.go b/worker/message.go
new file mode 100644
index 0000000000000000000000000000000000000000..3d3bc23f131ef96cdc97a3e8684d1386b07cbeb2
--- /dev/null
+++ b/worker/message.go
@@ -0,0 +1,19 @@
+////////////////////////////////////////////////////////////////////////////////
+// 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 worker
+
+// Message is the outer message that contains the contents of each message sent
+// to the worker. It is transmitted as JSON.
+type Message struct {
+	Tag      Tag    `json:"tag"`
+	ID       uint64 `json:"id"`
+	DeleteCB bool   `json:"deleteCB"`
+	Data     []byte `json:"data"`
+}
diff --git a/worker/tag.go b/worker/tag.go
new file mode 100644
index 0000000000000000000000000000000000000000..fa458c291802c47d0ffb1970ef75202b936561a4
--- /dev/null
+++ b/worker/tag.go
@@ -0,0 +1,18 @@
+////////////////////////////////////////////////////////////////////////////////
+// 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 worker
+
+// Tag describes how a message sent to or from the worker should be handled.
+type Tag string
+
+// Generic tags used by all workers.
+const (
+	readyTag Tag = "Ready"
+)
diff --git a/worker/thread.go b/worker/thread.go
new file mode 100644
index 0000000000000000000000000000000000000000..df824d045f4337c6384eac389f692b15bec200a8
--- /dev/null
+++ b/worker/thread.go
@@ -0,0 +1,220 @@
+////////////////////////////////////////////////////////////////////////////////
+// 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 worker
+
+import (
+	"encoding/json"
+	"github.com/pkg/errors"
+	jww "github.com/spf13/jwalterweatherman"
+	"gitlab.com/elixxir/xxdk-wasm/utils"
+	"sync"
+	"syscall/js"
+)
+
+// ThreadReceptionCallback is the function that handles incoming data from the
+// main thread.
+type ThreadReceptionCallback func(data []byte) ([]byte, error)
+
+// ThreadManager queues incoming messages from the main thread and handles them
+// based on their tag.
+type ThreadManager struct {
+	// messages is a list of queued messages sent from the main thread.
+	messages chan js.Value
+
+	// callbacks is a list of callbacks to handle messages that come from the
+	// main thread keyed on the callback tag.
+	callbacks map[Tag]ThreadReceptionCallback
+
+	// name describes the worker. It is used for debugging and logging purposes.
+	name string
+
+	// messageLogging determines if debug message logs should be printed every
+	// time a message is sent/received to/from the worker.
+	messageLogging bool
+
+	mux sync.Mutex
+}
+
+// NewThreadManager initialises a new ThreadManager.
+func NewThreadManager(name string, messageLogging bool) *ThreadManager {
+	mh := &ThreadManager{
+		messages:       make(chan js.Value, 100),
+		callbacks:      make(map[Tag]ThreadReceptionCallback),
+		name:           name,
+		messageLogging: messageLogging,
+	}
+
+	mh.addEventListeners()
+
+	return mh
+}
+
+// SignalReady sends a signal to the main thread indicating that the worker is
+// ready. Once the main thread receives this, it will initiate communication.
+// Therefore, this should only be run once all listeners are ready.
+func (tm *ThreadManager) SignalReady() {
+	tm.SendMessage(readyTag, nil)
+}
+
+// SendMessage sends a message to the main thread for the given tag.
+func (tm *ThreadManager) SendMessage(tag Tag, data []byte) {
+	msg := Message{
+		Tag:      tag,
+		ID:       initID,
+		DeleteCB: false,
+		Data:     data,
+	}
+
+	if tm.messageLogging {
+		jww.DEBUG.Printf("[WW] [%s] Worker sending message for %q with data: %s",
+			tm.name, tag, data)
+	}
+
+	payload, err := json.Marshal(msg)
+	if err != nil {
+		jww.FATAL.Panicf("[WW] [%s] Worker failed to marshal %T for %q going "+
+			"to main: %+v", tm.name, msg, tag, err)
+	}
+
+	go tm.postMessage(string(payload))
+}
+
+// sendResponse sends a reply to the main thread with the given tag and ID.
+func (tm *ThreadManager) sendResponse(
+	tag Tag, id uint64, data []byte) {
+	msg := Message{
+		Tag:      tag,
+		ID:       id,
+		DeleteCB: true,
+		Data:     data,
+	}
+
+	if tm.messageLogging {
+		jww.DEBUG.Printf("[WW] [%s] Worker sending reply for %q and ID %d "+
+			"with data: %s", tm.name, tag, id, data)
+	}
+
+	payload, err := json.Marshal(msg)
+	if err != nil {
+		jww.FATAL.Panicf("[WW] [%s] Worker failed to marshal %T for %q and ID "+
+			"%d going to main: %+v", tm.name, msg, tag, id, err)
+	}
+
+	go tm.postMessage(string(payload))
+}
+
+// receiveMessage is registered with the Javascript event listener and is called
+// everytime a message from the main thread is received. If the registered
+// callback returns a response, it is sent to the main thread.
+func (tm *ThreadManager) receiveMessage(data []byte) error {
+	var msg Message
+	err := json.Unmarshal(data, &msg)
+	if err != nil {
+		return err
+	}
+
+	if tm.messageLogging {
+		jww.DEBUG.Printf("[WW] [%s] Worker received message for %q and ID %d "+
+			"with data: %s", tm.name, msg.Tag, msg.ID, msg.Data)
+	}
+
+	tm.mux.Lock()
+	callback, exists := tm.callbacks[msg.Tag]
+	tm.mux.Unlock()
+	if !exists {
+		return errors.Errorf("no callback found for tag %q", msg.Tag)
+	}
+
+	// Call callback and register response with its return
+	go func() {
+		response, err2 := callback(msg.Data)
+		if err2 != nil {
+			jww.ERROR.Printf("[WW] [%s] Callback for for %q and ID %d "+
+				"returned an error: %+v", tm.name, msg.Tag, msg.ID, err)
+		}
+		if response != nil {
+			tm.sendResponse(msg.Tag, msg.ID, response)
+		}
+	}()
+
+	return nil
+}
+
+// RegisterCallback registers the callback with the given tag overwriting any
+// previous registered callbacks with the same tag. This function is thread
+// safe.
+//
+// If the callback returns anything but nil, it will be returned as a response.
+func (tm *ThreadManager) RegisterCallback(
+	tag Tag, receptionCallback ThreadReceptionCallback) {
+	jww.DEBUG.Printf(
+		"[WW] [%s] Worker registering callback for tag %q", tm.name, tag)
+	tm.mux.Lock()
+	tm.callbacks[tag] = receptionCallback
+	tm.mux.Unlock()
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// Javascript Call Wrappers                                                   //
+////////////////////////////////////////////////////////////////////////////////
+
+// addEventListeners adds the event listeners needed to receive a message from
+// the worker. Two listeners were added; one to receive messages from the worker
+// and the other to receive errors.
+func (tm *ThreadManager) addEventListeners() {
+	// Create a listener for when the message event is fire on the worker. This
+	// occurs when a message is received from the main thread.
+	// Doc: https://developer.mozilla.org/en-US/docs/Web/API/Worker/message_event
+	messageEvent := js.FuncOf(func(_ js.Value, args []js.Value) any {
+		err := tm.receiveMessage([]byte(args[0].Get("data").String()))
+		if err != nil {
+			jww.ERROR.Printf("[WW] [%s] Failed to receive message from "+
+				"main thread: %+v", tm.name, err)
+		}
+		return nil
+	})
+
+	// Create listener for when an error event is fired on the worker. This
+	// occurs when an error occurs in the worker.
+	// Doc: https://developer.mozilla.org/en-US/docs/Web/API/Worker/error_event
+	errorEvent := js.FuncOf(func(_ js.Value, args []js.Value) any {
+		event := args[0]
+		jww.ERROR.Printf("[WW] [%s] Worker received error event: %s",
+			tm.name, utils.JsErrorToJson(event))
+		return nil
+	})
+
+	// Create listener for when a messageerror event is fired on the worker.
+	// This occurs when it receives a message that cannot be deserialized.
+	// Doc: https://developer.mozilla.org/en-US/docs/Web/API/Worker/messageerror_event
+	messageerrorEvent := js.FuncOf(func(_ js.Value, args []js.Value) any {
+		event := args[0]
+		jww.ERROR.Printf("[WW] [%s] Worker received message error event: %s",
+			tm.name, utils.JsErrorToJson(event))
+		return nil
+	})
+
+	// Register each event listener on the worker using addEventListener
+	// Doc: https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener
+	js.Global().Call("addEventListener", "message", messageEvent)
+	js.Global().Call("addEventListener", "error", errorEvent)
+	js.Global().Call("addEventListener", "messageerror", messageerrorEvent)
+}
+
+// postMessage sends a message from this worker to the main WASM thread.
+//
+// aMessage must be a js.Value or a primitive type that can be converted via
+// js.ValueOf. The Javascript object must be "any value or JavaScript object
+// handled by the structured clone algorithm". See the doc for more information.
+//
+// Doc: https://developer.mozilla.org/docs/Web/API/DedicatedWorkerGlobalScope/postMessage
+func (tm *ThreadManager) postMessage(aMessage any) {
+	js.Global().Call("postMessage", aMessage)
+}
diff --git a/worker/thread_test.go b/worker/thread_test.go
new file mode 100644
index 0000000000000000000000000000000000000000..8f7d09cd931f5e28002ea9cb22b764df834c8062
--- /dev/null
+++ b/worker/thread_test.go
@@ -0,0 +1,73 @@
+////////////////////////////////////////////////////////////////////////////////
+// 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 worker
+
+import (
+	"encoding/json"
+	"testing"
+	"time"
+)
+
+// Tests that ThreadManager.receiveMessage calls the expected callback.
+func TestThreadManager_receiveMessage(t *testing.T) {
+	tm := &ThreadManager{callbacks: make(map[Tag]ThreadReceptionCallback)}
+
+	msg := Message{Tag: readyTag, ID: 5}
+	cbChan := make(chan struct{})
+	cb := func([]byte) ([]byte, error) { cbChan <- struct{}{}; return nil, nil }
+	tm.callbacks[msg.Tag] = cb
+
+	data, err := json.Marshal(msg)
+	if err != nil {
+		t.Fatalf("Failed to JSON marshal Message: %+v", err)
+	}
+
+	go func() {
+		select {
+		case <-cbChan:
+		case <-time.After(10 * time.Millisecond):
+			t.Error("Timed out waiting for callback to be called.")
+		}
+	}()
+
+	err = tm.receiveMessage(data)
+	if err != nil {
+		t.Errorf("Failed to receive message: %+v", err)
+	}
+}
+
+// Tests that ThreadManager.RegisterCallback registers a callback that is then
+// called by ThreadManager.receiveMessage.
+func TestThreadManager_RegisterCallback(t *testing.T) {
+	tm := &ThreadManager{callbacks: make(map[Tag]ThreadReceptionCallback)}
+
+	msg := Message{Tag: readyTag, ID: 5}
+	cbChan := make(chan struct{})
+	cb := func([]byte) ([]byte, error) { cbChan <- struct{}{}; return nil, nil }
+	tm.RegisterCallback(msg.Tag, cb)
+
+	data, err := json.Marshal(msg)
+	if err != nil {
+		t.Fatalf("Failed to JSON marshal Message: %+v", err)
+	}
+
+	go func() {
+		select {
+		case <-cbChan:
+		case <-time.After(10 * time.Millisecond):
+			t.Error("Timed out waiting for callback to be called.")
+		}
+	}()
+
+	err = tm.receiveMessage(data)
+	if err != nil {
+		t.Errorf("Failed to receive message: %+v", err)
+	}
+}