diff --git a/indexedDb/impl/channels/implementation.go b/indexedDb/impl/channels/implementation.go
index c858614524f989e5851d9add3f0bb4f304587f3c..54ec455870bb77645ec76f4e2cea7043bd5997e1 100644
--- a/indexedDb/impl/channels/implementation.go
+++ b/indexedDb/impl/channels/implementation.go
@@ -22,9 +22,11 @@ import (
 	jww "github.com/spf13/jwalterweatherman"
 
 	"gitlab.com/elixxir/client/v4/channels"
+	cft "gitlab.com/elixxir/client/v4/channelsFileTransfer"
 	"gitlab.com/elixxir/client/v4/cmix/rounds"
 	cryptoBroadcast "gitlab.com/elixxir/crypto/broadcast"
 	cryptoChannel "gitlab.com/elixxir/crypto/channel"
+	"gitlab.com/elixxir/crypto/fileTransfer"
 	"gitlab.com/elixxir/crypto/message"
 	"gitlab.com/elixxir/xxdk-wasm/indexedDb/impl"
 	wChannels "gitlab.com/elixxir/xxdk-wasm/indexedDb/worker/channels"
@@ -45,6 +47,131 @@ type wasmModel struct {
 	mutedUserCB       wChannels.MutedUserCallback
 }
 
+// ReceiveFile is called when a file upload or download beings.
+//
+// fileLink and fileData are nillable and may be updated based
+// upon the UUID or file ID later.
+//
+// fileID is always unique to the fileData. fileLink is the JSON of
+// channelsFileTransfer.FileLink.
+//
+// Returns any fatal errors.
+func (w *wasmModel) ReceiveFile(fileID fileTransfer.ID, fileLink,
+	fileData []byte, timestamp time.Time, status cft.Status) error {
+
+	newFile := &File{
+		Id:        fileID.Marshal(),
+		Data:      fileData,
+		Link:      fileLink,
+		Timestamp: timestamp,
+		Status:    uint8(status),
+	}
+	return w.upsertFile(newFile)
+}
+
+// UpdateFile is called when a file upload or download completes or changes.
+//
+// fileLink, fileData, timestamp, and status are all nillable and may be
+// updated based upon the file ID at a later date. If a nil value is passed,
+// then make no update.
+//
+// Returns an error if the file cannot be updated. It must return
+// channels.NoMessageErr if the file does not exist.
+func (w *wasmModel) UpdateFile(fileID fileTransfer.ID, fileLink,
+	fileData []byte, timestamp *time.Time, status *cft.Status) error {
+	parentErr := "[Channels indexedDB] failed to UpdateFile"
+
+	// Get the File as it currently exists in storage
+	fileObj, err := impl.Get(w.db, fileStoreName, impl.EncodeBytes(fileID.Marshal()))
+	if err != nil {
+		if strings.Contains(err.Error(), impl.ErrDoesNotExist) {
+			return errors.WithMessage(channels.NoMessageErr, parentErr)
+		}
+		return errors.WithMessage(err, parentErr)
+	}
+	currentFile, err := valueToFile(fileObj)
+	if err != nil {
+		return errors.WithMessage(err, parentErr)
+	}
+
+	// Update the fields if specified
+	if status != nil {
+		currentFile.Status = uint8(*status)
+	}
+	if timestamp != nil {
+		currentFile.Timestamp = *timestamp
+	}
+	if fileData != nil {
+		currentFile.Data = fileData
+	}
+	if fileLink != nil {
+		currentFile.Link = fileLink
+	}
+
+	return w.upsertFile(currentFile)
+}
+
+// upsertFile is a helper function that will update an existing File
+// if File.Id is specified. Otherwise, it will perform an insert.
+func (w *wasmModel) upsertFile(newFile *File) error {
+	newFileJson, err := json.Marshal(&newFile)
+	if err != nil {
+		return err
+	}
+	fileObj, err := utils.JsonToJS(newFileJson)
+	if err != nil {
+		return err
+	}
+
+	_, err = impl.Put(w.db, fileStoreName, fileObj)
+	return err
+}
+
+// GetFile returns the ModelFile containing the file data and download link
+// for the given file ID.
+//
+// Returns an error if the file cannot be retrieved. It must return
+// channels.NoMessageErr if the file does not exist.
+func (w *wasmModel) GetFile(fileID fileTransfer.ID) (
+	cft.ModelFile, error) {
+	fileObj, err := impl.Get(w.db, fileStoreName,
+		impl.EncodeBytes(fileID.Marshal()))
+	if err != nil {
+		if strings.Contains(err.Error(), impl.ErrDoesNotExist) {
+			return cft.ModelFile{}, channels.NoMessageErr
+		}
+		return cft.ModelFile{}, err
+	}
+
+	resultFile, err := valueToFile(fileObj)
+	if err != nil {
+		return cft.ModelFile{}, err
+	}
+
+	result := cft.ModelFile{
+		ID:        fileTransfer.NewID(resultFile.Data),
+		Link:      resultFile.Link,
+		Data:      resultFile.Data,
+		Timestamp: resultFile.Timestamp,
+		Status:    cft.Status(resultFile.Status),
+	}
+	return result, nil
+}
+
+// DeleteFile deletes the file with the given file ID.
+//
+// Returns fatal errors. It must return channels.NoMessageErr if the file
+// does not exist.
+func (w *wasmModel) DeleteFile(fileID fileTransfer.ID) error {
+	err := impl.Delete(w.db, fileStoreName, impl.EncodeBytes(fileID.Marshal()))
+	if err != nil {
+		if strings.Contains(err.Error(), impl.ErrDoesNotExist) {
+			return channels.NoMessageErr
+		}
+	}
+	return err
+}
+
 // JoinChannel is called whenever a channel is joined locally.
 func (w *wasmModel) JoinChannel(channel *cryptoBroadcast.Channel) {
 	parentErr := errors.New("failed to JoinChannel")
@@ -70,7 +197,7 @@ func (w *wasmModel) JoinChannel(channel *cryptoBroadcast.Channel) {
 		return
 	}
 
-	_, err = impl.Put(w.db, channelsStoreName, channelObj)
+	_, err = impl.Put(w.db, channelStoreName, channelObj)
 	if err != nil {
 		jww.ERROR.Printf("%+v", errors.WithMessagef(parentErr,
 			"Unable to put Channel: %+v", err))
@@ -82,7 +209,7 @@ func (w *wasmModel) LeaveChannel(channelID *id.ID) {
 	parentErr := errors.New("failed to LeaveChannel")
 
 	// Delete the channel from storage
-	err := impl.Delete(w.db, channelsStoreName, js.ValueOf(channelID.String()))
+	err := impl.Delete(w.db, channelStoreName, js.ValueOf(channelID.String()))
 	if err != nil {
 		jww.ERROR.Printf("%+v", errors.WithMessagef(parentErr,
 			"Unable to delete Channel: %+v", err))
@@ -505,9 +632,11 @@ func (w *wasmModel) MuteUser(
 // valueToMessage is a helper for converting js.Value to Message.
 func valueToMessage(msgObj js.Value) (*Message, error) {
 	resultMsg := &Message{}
-	err := json.Unmarshal([]byte(utils.JsToJson(msgObj)), resultMsg)
-	if err != nil {
-		return nil, err
-	}
-	return resultMsg, nil
+	return resultMsg, json.Unmarshal([]byte(utils.JsToJson(msgObj)), resultMsg)
+}
+
+// valueToFile is a helper for converting js.Value to File.
+func valueToFile(fileObj js.Value) (*File, error) {
+	resultFile := &File{}
+	return resultFile, json.Unmarshal([]byte(utils.JsToJson(fileObj)), resultFile)
 }
diff --git a/indexedDb/impl/channels/implementation_test.go b/indexedDb/impl/channels/implementation_test.go
index 9577217901a13075bcd6b5bb3848d69d8577e81c..4b22b8300f5549a5dc2f3902f7ed95277777555f 100644
--- a/indexedDb/impl/channels/implementation_test.go
+++ b/indexedDb/impl/channels/implementation_test.go
@@ -10,9 +10,13 @@
 package main
 
 import (
+	"bytes"
 	"crypto/ed25519"
 	"encoding/json"
+	"errors"
 	"fmt"
+	cft "gitlab.com/elixxir/client/v4/channelsFileTransfer"
+	"gitlab.com/elixxir/crypto/fileTransfer"
 	"os"
 	"strconv"
 	"testing"
@@ -42,6 +46,79 @@ func dummyReceivedMessageCB(uint64, *id.ID, bool)      {}
 func dummyDeletedMessageCB(message.ID)                 {}
 func dummyMutedUserCB(*id.ID, ed25519.PublicKey, bool) {}
 
+// Happy path test for receiving, updating, getting, and deleting a File.
+func TestWasmModel_ReceiveFile(t *testing.T) {
+	testString := "TestWasmModel_ReceiveFile"
+	m, err := newWASMModel(testString, nil,
+		dummyReceivedMessageCB, dummyDeletedMessageCB, dummyMutedUserCB)
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	testTs := time.Now()
+	testBytes := []byte(testString)
+	testStatus := cft.Downloading
+
+	// Insert a test row
+	fId := fileTransfer.NewID(testBytes)
+	err = m.ReceiveFile(fId, testBytes, testBytes, testTs, testStatus)
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	// Attempt to get stored row
+	storedFile, err := m.GetFile(fId)
+	if err != nil {
+		t.Fatal(err)
+	}
+	// Spot check stored attribute
+	if !bytes.Equal(storedFile.Link, testBytes) {
+		t.Fatalf("Got unequal FileLink values")
+	}
+
+	// Attempt to updated stored row
+	newTs := time.Now()
+	newBytes := []byte("test")
+	newStatus := cft.Complete
+	err = m.UpdateFile(fId, nil, newBytes, &newTs, &newStatus)
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	// Check that the update took
+	updatedFile, err := m.GetFile(fId)
+	if err != nil {
+		t.Fatal(err)
+	}
+	// Link should not have changed
+	if !bytes.Equal(updatedFile.Link, testBytes) {
+		t.Fatalf("Link should not have changed")
+	}
+	// Other attributes should have changed
+	if !bytes.Equal(updatedFile.Data, newBytes) {
+		t.Fatalf("Data should have updated")
+	}
+	if !updatedFile.Timestamp.Equal(newTs) {
+		t.Fatalf("TS should have updated, expected %s got %s",
+			newTs, updatedFile.Timestamp)
+	}
+	if updatedFile.Status != newStatus {
+		t.Fatalf("Status should have updated")
+	}
+
+	// Delete the row
+	err = m.DeleteFile(fId)
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	// Check that the delete operation took and get provides the expected error
+	_, err = m.GetFile(fId)
+	if err == nil || !errors.Is(channels.NoMessageErr, err) {
+		t.Fatal(err)
+	}
+}
+
 // Happy path, insert message and look it up
 func TestWasmModel_GetMessage(t *testing.T) {
 	cipher, err := cryptoChannel.NewCipher(
@@ -54,7 +131,7 @@ func TestWasmModel_GetMessage(t *testing.T) {
 		if c != nil {
 			cs = "_withCipher"
 		}
-		testString := "TestWasmModel_msgIDLookup" + cs
+		testString := "TestWasmModel_GetMessage" + cs
 		t.Run(testString, func(t *testing.T) {
 			storage.GetLocalStorage().Clear()
 			testMsgId := message.DeriveChannelMessageID(&id.ID{1}, 0, []byte(testString))
@@ -235,7 +312,7 @@ func Test_wasmModel_JoinChannel_LeaveChannel(t *testing.T) {
 			}
 			eventModel.JoinChannel(testChannel)
 			eventModel.JoinChannel(testChannel2)
-			results, err2 := impl.Dump(eventModel.db, channelsStoreName)
+			results, err2 := impl.Dump(eventModel.db, channelStoreName)
 			if err2 != nil {
 				t.Fatal(err2)
 			}
@@ -243,7 +320,7 @@ func Test_wasmModel_JoinChannel_LeaveChannel(t *testing.T) {
 				t.Fatalf("Expected 2 channels to exist")
 			}
 			eventModel.LeaveChannel(testChannel.ReceptionID)
-			results, err = impl.Dump(eventModel.db, channelsStoreName)
+			results, err = impl.Dump(eventModel.db, channelStoreName)
 			if err != nil {
 				t.Fatal(err)
 			}
diff --git a/indexedDb/impl/channels/init.go b/indexedDb/impl/channels/init.go
index 9a8b940898ac30e81fa755e9d4053a21220ef66b..363440d541029007a6c167d94f8a1fcd66edf4fd 100644
--- a/indexedDb/impl/channels/init.go
+++ b/indexedDb/impl/channels/init.go
@@ -23,7 +23,7 @@ import (
 
 // currentVersion is the current version of the IndexedDb runtime. Used for
 // migration purposes.
-const currentVersion uint = 1
+const currentVersion uint = 2
 
 // NewWASMEventModel returns a [channels.EventModel] backed by a wasmModel.
 // The name should be a base64 encoding of the users public key. Returns the
@@ -62,6 +62,14 @@ func newWASMModel(databaseName string, encryption cryptoChannel.Cipher,
 				oldVersion = 1
 			}
 
+			if oldVersion == 1 && newVersion >= 2 {
+				err := v2Upgrade(db)
+				if err != nil {
+					return err
+				}
+				oldVersion = 2
+			}
+
 			// if oldVersion == 1 && newVersion >= 2 { v2Upgrade(), oldVersion = 2 }
 			return nil
 		})
@@ -136,10 +144,22 @@ func v1Upgrade(db *idb.Database) error {
 	}
 
 	// Build Channel ObjectStore
-	_, err = db.CreateObjectStore(channelsStoreName, storeOpts)
+	_, err = db.CreateObjectStore(channelStoreName, storeOpts)
 	if err != nil {
 		return err
 	}
 
 	return nil
 }
+
+// v1Upgrade performs the v1 -> v2 database upgrade.
+//
+// This can never be changed without permanently breaking backwards
+// compatibility.
+func v2Upgrade(db *idb.Database) error {
+	_, err := db.CreateObjectStore(fileStoreName, idb.ObjectStoreOptions{
+		KeyPath:       js.ValueOf(pkeyName),
+		AutoIncrement: false,
+	})
+	return err
+}
diff --git a/indexedDb/impl/channels/model.go b/indexedDb/impl/channels/model.go
index d1dcee88b3efd5f4a877685366154caefaa46a82..e5d3e00aa5209985de60a77f5330a53208b1af88 100644
--- a/indexedDb/impl/channels/model.go
+++ b/indexedDb/impl/channels/model.go
@@ -18,8 +18,9 @@ const (
 	pkeyName = "id"
 
 	// Text representation of the names of the various [idb.ObjectStore].
-	messageStoreName  = "messages"
-	channelsStoreName = "channels"
+	messageStoreName = "messages"
+	channelStoreName = "channels"
+	fileStoreName    = "files"
 
 	// Message index names.
 	messageStoreMessageIndex   = "message_id_index"
@@ -73,3 +74,21 @@ type Channel struct {
 	Name        string `json:"name"`
 	Description string `json:"description"`
 }
+
+// File defines the IndexedDb representation of a single File.
+type File struct {
+	// Id is a unique identifier for a given File.
+	Id []byte `json:"id"` // Matches pkeyName
+
+	// Data stores the actual contents of the File.
+	Data []byte `json:"data"`
+
+	// Link contains all the information needed to download the file data.
+	Link []byte `json:"link"`
+
+	// Timestamp is the last time the file data, link, or status was modified.
+	Timestamp time.Time `json:"timestamp"`
+
+	// Status of the file in the event model.
+	Status uint8 `json:"status"`
+}
diff --git a/indexedDb/impl/utils_test.go b/indexedDb/impl/utils_test.go
index 6b6da6ff36ee8be5412146443093ee7d85c07c4f..00e235834c44788905af73cafd7448961708a4dc 100644
--- a/indexedDb/impl/utils_test.go
+++ b/indexedDb/impl/utils_test.go
@@ -40,6 +40,20 @@ func TestGetIndex_NoMessageError(t *testing.T) {
 	}
 }
 
+// Test simple put on empty DB is successful
+func TestPut(t *testing.T) {
+	objectStoreName := "messages"
+	db := newTestDB(objectStoreName, "index", t)
+	testValue := js.ValueOf(make(map[string]interface{}))
+	result, err := Put(db, objectStoreName, testValue)
+	if err != nil {
+		t.Fatalf(err.Error())
+	}
+	if !result.Equal(js.ValueOf(1)) {
+		t.Fatalf("Failed to generate autoincremented key")
+	}
+}
+
 // newTestDB creates a new idb.Database for testing.
 func newTestDB(name, index string, t *testing.T) *idb.Database {
 	// Attempt to open database object