git » sdk » commit 8aad23c

Store both timeReceived and timeSent

author Stephen Paul Weber
2026-09-28 18:55:59 UTC
committer Stephen Paul Weber
2026-09-30 18:16:02 UTC
parent b800d5df38bc5abda136a90a278d605a6a519593

Store both timeReceived and timeSent

Intead of an ambiguous "timestamp" store timeReceived (MAM time, or now
for live messages) and timeSent (delayed send time, fallback to
timeReceived) and use them as appropriate for various queries.

borogove/Chat.hx +5 -3
borogove/ChatMessage.hx +18 -6
borogove/ChatMessageBuilder.hx +26 -11
borogove/ChatMessageCombiner.hx +4 -3
borogove/Client.hx +4 -4
borogove/Importer.hx +4 -4
borogove/Message.hx +9 -10
borogove/MessageSync.hx +6 -6
borogove/ModerationAction.hx +5 -3
borogove/Notification.hx +1 -1
borogove/calls/Session.hx +0 -1
borogove/persistence/IDB.js +36 -14
borogove/persistence/Sqlite.hx +47 -26
test/TestChatMessageCombiner.hx +1 -1
test/TestClient.hx +5 -5
test/TestImporter.hx +4 -4
test/TestSortId.hx +1 -1
test/TestSqlite.hx +31 -28
test/persistence-factories.ts +2 -2
test/persistence-tests.ts +30 -30

diff --git a/borogove/Chat.hx b/borogove/Chat.hx
index 7aa5406..4082999 100644
--- a/borogove/Chat.hx
+++ b/borogove/Chat.hx
@@ -1274,7 +1274,8 @@ class DirectChat extends Chat {
 	}
 
 	private function prepareOutgoingMessage(message:ChatMessageBuilder) {
-		message.timestamp = message.timestamp ?? Date.format(std.Date.now());
+		message.timeSent = message.timeSent ?? Date.format(std.Date.now());
+		message.timeReceived = Date.format(std.Date.now());
 		message.direction = MessageSent;
 		message.from = client.jid;
 		message.sender = message.from.asBare();
@@ -1970,7 +1971,7 @@ class Channel extends Chat {
 					}
 
 					// Sort by time so that eg edits go into the past
-					dedupedMessages.sort((x, y) -> Reflect.compare(x.timestamp, y.timestamp));
+					dedupedMessages.sort((x, y) -> Reflect.compare(x.timeReceived, y.timeReceived));
 
 					final readIndex = dedupedMessages.findLastIndex((m) -> m.serverId == readUpToId || !m.isIncoming());
 					if (readIndex < 0) {
@@ -2348,7 +2349,8 @@ class Channel extends Chat {
 
 	private function prepareOutgoingMessage(message:ChatMessageBuilder) {
 		message.type = MessageChannel;
-		message.timestamp = message.timestamp ?? Date.format(std.Date.now());
+		message.timeSent = message.timeSent ?? Date.format(std.Date.now());
+		message.timeReceived = Date.format(std.Date.now());
 		message.direction = MessageSent;
 		message.from = client.jid;
 		message.senderId = self?.id ?? getFullJid().asString(); // TODO: what if we aren't joined and self is null and this is an occupant id room?
diff --git a/borogove/ChatMessage.hx b/borogove/ChatMessage.hx
index 22ad453..de53f3f 100644
--- a/borogove/ChatMessage.hx
+++ b/borogove/ChatMessage.hx
@@ -193,9 +193,19 @@ class ChatMessage {
 	private final replyId : Null<String>;
 
 	/**
-		The timestamp of this message, in format YYYY-MM-DDThh:mm:ss[.sss]Z
+		The timestamp this message was sent, in format YYYY-MM-DDThh:mm:ss[.sss]Z
+
+		If the exact time is not known, the received time may be used
+	**/
+	public final timeSent: String;
+
+	/**
+		The timestamp this message was received, in format YYYY-MM-DDThh:mm:ss[.sss]Z
+
+		This will be the time received by our server, if known, or the time
+		received at the client otherwise.
 	**/
-	public final timestamp: String;
+	public final timeReceived: String;
 
 	@:allow(borogove)
 	private final to: JID;
@@ -311,7 +321,8 @@ class ChatMessage {
 		?type: MessageType,
 		?syncPoint: Bool,
 		?replyId: Null<String>,
-		timestamp: String,
+		timeSent: String,
+		timeReceived: String,
 		to: JID,
 		from: JID,
 		senderId: String,
@@ -340,7 +351,8 @@ class ChatMessage {
 		this.type = params.type ?? MessageChat;
 		this.syncPoint = params.syncPoint ?? false;
 		this.replyId = params.replyId;
-		this.timestamp = params.timestamp;
+		this.timeSent = params.timeSent;
+		this.timeReceived = params.timeReceived;
 		this.to = params.to;
 		this.from = params.from;
 		this.senderId = params.senderId;
@@ -564,11 +576,11 @@ class ChatMessage {
 	**/
 	public function callDuration(): Null<String> {
 		if (versions.length < 2) return null;
-		final startedStr = versions[versions.length - 1].timestamp;
+		final startedStr = versions[versions.length - 1].timeReceived;
 
 		return switch (callStatus()) {
 		case "finish":
-			final endedStr = versions[0].timestamp;
+			final endedStr = versions[0].timeReceived;
 			if (startedStr == null || endedStr == null) return null;
 			final started = DateTime.fromString(startedStr);
 			final ended = DateTime.fromString(endedStr);
diff --git a/borogove/ChatMessageBuilder.hx b/borogove/ChatMessageBuilder.hx
index fe9aee9..a79d138 100644
--- a/borogove/ChatMessageBuilder.hx
+++ b/borogove/ChatMessageBuilder.hx
@@ -59,9 +59,19 @@ class ChatMessageBuilder {
 	private var replyId: Null<String> = null;
 
 	/**
-		The timestamp of this message, in format YYYY-MM-DDThh:mm:ss[.sss]+00:00
+		The timestamp this message was sent, in format YYYY-MM-DDThh:mm:ss[.sss]Z
+
+		If the exact time is not known, the received time may be used
+	**/
+	public var timeSent: Null<String> = null;
+
+	/**
+		The timestamp this message was received, in format YYYY-MM-DDThh:mm:ss[.sss]Z
+
+		This will be the time received by our server, if known, or the time
+		received at the client otherwise.
 	**/
-	public var timestamp: Null<String> = null;
+	public var timeReceived: Null<String> = null;
 
 	@:allow(borogove)
 	private var to: Null<JID> = null;
@@ -176,7 +186,8 @@ class ChatMessageBuilder {
 		?type: MessageType,
 		?syncPoint: Bool,
 		?replyId: Null<String>,
-		?timestamp: String,
+		?timeSent: String,
+		?timeReceived: String,
 		?senderId: String,
 		?replyToMessage: Null<ChatMessage>,
 		?threadId: Null<String>,
@@ -199,7 +210,8 @@ class ChatMessageBuilder {
 		this.type = params?.type ?? MessageChat;
 		this.syncPoint = params?.syncPoint ?? false;
 		this.replyId = params?.replyId;
-		this.timestamp = params?.timestamp;
+		this.timeSent = params?.timeSent;
+		this.timeReceived = params?.timeReceived;
 		this.senderId = params?.senderId;
 		this.replyToMessage = params?.replyToMessage;
 		this.threadId = params?.threadId;
@@ -232,7 +244,8 @@ class ChatMessageBuilder {
 		builder.type = m.type;
 		builder.syncPoint = m.syncPoint;
 		builder.replyId = m.replyId;
-		builder.timestamp = m.timestamp;
+		builder.timeSent = m.timeSent;
+		builder.timeReceived = m.timeReceived;
 		builder.to = m.to;
 		builder.from = m.from;
 		builder.senderId = m.senderId;
@@ -257,7 +270,7 @@ class ChatMessageBuilder {
 	}
 
 	@:allow(borogove)
-	private static function makeModerated(m: ChatMessage, timestamp: String, moderatorId: Null<String>, reason: Null<String>) {
+	private static function makeModerated(m: ChatMessage, timeSent: String, timeReceived: String, moderatorId: Null<String>, reason: Null<String>) {
 		final builder = new ChatMessageBuilder();
 		builder.localId = m.localId;
 		builder.serverId = m.serverId;
@@ -266,7 +279,8 @@ class ChatMessageBuilder {
 		builder.type = m.type;
 		builder.syncPoint = m.syncPoint;
 		builder.replyId = m.replyId;
-		builder.timestamp = m.timestamp;
+		builder.timeSent = m.timeSent;
+		builder.timeReceived = m.timeReceived;
 		builder.to = m.to;
 		builder.from = m.from;
 		builder.senderId = m.senderId;
@@ -279,13 +293,13 @@ class ChatMessageBuilder {
 		builder.direction = m.direction;
 		builder.status = m.status;
 		final cleanedStub = builder.build();
-		final payload = new Stanza("retracted", { xmlns: "urn:xmpp:message-retract:1", stamp: timestamp });
+		final payload = new Stanza("retracted", { xmlns: "urn:xmpp:message-retract:1", stamp: timeSent });
 		if (reason != null) payload.textTag("reason", reason);
 		payload.tag("moderated", { by: moderatorId, xmlns: "urn:xmpp:message-moderate:1" }).up();
 		builder.payloads.push(payload);
-		builder.timestamp = timestamp;
+		builder.timeSent = timeSent;
+		builder.timeReceived = timeReceived;
 		builder.versions = [builder.build(), cleanedStub];
-		builder.timestamp = m.timestamp;
 		return builder.build();
 	}
 
@@ -405,7 +419,8 @@ class ChatMessageBuilder {
 			type: type,
 			syncPoint: syncPoint,
 			replyId: replyId,
-			timestamp: timestamp ?? Date.format(std.Date.now()),
+			timeReceived: timeReceived ?? Date.format(std.Date.now()),
+			timeSent: timeSent ?? timeReceived ?? Date.format(std.Date.now()),
 			to: to,
 			from: from,
 			senderId: senderId,
diff --git a/borogove/ChatMessageCombiner.hx b/borogove/ChatMessageCombiner.hx
index be89ab2..4ef7d23 100644
--- a/borogove/ChatMessageCombiner.hx
+++ b/borogove/ChatMessageCombiner.hx
@@ -37,7 +37,8 @@ class ChatMessageCombiner {
 		builder.serverId = original.serverId;
 		builder.serverIdBy = original.serverIdBy;
 		builder.sortId = original.sortId;
-		builder.timestamp = original.timestamp;
+		builder.timeSent = original.timeSent;
+		builder.timeReceived = original.timeReceived;
 		builder.replyId = original.replyId;
 		builder.reactions = original.reactions;
 		if (original.type == MessageCall) {
@@ -63,14 +64,14 @@ class ChatMessageCombiner {
 			(version, uniqueVersions:Map<String, ChatMessage>) -> {
 				final id = identity(version);
 				final previous = uniqueVersions[id];
-				if (previous == null || version.timestamp > previous.timestamp) {
+				if (previous == null || version.timeReceived > previous.timeReceived) {
 					uniqueVersions.set(id, version);
 				}
 				uniqueVersions;
 			},
 			new Map<String, ChatMessage>()
 		).map(version -> version);
-		sortedVersions.sort((a, b) -> Reflect.compare(b.timestamp, a.timestamp));
+		sortedVersions.sort((a, b) -> Reflect.compare(b.timeReceived, a.timeReceived));
 		return sortedVersions;
 	}
 
diff --git a/borogove/Client.hx b/borogove/Client.hx
index 641f5c7..bc39a66 100644
--- a/borogove/Client.hx
+++ b/borogove/Client.hx
@@ -1533,7 +1533,7 @@ class Client extends EventEmitter {
 						persistence.removeMedia(hash.algorithm, hash.hash);
 					}
 				}
-				moderateMessage = ChatMessageBuilder.makeModerated(moderateMessage, action.timestamp, action.moderatorId, action.reason);
+				moderateMessage = ChatMessageBuilder.makeModerated(moderateMessage, action.timeSent, action.timeReceived, action.moderatorId, action.reason);
 				persistence.updateMessage(accountId(), moderateMessage);
 				resolve(moderateMessage);
 			})
@@ -1989,7 +1989,7 @@ class Client extends EventEmitter {
 	private function sortChats() {
 		chats.sort((a, b) -> {
 			if (a.uiState == b.uiState) {
-				final tcompare = -Reflect.compare(a.lastMessage?.timestamp ?? "0", b.lastMessage?.timestamp ?? "0");
+				final tcompare = -Reflect.compare(a.lastMessage?.timeReceived ?? "0", b.lastMessage?.timeReceived ?? "0");
 				if (tcompare != 0) return tcompare;
 				return Reflect.compare(a.getDisplayName(), b.getDisplayName());
 			} else {
@@ -2047,7 +2047,7 @@ class Client extends EventEmitter {
 
 		persistence.getMessagesByStatus(accountId(), MessagePending).then(messages -> {
 			for (m in messages.filter(m -> m.type != MessageCall)) {
-				if (m.timestamp < tooOld) {
+				if (m.timeSent < tooOld) {
 					persistence.updateMessageStatus(accountId(), m.localId, MessageFailedToSend, "Message too old to auto-resend").then(newM -> {
 						notifyMessageHandlers(newM, StatusEvent);
 					});
@@ -2060,7 +2060,7 @@ class Client extends EventEmitter {
 						stanza.textTag("delay", "Resending after reconnect", {
 							xmlns: "urn:xmpp:delay",
 							from: accountId(),
-							stamp: m.timestamp
+							stamp: m.timeSent
 						});
 						// TODO: don't know if was originally send with OMEMO
 						chat.sendMessageStanza(stanza);
diff --git a/borogove/Importer.hx b/borogove/Importer.hx
index f3961e8..3d5d861 100644
--- a/borogove/Importer.hx
+++ b/borogove/Importer.hx
@@ -146,10 +146,10 @@ class Importer {
 		final sortLower = sortA;
 		sortA = FractionalIndexing.between(sortLower, sortB, FractionalIndexing.BASE_95_DIGITS);
 		final serverReceivedAt = resultStanza.findText("{urn:xmpp:forward:0}forwarded/{urn:xmpp:delay}delay@stamp");
-		var timestamp = serverReceivedAt;
-		if (timestamp != null) {
+		var timeReceived = serverReceivedAt;
+		if (timeReceived != null) {
 			// If no subseconds, fix them to at least sort right
-			timestamp = ~/([0-9][0-9]:[0-9][0-9]:[0-9][0-9])(\.[0-9][0-9][0-9])?/.map(timestamp, (ereg) -> {
+			timeReceived = ~/([0-9][0-9]:[0-9][0-9]:[0-9][0-9])(\.[0-9][0-9][0-9])?/.map(timeReceived, (ereg) -> {
 				if (ereg.matched(2) == null || ereg.matched(2) == ".000") {
 					if (ereg.matched(1) == previousMessageTime) {
 						counterSameTime++;
@@ -179,7 +179,7 @@ class Importer {
 			};
 			builder.serverId = resultStanza.attr.get("id");
 			builder.serverIdBy = source;
-			if (timestamp != null && builder.timestamp == null) builder.timestamp = timestamp;
+			if (timeReceived != null && builder.timeReceived == null) builder.timeReceived = timeReceived;
 			return builder;
 		});
 
diff --git a/borogove/Message.hx b/borogove/Message.hx
index b3c7408..064ad16 100644
--- a/borogove/Message.hx
+++ b/borogove/Message.hx
@@ -74,7 +74,7 @@ class Message {
 
 		var msg = new ChatMessageBuilder();
 		msg.stanza = stanza;
-		msg.timestamp = stanza.findText("{urn:xmpp:delay}delay@stamp");
+		msg.timeSent = stanza.findText("{urn:xmpp:delay}delay@stamp");
 		msg.threadId = stanza.getChildText("thread");
 		msg.lang = stanza.attr.get("xml:lang");
 		msg.text = stanza.getChildText("body");
@@ -247,8 +247,7 @@ class Message {
 		}
 
 		if (addContext != null) msg = addContext(msg, stanza);
-		final timestamp = msg.timestamp ?? Date.format(std.Date.now());
-		msg.timestamp = timestamp;
+		final timeSent = msg.timeSent ?? msg.timeReceived ?? Date.format(std.Date.now());
 
 		if (jmi != null && jmi.name != "accept" && jmi.name != "propose") {
 			msg.versions = [msg.build()];
@@ -267,8 +266,8 @@ class Message {
 					isGroupchat ? null : reactionId,
 					msg.chatId(),
 					msg.senderId ?? throw "no sender",
-					timestamp,
-					reactions.map(text -> new Reaction(msg.senderId ?? throw "no sender", timestamp, text, msg.localId)),
+					timeSent,
+					reactions.map(text -> new Reaction(msg.senderId ?? throw "no sender", timeSent, text, msg.localId)),
 					EmojiReactions
 				)), encryptionInfo);
 			}
@@ -286,7 +285,7 @@ class Message {
 				msg.chatId(),
 				msg.senderId,
 				msg.threadId,
-				ModerateMessageStanza(new ModerationAction(msg.chatId(), moderateServerId, timestamp, by, reason)),
+				ModerateMessageStanza(new ModerationAction(msg.chatId(), moderateServerId, timeSent, msg.timeReceived ?? Date.format(std.Date.now()), by, reason)),
 				encryptionInfo
 			);
 		}
@@ -336,8 +335,8 @@ class Message {
 					isGroupchat ? null : replyToID,
 					msg.chatId(),
 					msg.senderId ?? throw "no sender",
-					timestamp,
-					[new Reaction(msg.senderId ?? throw "no sender", timestamp, text.trim(), msg.localId)],
+					timeSent,
+					[new Reaction(msg.senderId ?? throw "no sender", timeSent, text.trim(), msg.localId)],
 					AppendReactions
 				)), encryptionInfo);
 			}
@@ -356,8 +355,8 @@ class Message {
 								isGroupchat ? null : replyToID,
 								msg.chatId(),
 								msg.senderId ?? throw "no sender",
-								timestamp,
-								[new CustomEmojiReaction(msg.senderId ?? throw "no sender", timestamp, els[0].attr.get("alt") ?? "", hash.serializeUri(), msg.localId)],
+								timeSent,
+								[new CustomEmojiReaction(msg.senderId ?? throw "no sender", timeSent, els[0].attr.get("alt") ?? "", hash.serializeUri(), msg.localId)],
 								AppendReactions
 							)), encryptionInfo);
 						}
diff --git a/borogove/MessageSync.hx b/borogove/MessageSync.hx
index 098ab7e..fbf6cc5 100644
--- a/borogove/MessageSync.hx
+++ b/borogove/MessageSync.hx
@@ -87,12 +87,12 @@ class MessageSync {
 			final sortLower = sortA;
 			sortA = FractionalIndexing.between(sortLower, sortB, FractionalIndexing.BASE_95_DIGITS);
 			final serverReceivedAt = result.findText("{urn:xmpp:forward:0}forwarded/{urn:xmpp:delay}delay@stamp");
-			var timestamp = serverReceivedAt;
-			if (timestamp == null) {
+			var timeReceived = serverReceivedAt;
+			if (timeReceived == null) {
 				trace("MAM result with no timestamp", result);
 			} else {
 				// If no subseconds, fix them to at least sort right
-				timestamp = ~/([0-9][0-9]:[0-9][0-9]:[0-9][0-9])(\.[0-9][0-9][0-9])?/.map(timestamp, (ereg) -> {
+				timeReceived = ~/([0-9][0-9]:[0-9][0-9]:[0-9][0-9])(\.[0-9][0-9][0-9])?/.map(timeReceived, (ereg) -> {
 					if (ereg.matched(2) == null || ereg.matched(2) == ".000") {
 						if (ereg.matched(1) == previousMessageTime) {
 							counterSameTime++;
@@ -136,7 +136,7 @@ class MessageSync {
 						builder.serverId = result.attr.get("id");
 						builder.serverIdBy = serviceJID;
 						builder.encryption = decryptionResult.encryptionInfo;
-						if (timestamp != null && builder.timestamp == null) builder.timestamp = timestamp;
+						if (timeReceived != null && builder.timeReceived == null) builder.timeReceived = timeReceived;
 						return contextHandler(builder, stanza);
 					});
 				}, (err) -> {
@@ -155,7 +155,7 @@ class MessageSync {
 							};
 							builder.serverId = result.attr.get("id");
 							builder.serverIdBy = serviceJID;
-							if (timestamp != null && builder.timestamp == null) builder.timestamp = timestamp;
+							if (timeReceived != null && builder.timeReceived == null) builder.timeReceived = timeReceived;
 							return contextHandler(builder, stanza);
 						},
 						new EncryptionInfo(DecryptionFailure, NS.OMEMO, "OMEMO", "internal-error", Std.string(err))
@@ -181,7 +181,7 @@ class MessageSync {
 				};
 				builder.serverId = result.attr.get("id");
 				builder.serverIdBy = serviceJID;
-				if (timestamp != null && builder.timestamp == null) builder.timestamp = timestamp;
+				if (timeReceived != null && builder.timeReceived == null) builder.timeReceived = timeReceived;
 				return contextHandler(builder, stanza);
 			});
 
diff --git a/borogove/ModerationAction.hx b/borogove/ModerationAction.hx
index 18b438f..bf9ecdc 100644
--- a/borogove/ModerationAction.hx
+++ b/borogove/ModerationAction.hx
@@ -3,14 +3,16 @@ package borogove;
 class ModerationAction {
 	public final chatId: String;
 	public final moderateServerId: String;
-	public final timestamp: String;
+	public final timeSent: String;
+	public final timeReceived: String;
 	public final moderatorId: Null<String>;
 	public final reason: Null<String>;
 
-	public function new(chatId: String, moderateServerId: String, timestamp: String, moderatorId: Null<String>, reason: Null<String>) {
+	public function new(chatId: String, moderateServerId: String, timeSent: String, timeReceived: String, moderatorId: Null<String>, reason: Null<String>) {
 		this.chatId = chatId;
 		this.moderateServerId = moderateServerId;
-		this.timestamp = timestamp;
+		this.timeSent = timeSent;
+		this.timeReceived = timeReceived;
 		this.moderatorId = moderatorId;
 		this.reason = reason;
 	}
diff --git a/borogove/Notification.hx b/borogove/Notification.hx
index ec88e23..0d115d8 100644
--- a/borogove/Notification.hx
+++ b/borogove/Notification.hx
@@ -98,7 +98,7 @@ class Notification {
 			m.callSid(),
 			imageUri,
 			m.lang,
-			m.timestamp
+			m.timeSent
 		);
 	}
 
diff --git a/borogove/calls/Session.hx b/borogove/calls/Session.hx
index 6f3f38e..8d9bff7 100644
--- a/borogove/calls/Session.hx
+++ b/borogove/calls/Session.hx
@@ -61,7 +61,6 @@ private function mkCallMessage(to: JID, client: Client, event: Stanza) {
 	m.replyTo = [m.sender];
 	m.direction = MessageSent;
 	m.setBody(Html.text("call " + event.name));
-	m.timestamp = Date.format(std.Date.now());
 	m.payloads.push(event);
 	m.localId = ID.unique();
 	if (event.name != "propose") {
diff --git a/borogove/persistence/IDB.js b/borogove/persistence/IDB.js
index 97eb0d6..79aaf5b 100644
--- a/borogove/persistence/IDB.js
+++ b/borogove/persistence/IDB.js
@@ -454,6 +454,12 @@ export default async (dbname, media, tokenize, stemmer) => {
 		return newMap;
 	}
 
+	function toISOString(dateOrNothing) {
+		if (!dateOrNothing) return dateOrNothing;
+
+		return dateOrNothing.toISOString();
+	}
+
 	function hydrateMessageSync(value) {
 		if (!value) return null;
 
@@ -467,7 +473,8 @@ export default async (dbname, media, tokenize, stemmer) => {
 		message.direction = value.direction;
 		message.status = value.status;
 		message.statusText = value.statusText;
-		message.timestamp = value.timestamp && value.timestamp.toISOString();
+		message.timeSent = toISOString(value.timeSent || value.timestamp);
+		message.timeReceived = toISOString(value.timeReceived || value.timestamp);
 		message.from = value.from && borogove_JID.parse(value.from);
 		message.sender = value.sender && borogove_JID.parse(value.sender);
 		message.senderId = value.senderId;
@@ -489,7 +496,7 @@ export default async (dbname, media, tokenize, stemmer) => {
 		);
 		message.debug = value.debug ?? null;
 		message.linkMetadata = value.linkMetadata ?? [];
-		message.reactions = hydrateReactions(value.reactions, message.timestamp);
+		message.reactions = hydrateReactions(value.reactions, message.timeSent);
 		message.text = value.text;
 		message.lang = value.lang;
 		message.type =
@@ -572,7 +579,9 @@ export default async (dbname, media, tokenize, stemmer) => {
 			senderId: message.senderId,
 			recipients: message.recipients.map((r) => r.asString()),
 			replyTo: message.replyTo.map((r) => r.asString()),
-			timestamp: new Date(message.timestamp),
+			timeReceived: undefined, // We store it as timestamp for now
+			timestamp: new Date(message.timeReceived),
+			timeSent: new Date(message.timeSent),
 			replyToMessage: message.replyToMessage && [
 				account,
 				message.replyToMessage.serverId || "",
@@ -620,7 +629,8 @@ export default async (dbname, media, tokenize, stemmer) => {
 		head.localId = outer.localId;
 		head.replyId = outer.replyId;
 		// Edited version is not newer
-		head.timestamp = new Date(outer.timestamp);
+		head.timestamp = outer.timestamp ?? new Date(outer.timeReceived);
+		head.timeSent = new Date(outer.timeSent);
 		head.sortId = outer.sortId;
 		head.versions = versions;
 		head.reactions = outer.reactions; // Preserve these, edit doesn't touch them
@@ -1351,7 +1361,7 @@ export default async (dbname, media, tokenize, stemmer) => {
 						reactionResult.value.localId,
 						message.chatId(),
 						message.senderId,
-						message.timestamp,
+						message.timeSent,
 						reactions,
 						enums.borogove_ReactionUpdateKind.CompleteReactions,
 					),
@@ -1487,7 +1497,7 @@ export default async (dbname, media, tokenize, stemmer) => {
 							.openCursor(
 								IDBKeyRange.bound(
 									[account, chatId],
-									[account, chatId, new Date(before.timestamp)],
+									[account, chatId, new Date(before.timeReceived)],
 								),
 								"prev",
 							)
@@ -1507,7 +1517,7 @@ export default async (dbname, media, tokenize, stemmer) => {
 			);
 
 			if (messages.length > 0 && messages[0].serverIdBy === chatId) {
-				const earliest = new Date(messages[messages.length - 1].timestamp);
+				const earliest = new Date(messages[messages.length - 1].timeReceived);
 				const tx = db.transaction(["messages"], "readonly");
 				const store = tx.objectStore("messages");
 				const pmCursor = store
@@ -1515,7 +1525,7 @@ export default async (dbname, media, tokenize, stemmer) => {
 					.openCursor(
 						IDBKeyRange.bound(
 							[account, chatId],
-							[account, chatId, before ? new Date(before.timestamp) : []],
+							[account, chatId, before ? new Date(before.timeReceived) : []],
 						),
 						"prev",
 					);
@@ -1537,7 +1547,9 @@ export default async (dbname, media, tokenize, stemmer) => {
 
 				const pms = await Promise.all(promisePMs);
 				for (const pm of pms) {
-					const idx = messages.findIndex((m) => m.timestamp <= pm.timestamp);
+					const idx = messages.findIndex(
+						(m) => m.timeReceived <= pm.timeReceived,
+					);
 					if (idx >= 0) messages.splice(idx, 0, pm);
 				}
 			}
@@ -1553,7 +1565,7 @@ export default async (dbname, media, tokenize, stemmer) => {
 			const bound = after
 				? [
 						after?.type === enums.borogove_MessageType.MessageChannelPrivate
-							? new Date(after.timestamp)
+							? new Date(after.timeReceived)
 							: after.sortId,
 					]
 				: [];
@@ -1572,14 +1584,18 @@ export default async (dbname, media, tokenize, stemmer) => {
 			);
 
 			if (messages.length > 0 && messages[0].serverIdBy === chatId) {
-				const latest = new Date(messages[messages.length - 1].timestamp);
+				const latest = new Date(messages[messages.length - 1].timeReceived);
 				const tx = db.transaction(["messages"], "readonly");
 				const store = tx.objectStore("messages");
 				const pmCursor = store
 					.index("chats")
 					.openCursor(
 						IDBKeyRange.bound(
-							[account, chatId, ...(after ? [new Date(after.timestamp)] : [])],
+							[
+								account,
+								chatId,
+								...(after ? [new Date(after.timeReceived)] : []),
+							],
 							[account, chatId, []],
 						),
 						"next",
@@ -1604,7 +1620,9 @@ export default async (dbname, media, tokenize, stemmer) => {
 
 				const pms = await Promise.all(promisePMs);
 				for (const pm of pms) {
-					const idx = messages.findLastIndex((m) => m.timestamp < pm.timestamp);
+					const idx = messages.findLastIndex(
+						(m) => m.timeReceived < pm.timeReceived,
+					);
 					if (idx >= 0) messages.splice(idx + 1, 0, pm);
 				}
 			}
@@ -1720,7 +1738,11 @@ export default async (dbname, media, tokenize, stemmer) => {
 			}
 
 			return result.sort((a, b) =>
-				a.timestamp < b.timestamp ? -1 : a.timestamp > b.timestamp ? 1 : 0,
+				a.timeReceived < b.timeReceived
+					? -1
+					: a.timeReceived > b.timeReceived
+						? 1
+						: 0,
 			);
 		},
 
diff --git a/borogove/persistence/Sqlite.hx b/borogove/persistence/Sqlite.hx
index 83a4b71..4d6d677 100644
--- a/borogove/persistence/Sqlite.hx
+++ b/borogove/persistence/Sqlite.hx
@@ -374,6 +374,18 @@ class Sqlite implements Persistence implements KeyValueStore {
 							"PRAGMA user_version = 23"]);
 					}
 					return Promise.resolve(null);
+				}).then(_ -> {
+					if (version < 24) {
+						return exec(["DROP INDEX messages_created_at",
+							"DROP INDEX messages_status",
+							"ALTER TABLE messages RENAME COLUMN created_at TO time_sent",
+							"ALTER TABLE messages ADD COLUMN time_received INTEGER", // should be NOT NULL ideally
+							"CREATE INDEX messages_time_received ON messages (account_id, chat_id, time_received)",
+							"CREATE INDEX messages_status ON messages (account_id, status, time_received)",
+							"UPDATE messages SET time_received=time_sent", // They're all NOT NULL now but sqlite won't let us change it
+							"PRAGMA user_version = 24"]);
+					}
+					return Promise.resolve(null);
 				});
 			});
 		});
@@ -924,9 +936,9 @@ class Sqlite implements Persistence implements KeyValueStore {
 		var q = "WITH page AS (SELECT stanza_id, mam_id FROM messages where account_id=? AND chat_id=? AND (stanza_id IS NULL OR stanza_id='' OR stanza_id=correction_id) AND type<>?";
 		final params: Array<Dynamic> = [accountId, chatId, MessageChannelPrivate];
 		if (useTimestamp) {
-			q += " AND messages.created_at " + op + " unixepoch(?, 'subsec') * 1000";
+			q += " AND messages.time_received " + op + " unixepoch(?, 'subsec') * 1000";
 			params.push(timestamp);
-			q += " ORDER BY messages.created_at";
+			q += " ORDER BY messages.time_received";
 		} else if (sortId != null) {
 			q += " AND messages.sort_id " + op + " ?";
 			params.push(sortId);
@@ -949,7 +961,7 @@ class Sqlite implements Persistence implements KeyValueStore {
 		';
 		q += " ORDER BY messages.sort_id";
 		if (op == "<" || op == "<=") q += " DESC";
-		q += ", messages.created_at";
+		q += ", messages.time_received";
 		if (op == "<" || op == "<=") q += " DESC";
 
 		params.push(accountId);
@@ -959,7 +971,7 @@ class Sqlite implements Persistence implements KeyValueStore {
 		return db.exec(q, params).then(result -> {
 			final messages = hydrateMessages(accountId, result);
 			if (messages.length > 0 && messages[0].serverIdBy == chatId) {
-				final boundary = messages[messages.length - 1].timestamp;
+				final boundary = messages[messages.length - 1].timeReceived;
 				var pmQ = '
 					SELECT
 						${messageColumnsString()}
@@ -969,18 +981,18 @@ class Sqlite implements Persistence implements KeyValueStore {
 				final pmParams: Array<Dynamic> = [accountId, chatId, MessageChannelPrivate];
 
 				if (timestamp != null) {
-					pmQ += " AND messages.created_at " + op + " unixepoch(?, 'subsec') * 1000";
+					pmQ += " AND messages.time_received " + op + " unixepoch(?, 'subsec') * 1000";
 					pmParams.push(timestamp);
 				}
 
 				if (op == "<" || op == "<=") {
-					pmQ += " AND messages.created_at > unixepoch(?, 'subsec') * 1000";
+					pmQ += " AND messages.time_received > unixepoch(?, 'subsec') * 1000";
 					pmParams.push(boundary);
 				} else {
 					// We don't want to limit by timestamp in SQL because we want any PMs coming after the end but before the next message
 				}
 
-				pmQ += " ORDER BY messages.created_at";
+				pmQ += " ORDER BY messages.time_received";
 				if (op == "<" || op == "<=") q += " DESC";
 				pmQ += " LIMIT 50";
 
@@ -988,19 +1000,19 @@ class Sqlite implements Persistence implements KeyValueStore {
 					final pms = hydrateMessages(accountId, pmResult);
 					for (pm in pms) {
 						if (op == "<" || op == "<=") {
-							final idx = messages.findIndex(m -> m.timestamp <= pm.timestamp);
+							final idx = messages.findIndex(m -> m.timeReceived <= pm.timeReceived);
 							if (idx >= 0) messages.insert(idx, pm);
 						} else {
 							var idx = messages.length - 1;
 							while (idx >= 0) {
-								if (messages[idx].timestamp < pm.timestamp) {
+								if (messages[idx].timeReceived < pm.timeReceived) {
 									break;
 								}
 								idx--;
 							}
 							if (idx >= 0) messages.insert(idx + 1, pm);
 
-							if (pm.timestamp > boundary) break;
+							if (pm.timeReceived > boundary) break;
 						}
 					}
 					return messages;
@@ -1017,20 +1029,20 @@ class Sqlite implements Persistence implements KeyValueStore {
 
 	@HaxeCBridge.noemit
 	public function getMessagesBefore(accountId: String, chatId: String, before: Null<ChatMessage>): Promise<Array<ChatMessage>> {
-		return getMessages(accountId, chatId, before?.sortId, "<", before?.type == MessageChannelPrivate, before?.timestamp);
+		return getMessages(accountId, chatId, before?.sortId, "<", before?.type == MessageChannelPrivate, before?.timeReceived);
 	}
 
 	@HaxeCBridge.noemit
 	public function getMessagesAfter(accountId: String, chatId: String, after: Null<ChatMessage>): Promise<Array<ChatMessage>> {
-		return getMessages(accountId, chatId, after?.sortId, ">", after?.type == MessageChannelPrivate, after?.timestamp);
+		return getMessages(accountId, chatId, after?.sortId, ">", after?.type == MessageChannelPrivate, after?.timeReceived);
 	}
 
 	@HaxeCBridge.noemit
 	public function getMessagesAround(accountId: String, around: ChatMessage): Promise<Array<ChatMessage>> {
 		final chatId = around.chatId();
 		return thenshim.PromiseTools.all([
-			getMessages(accountId, chatId, around.sortId, "<", around?.type == MessageChannelPrivate, around?.timestamp),
-			getMessages(accountId, chatId, around.sortId, ">=", around?.type == MessageChannelPrivate, around?.timestamp)
+			getMessages(accountId, chatId, around.sortId, "<", around?.type == MessageChannelPrivate, around?.timeReceived),
+			getMessages(accountId, chatId, around.sortId, ">=", around?.type == MessageChannelPrivate, around?.timeReceived)
 		]).then(results -> results.flatten());
 	}
 
@@ -1042,7 +1054,7 @@ class Sqlite implements Persistence implements KeyValueStore {
 					${messageColumnsString()}
 				FROM messages
 				WHERE account_id=? AND status=?
-				ORDER BY created_at
+				ORDER BY time_received
 			',
 			[accountId, status]
 		).then(result -> hydrateMessagesAndLoadMeta(accountId, result));
@@ -1464,7 +1476,8 @@ class Sqlite implements Persistence implements KeyValueStore {
 		accountId: String,
 		rows: Iterator<{
 			stanza: String,
-			timestamp: String,
+			time_sent: String,
+			time_received: String,
 			direction: MessageDirection,
 			type: MessageType,
 			status: MessageStatus,
@@ -1503,7 +1516,8 @@ class Sqlite implements Persistence implements KeyValueStore {
 		accountId: String,
 		rows: Iterator<{
 			stanza: String,
-			timestamp: String,
+			time_sent: String,
+			time_received: String,
 			direction: MessageDirection,
 			type: MessageType,
 			status: MessageStatus,
@@ -1523,7 +1537,8 @@ class Sqlite implements Persistence implements KeyValueStore {
 		final accountJid = JID.parse(accountId);
 		return { iterator: () -> rows }.map(row -> ChatMessage.fromStanza(Stanza.parse(row.stanza), accountJid, (builder, _) -> {
 			builder.syncPoint = row.sync_point != 0;
-			builder.timestamp = row.timestamp;
+			builder.timeSent = row.time_sent;
+			builder.timeReceived = row.time_received;
 			builder.type = row.type;
 			builder.status = row.status;
 			builder.statusText = row.status_text;
@@ -1541,7 +1556,8 @@ class Sqlite implements Persistence implements KeyValueStore {
 			if (row.stanza_id != null && row.stanza_id != "") builder.localId = row.stanza_id;
 			if (row.versions != null) {
 				final versions: DynamicAccess<{
-					timestamp: String,
+					time_sent: String,
+					time_received: String,
 					sort_id: String,
 					stanza: String,
 					encryption: Dynamic,
@@ -1552,7 +1568,8 @@ class Sqlite implements Persistence implements KeyValueStore {
 					for (versionId => version in versions) {
 						final versionM = ChatMessage.fromStanza(Stanza.parse(version.stanza), accountJid, (toPushB, _) -> {
 							if (toPushB.serverId == null && versionId != toPushB.localId) toPushB.serverId = versionId;
-							toPushB.timestamp = version.timestamp;
+							toPushB.timeSent = version.time_sent;
+							toPushB.timeReceived = version.time_received;
 							toPushB.sortId = version.sort_id;
 							toPushB.debug = version.debug;
 							return toPushB;
@@ -1562,7 +1579,7 @@ class Sqlite implements Persistence implements KeyValueStore {
 							builder.versions.push(toPush);
 						}
 					}
-					builder.versions.sort((a, b) -> Reflect.compare(b.timestamp, a.timestamp));
+					builder.versions.sort((a, b) -> Reflect.compare(b.timeReceived, a.timeReceived));
 				}
 			}
 			return builder;
@@ -1813,7 +1830,8 @@ class Sqlite implements Persistence implements KeyValueStore {
 			col("type"),
 			col("status"),
 			col("status_text"),
-			col("timestamp", "strftime('%FT%H:%M:%fZ', created_at / 1000.0, 'unixepoch') AS timestamp"),
+			col("time_sent", "strftime('%FT%H:%M:%fZ', time_sent / 1000.0, 'unixepoch') AS time_sent"),
+			col("time_received", "strftime('%FT%H:%M:%fZ', time_received / 1000.0, 'unixepoch') AS time_received"),
 			col("sender_id"),
 			col("mam_id"),
 			col("mam_by"),
@@ -1839,7 +1857,8 @@ class Sqlite implements Persistence implements KeyValueStore {
 				ELSE versions.mam_id
 			END,
 			json_object(
-				'timestamp', strftime('%FT%H:%M:%fZ', versions.created_at / 1000.0, 'unixepoch'),
+				'time_sent', strftime('%FT%H:%M:%fZ', versions.time_sent / 1000.0, 'unixepoch'),
+				'time_received', strftime('%FT%H:%M:%fZ', versions.time_received / 1000.0, 'unixepoch'),
 				'sort_id', versions.sort_id,
 				'stanza', versions.stanza,
 				'encryption', json(versions.encryption),
@@ -1850,13 +1869,14 @@ class Sqlite implements Persistence implements KeyValueStore {
 		messages.type,
 		messages.status,
 		messages.status_text,
-		strftime('%FT%H:%M:%fZ', messages.created_at / 1000.0, 'unixepoch') AS timestamp,
+		strftime('%FT%H:%M:%fZ', messages.time_sent / 1000.0, 'unixepoch') AS time_sent,
+		strftime('%FT%H:%M:%fZ', messages.time_received / 1000.0, 'unixepoch') AS time_received,
 		messages.sender_id,
 		messages.mam_id,
 		messages.mam_by,
 		messages.sort_id,
 		messages.sync_point,
-		MAX(versions.created_at),
+		MAX(versions.time_received),
 		json(versions.encryption) AS encryption,
 		json(versions.debug) AS debug,
 		versions.stanza";
@@ -1883,7 +1903,8 @@ class Sqlite implements Persistence implements KeyValueStore {
 			insertCol("sync_point", (m) -> m.syncPoint),
 			insertCol("chat_id", (m) -> m.chatId()),
 			insertCol("sender_id", (m) -> m.senderId),
-			insertCol("created_at", (m) -> originalMessage(m).timestamp, "CAST(unixepoch(?, 'subsec') * 1000 AS INTEGER)"),
+			insertCol("time_sent", (m) -> originalMessage(m).timeSent, "CAST(unixepoch(?, 'subsec') * 1000 AS INTEGER)"),
+			insertCol("time_received", (m) -> originalMessage(m).timeReceived, "CAST(unixepoch(?, 'subsec') * 1000 AS INTEGER)"),
 			insertCol("status", (m) -> originalMessage(m).status),
 			insertCol("direction", (m) -> originalMessage(m).direction),
 			insertCol("type", (m) -> originalMessage(m).type),
diff --git a/test/TestChatMessageCombiner.hx b/test/TestChatMessageCombiner.hx
index 0a7f468..1459cff 100644
--- a/test/TestChatMessageCombiner.hx
+++ b/test/TestChatMessageCombiner.hx
@@ -116,7 +116,7 @@ class TestChatMessageCombiner extends utest.Test {
 		builder.senderId = params.senderId ?? "alice@example.com";
 		builder.type = params.type ?? MessageChat;
 		builder.versions = params.versions ?? [];
-		builder.timestamp = params.timestamp;
+		builder.timeReceived = params.timestamp;
 		builder.sortId = params.sortId;
 		builder.setBody(borogove.Html.text(params.body ?? "message"));
 		return builder.build();
diff --git a/test/TestClient.hx b/test/TestClient.hx
index 026b659..8b0fa6e 100644
--- a/test/TestClient.hx
+++ b/test/TestClient.hx
@@ -142,10 +142,10 @@ class TestClient extends utest.Test {
 		client.checkForResends();
 	}
 
-	private function pendingMessage(localId: String, timestamp: String): ChatMessage {
+	private function pendingMessage(localId: String, timeSent: String): ChatMessage {
 		final builder = new ChatMessageBuilder();
 		builder.localId = localId;
-		builder.timestamp = timestamp;
+		builder.timeSent = timeSent;
 		builder.senderId = "test@example.com";
 		builder.from = JID.parse("test@example.com");
 		builder.to = JID.parse("friend@example.com");
@@ -311,7 +311,7 @@ class TestClient extends utest.Test {
 		final originalMessage = builder.build();
 
 		persistence.storeMessages(client.accountId(), [originalMessage]).then((_) -> {
-			final action = new ModerationAction(chatId, serverId, "2023-01-01T00:00:00Z", "mod@example.com", "Spam");
+			final action = new ModerationAction(chatId, serverId, "2023-01-01T00:00:00Z", "2023-01-01T00:00:00Z", "mod@example.com", "Spam");
 
 			client.moderateMessage(action).then((moderatedMessage) -> {
 				Assert.notNull(moderatedMessage);
@@ -337,7 +337,7 @@ class TestClient extends utest.Test {
 		final originalMessage = builder.build();
 
 		persistence.storeMessages(client.accountId(), [originalMessage]).then((_) -> {
-			final action = new ModerationAction(chatId, serverId, "2023-01-01T00:00:00Z", "mod@example.com", null);
+			final action = new ModerationAction(chatId, serverId, "2023-01-01T00:00:00Z", "2023-01-01T00:00:00Z", "mod@example.com", null);
 
 			client.moderateMessage(action).then((moderatedMessage) -> {
 				Assert.notNull(moderatedMessage);
@@ -364,7 +364,7 @@ class TestClient extends utest.Test {
 		builder.to = JID.parse("test@example.com");
 		builder.senderId = "alice@example.com";
 		builder.text = "hello";
-		builder.timestamp = "2023-01-01T00:00:00Z";
+		builder.timeReceived = "2023-01-01T00:00:00Z";
 		builder.sortId = "1";
 		builder.direction = MessageReceived;
 		final originalMessage = builder.build();
diff --git a/test/TestImporter.hx b/test/TestImporter.hx
index c876b79..83d2f85 100644
--- a/test/TestImporter.hx
+++ b/test/TestImporter.hx
@@ -83,7 +83,7 @@ class TestImporter extends utest.Test {
 				Assert.equals("Hello Romeo", chatMsg.body().toPlainText());
 				Assert.equals("mam-id-1", chatMsg.serverId);
 				Assert.equals("juliet@capulet.com", chatMsg.serverIdBy);
-				Assert.equals("2023-10-26T09:00:00Z", chatMsg.timestamp);
+				Assert.equals("2023-10-26T09:00:00Z", chatMsg.timeSent);
 				Assert.equals("2023-10-27T10:00:00Z", chatMsg.debug.serverReceivedAt);
 				Assert.equals("import", chatMsg.debug.source);
 				Assert.isFalse(chatMsg.isIncoming());
@@ -169,15 +169,15 @@ class TestImporter extends utest.Test {
 		Assert.equals(3, items.length);
 
 		switch (items[0].parsed) {
-			case ChatMessageStanza(m): Assert.equals("2023-10-27T10:00:00.001Z", m.timestamp);
+			case ChatMessageStanza(m): Assert.equals("2023-10-27T10:00:00.001Z", m.timeReceived);
 			default: Assert.fail();
 		}
 		switch (items[1].parsed) {
-			case ChatMessageStanza(m): Assert.equals("2023-10-27T10:00:00.002Z", m.timestamp);
+			case ChatMessageStanza(m): Assert.equals("2023-10-27T10:00:00.002Z", m.timeReceived);
 			default: Assert.fail();
 		}
 		switch (items[2].parsed) {
-			case ChatMessageStanza(m): Assert.equals("2023-10-27T10:00:01.001Z", m.timestamp);
+			case ChatMessageStanza(m): Assert.equals("2023-10-27T10:00:01.001Z", m.timeReceived);
 			default: Assert.fail();
 		}
 	}
diff --git a/test/TestSortId.hx b/test/TestSortId.hx
index 9f016d1..79c8427 100644
--- a/test/TestSortId.hx
+++ b/test/TestSortId.hx
@@ -142,7 +142,7 @@ class TestSortId extends utest.Test {
 			Assert.isTrue("a " < m1.sortId, "\"a \" < m1.sortId");
 			Assert.isTrue(m1.sortId < m2.sortId, "m1.sortId < m2.sortId");
 			Assert.isTrue(m2.sortId < "b00", "m2.sortId < \"b00\"");
-			Assert.isTrue(m1.timestamp < m2.timestamp, "m1.timestamp < m2.timestamp"); // fake fractional part
+			Assert.isTrue(m1.timeReceived < m2.timeReceived, "m1.timeReceived < m2.timeReceived"); // fake fractional part
 			Assert.equals("mam", m1.debug.source);
 			Assert.equals("2023-01-01T00:00:00Z", m1.debug.serverReceivedAt);
 			Assert.equals("2023-01-01T00:00:00Z", m2.debug.serverReceivedAt);
diff --git a/test/TestSqlite.hx b/test/TestSqlite.hx
index 7eb75ef..7e76a97 100644
--- a/test/TestSqlite.hx
+++ b/test/TestSqlite.hx
@@ -152,7 +152,7 @@ class TestSqlite extends utest.Test {
 		builder.senderId = "teaparty@example.com/hatter";
 		builder.direction = MessageReceived;
 		builder.type = MessageChannel;
-		builder.timestamp = "2020-01-01T00:00:01Z";
+		builder.timeReceived = "2020-01-01T00:00:01Z";
 		builder.sortId = "a0";
 		builder.to = JID.parse("alice@example.com");
 		builder.from = JID.parse("teaparty@example.com/hatter");
@@ -164,7 +164,7 @@ class TestSqlite extends utest.Test {
 		builder2.senderId = "teaparty@example.com/hatter";
 		builder2.direction = MessageReceived;
 		builder2.type = MessageChannel;
-		builder2.timestamp = "2020-01-01T00:00:00Z";
+		builder2.timeReceived = "2020-01-01T00:00:00Z";
 		builder2.sortId = "b0";
 		builder2.to = JID.parse("alice@example.com");
 		builder2.from = JID.parse("teaparty@example.com/hatter");
@@ -176,7 +176,7 @@ class TestSqlite extends utest.Test {
 		builder3.senderId = "teaparty@example.com/hatter";
 		builder3.direction = MessageReceived;
 		builder3.type = MessageChannelPrivate;
-		builder3.timestamp = "2020-01-01T00:00:03Z";
+		builder3.timeReceived = "2020-01-01T00:00:03Z";
 		builder3.sortId = "a0";
 		builder3.to = JID.parse("alice@example.com");
 		builder3.from = JID.parse("teaparty@example.com/hatter");
@@ -208,7 +208,7 @@ class TestSqlite extends utest.Test {
 		builder.senderId = "teaparty@example.com/hatter";
 		builder.direction = MessageReceived;
 		builder.type = MessageChannel;
-		builder.timestamp = "2020-01-01T00:00:01Z";
+		builder.timeReceived = "2020-01-01T00:00:01Z";
 		builder.sortId = "a0";
 		builder.to = JID.parse("alice@example.com");
 		builder.from = JID.parse("teaparty@example.com/hatter");
@@ -220,7 +220,7 @@ class TestSqlite extends utest.Test {
 		builder2.senderId = "teaparty@example.com/hatter";
 		builder2.direction = MessageReceived;
 		builder2.type = MessageChannel;
-		builder2.timestamp = "2020-01-01T00:00:00Z";
+		builder2.timeReceived = "2020-01-01T00:00:00Z";
 		builder2.sortId = "b0";
 		builder2.to = JID.parse("alice@example.com");
 		builder2.from = JID.parse("teaparty@example.com/hatter");
@@ -232,7 +232,7 @@ class TestSqlite extends utest.Test {
 		builder3.senderId = "teaparty@example.com/hatter";
 		builder3.direction = MessageReceived;
 		builder3.type = MessageChannelPrivate;
-		builder3.timestamp = "2020-01-01T00:00:03Z";
+		builder3.timeReceived = "2020-01-01T00:00:03Z";
 		builder3.sortId = "Z~";
 		builder3.to = JID.parse("alice@example.com");
 		builder3.from = JID.parse("teaparty@example.com/hatter");
@@ -244,7 +244,7 @@ class TestSqlite extends utest.Test {
 		builder4.senderId = "teaparty@example.com/hatter";
 		builder4.direction = MessageReceived;
 		builder4.type = MessageChannel;
-		builder4.timestamp = "2020-01-01T00:00:04Z";
+		builder4.timeReceived = "2020-01-01T00:00:04Z";
 		builder4.sortId = "c0";
 		builder4.to = JID.parse("alice@example.com");
 		builder4.from = JID.parse("teaparty@example.com/hatter");
@@ -277,7 +277,7 @@ class TestSqlite extends utest.Test {
 		builder.senderId = "teaparty@example.com/hatter";
 		builder.direction = MessageReceived;
 		builder.type = MessageChannel;
-		builder.timestamp = "2020-01-01T00:00:00Z";
+		builder.timeReceived = "2020-01-01T00:00:00Z";
 		builder.sortId = "a0";
 		builder.to = JID.parse("alice@example.com");
 		builder.from = JID.parse("teaparty@example.com/hatter");
@@ -289,7 +289,7 @@ class TestSqlite extends utest.Test {
 		builder2.senderId = "teaparty@example.com/hatter";
 		builder2.direction = MessageReceived;
 		builder2.type = MessageChannel;
-		builder2.timestamp = "2020-01-01T00:00:01Z";
+		builder2.timeReceived = "2020-01-01T00:00:01Z";
 		builder2.sortId = "b0";
 		builder2.to = JID.parse("alice@example.com");
 		builder2.from = JID.parse("teaparty@example.com/hatter");
@@ -301,7 +301,7 @@ class TestSqlite extends utest.Test {
 		builder3.senderId = "teaparty@example.com/hatter";
 		builder3.direction = MessageReceived;
 		builder3.type = MessageChannelPrivate;
-		builder3.timestamp = "2020-01-01T00:00:03Z";
+		builder3.timeReceived = "2020-01-01T00:00:03Z";
 		builder3.sortId = "Z~";
 		builder3.to = JID.parse("alice@example.com");
 		builder3.from = JID.parse("teaparty@example.com/hatter");
@@ -313,7 +313,7 @@ class TestSqlite extends utest.Test {
 		builder4.senderId = "teaparty@example.com/hatter";
 		builder4.direction = MessageReceived;
 		builder4.type = MessageChannel;
-		builder4.timestamp = "2020-01-01T00:00:04Z";
+		builder4.timeReceived = "2020-01-01T00:00:04Z";
 		builder4.sortId = "c0";
 		builder4.to = JID.parse("alice@example.com");
 		builder4.from = JID.parse("teaparty@example.com/hatter");
@@ -345,7 +345,7 @@ class TestSqlite extends utest.Test {
 		builder.senderId = "teaparty@example.com/hatter";
 		builder.direction = MessageReceived;
 		builder.type = MessageChannel;
-		builder.timestamp = "2020-01-01T00:00:00Z";
+		builder.timeReceived = "2020-01-01T00:00:00Z";
 		builder.sortId = "a0";
 		builder.to = JID.parse("alice@example.com");
 		builder.from = JID.parse("teaparty@example.com/hatter");
@@ -357,7 +357,7 @@ class TestSqlite extends utest.Test {
 		builder2.senderId = "teaparty@example.com/hatter";
 		builder2.direction = MessageReceived;
 		builder2.type = MessageChannel;
-		builder2.timestamp = "2020-01-01T00:00:01Z";
+		builder2.timeReceived = "2020-01-01T00:00:01Z";
 		builder2.sortId = "b0";
 		builder2.to = JID.parse("alice@example.com");
 		builder2.from = JID.parse("teaparty@example.com/hatter");
@@ -369,7 +369,7 @@ class TestSqlite extends utest.Test {
 		builder3.senderId = "teaparty@example.com/hatter";
 		builder3.direction = MessageReceived;
 		builder3.type = MessageChannelPrivate;
-		builder3.timestamp = "2020-01-01T00:00:03Z";
+		builder3.timeReceived = "2020-01-01T00:00:03Z";
 		builder3.sortId = "a1";
 		builder3.to = JID.parse("alice@example.com");
 		builder3.from = JID.parse("teaparty@example.com/hatter");
@@ -401,7 +401,7 @@ class TestSqlite extends utest.Test {
 		builder.senderId = "teaparty@example.com/hatter";
 		builder.direction = MessageReceived;
 		builder.type = MessageChannel;
-		builder.timestamp = "2020-01-01T00:00:01Z";
+		builder.timeReceived = "2020-01-01T00:00:01Z";
 		builder.sortId = "a0";
 		builder.to = JID.parse("alice@example.com");
 		builder.from = JID.parse("teaparty@example.com/hatter");
@@ -413,7 +413,7 @@ class TestSqlite extends utest.Test {
 		builder2.senderId = "teaparty@example.com/hatter";
 		builder2.direction = MessageReceived;
 		builder2.type = MessageChannel;
-		builder2.timestamp = "2020-01-01T00:00:00Z";
+		builder2.timeReceived = "2020-01-01T00:00:00Z";
 		builder2.sortId = "b0";
 		builder2.to = JID.parse("alice@example.com");
 		builder2.from = JID.parse("teaparty@example.com/hatter");
@@ -425,7 +425,7 @@ class TestSqlite extends utest.Test {
 		builder3.senderId = "teaparty@example.com/hatter";
 		builder3.direction = MessageReceived;
 		builder3.type = MessageChannelPrivate;
-		builder3.timestamp = "2020-01-01T00:00:03Z";
+		builder3.timeReceived = "2020-01-01T00:00:03Z";
 		builder3.sortId = "Z~";
 		builder3.to = JID.parse("alice@example.com");
 		builder3.from = JID.parse("teaparty@example.com/hatter");
@@ -437,7 +437,7 @@ class TestSqlite extends utest.Test {
 		builder4.senderId = "teaparty@example.com/hatter";
 		builder4.direction = MessageReceived;
 		builder4.type = MessageChannel;
-		builder4.timestamp = "2020-01-01T00:00:04Z";
+		builder4.timeReceived = "2020-01-01T00:00:04Z";
 		builder4.sortId = "c0";
 		builder4.to = JID.parse("alice@example.com");
 		builder4.from = JID.parse("teaparty@example.com/hatter");
@@ -470,7 +470,7 @@ class TestSqlite extends utest.Test {
 		builder.senderId = "teaparty@example.com/hatter";
 		builder.direction = MessageReceived;
 		builder.type = MessageChannel;
-		builder.timestamp = "2020-01-01T00:00:00Z";
+		builder.timeReceived = "2020-01-01T00:00:00Z";
 		builder.sortId = "a0";
 		builder.to = JID.parse("alice@example.com");
 		builder.from = JID.parse("teaparty@example.com/hatter");
@@ -482,7 +482,7 @@ class TestSqlite extends utest.Test {
 		builder2.senderId = "teaparty@example.com/hatter";
 		builder2.direction = MessageReceived;
 		builder2.type = MessageChannel;
-		builder2.timestamp = "2020-01-01T00:00:01Z";
+		builder2.timeReceived = "2020-01-01T00:00:01Z";
 		builder2.sortId = "b0";
 		builder2.to = JID.parse("alice@example.com");
 		builder2.from = JID.parse("teaparty@example.com/hatter");
@@ -494,7 +494,7 @@ class TestSqlite extends utest.Test {
 		builder3.senderId = "teaparty@example.com/hatter";
 		builder3.direction = MessageReceived;
 		builder3.type = MessageChannelPrivate;
-		builder3.timestamp = "2020-01-01T00:00:03Z";
+		builder3.timeReceived = "2020-01-01T00:00:03Z";
 		builder3.sortId = "Z~";
 		builder3.to = JID.parse("alice@example.com");
 		builder3.from = JID.parse("teaparty@example.com/hatter");
@@ -506,7 +506,7 @@ class TestSqlite extends utest.Test {
 		builder4.senderId = "teaparty@example.com/hatter";
 		builder4.direction = MessageReceived;
 		builder4.type = MessageChannel;
-		builder4.timestamp = "2020-01-01T00:00:04Z";
+		builder4.timeReceived = "2020-01-01T00:00:04Z";
 		builder4.sortId = "c0";
 		builder4.to = JID.parse("alice@example.com");
 		builder4.from = JID.parse("teaparty@example.com/hatter");
@@ -706,9 +706,9 @@ class TestSqlite extends utest.Test {
 		});
 
 		persistence.storeMessages(account, [message]).then(_ -> {
-			return persistence.db.exec("SELECT strftime('%FT%H:%M:%fZ', created_at / 1000.0, 'unixepoch') AS timestamp FROM messages WHERE mam_id=?", [message.serverId]);
+			return persistence.db.exec("SELECT strftime('%FT%H:%M:%fZ', time_received / 1000.0, 'unixepoch') AS time_received FROM messages WHERE mam_id=?", [message.serverId]);
 		}).then(rows -> {
-			Assert.equals("2020-01-01T00:00:00.123Z", rows.next().timestamp);
+			Assert.equals("2020-01-01T00:00:00.123Z", rows.next().time_received);
 			async.done();
 		}).catchError(e -> {
 			Assert.fail(Std.string(e));
@@ -781,7 +781,7 @@ class TestSqlite extends utest.Test {
 		builder.senderId = senderId;
 		builder.direction = params.received ?? false ? MessageReceived : MessageSent;
 		builder.sortId = params.sortId ?? params.localId ?? params.serverId ?? "message";
-		builder.timestamp = params.timestamp;
+		builder.timeReceived = params.timestamp;
 		builder.syncPoint = params.syncPoint ?? false;
 		builder.status = params.status ?? MessagePending;
 		builder.versions = params.versions ?? [];
@@ -2065,7 +2065,8 @@ class TestSqlite extends utest.Test {
 			"type",
 			"status",
 			"status_text",
-			"strftime('%FT%H:%M:%fZ', created_at / 1000.0, 'unixepoch') AS timestamp",
+			"strftime('%FT%H:%M:%fZ', time_sent / 1000.0, 'unixepoch') AS time_sent",
+			"strftime('%FT%H:%M:%fZ', time_received / 1000.0, 'unixepoch') AS time_received",
 			"sender_id",
 			"mam_id",
 			"mam_by",
@@ -2089,7 +2090,8 @@ class TestSqlite extends utest.Test {
 			"type",
 			"status",
 			"status_text",
-			"strftime('%FT%H:%M:%fZ', created_at / 1000.0, 'unixepoch') AS timestamp",
+			"strftime('%FT%H:%M:%fZ', time_sent / 1000.0, 'unixepoch') AS time_sent",
+			"strftime('%FT%H:%M:%fZ', time_received / 1000.0, 'unixepoch') AS time_received",
 			"sender_id",
 			"mam_id",
 			"mam_by",
@@ -2117,7 +2119,8 @@ class TestSqlite extends utest.Test {
 			"type",
 			"status",
 			"status_text",
-			"strftime('%FT%H:%M:%fZ', created_at / 1000.0, 'unixepoch') AS timestamp",
+			"strftime('%FT%H:%M:%fZ', time_sent / 1000.0, 'unixepoch') AS time_sent",
+			"strftime('%FT%H:%M:%fZ', time_received / 1000.0, 'unixepoch') AS time_received",
 			"sender_id",
 			"mam_id",
 			"mam_by",
diff --git a/test/persistence-factories.ts b/test/persistence-factories.ts
index 53cb915..0963be2 100644
--- a/test/persistence-factories.ts
+++ b/test/persistence-factories.ts
@@ -53,13 +53,13 @@ export function createFactories(borogove: any) {
 		builder.replyTo = replyTo ?? [fromJID];
 		builder.versions = versions ?? [];
 		builder.text = text ?? faker.lorem.sentence();
-		builder.timestamp = timestamp?.toISOString() ?? new Date().toISOString();
+		builder.timeReceived = timestamp?.toISOString() ?? new Date().toISOString();
 
 		return builder.build();
 	};
 
 	const corrections = (original: any, props: CorrectionProps[]) => {
-		const originalTimestamp = new Date(original.timestamp).getTime();
+		const originalTimestamp = new Date(original.timeReceived).getTime();
 
 		return props.map(({ text }, index) => {
 			const versionTimestamp = new Date(originalTimestamp + index + 1);
diff --git a/test/persistence-tests.ts b/test/persistence-tests.ts
index a35a3e7..6c2ace1 100644
--- a/test/persistence-tests.ts
+++ b/test/persistence-tests.ts
@@ -13,7 +13,7 @@ export function sharedPersistenceTests(test) {
 				senderId: account,
 				direction: borogove.MessageDirection.MessageSent,
 			});
-			builder.timestamp = timestamp;
+			builder.timeReceived = timestamp;
 			builder.sortId = localId;
 			builder.status = status;
 			builder.to = borogove.JID.parse("hatter@example.com");
@@ -441,7 +441,7 @@ export function sharedPersistenceTests(test) {
 			localId: "encrypted-original",
 			senderId: account,
 			direction: 1,
-			timestamp: "2026-08-26T12:00:00Z",
+			timeReceived: "2026-08-26T12:00:00Z",
 		});
 		originalBuilder.sortId = "encrypted-c0";
 		originalBuilder.text = "Original encrypted text";
@@ -463,7 +463,7 @@ export function sharedPersistenceTests(test) {
 			localId: "encrypted-correction",
 			senderId: account,
 			direction: 1,
-			timestamp: "2026-08-26T12:01:00Z",
+			timeReceived: "2026-08-26T12:01:00Z",
 		});
 		correctionBuilder.sortId = "encrypted-c0";
 		correctionBuilder.text = "Corrected encrypted text";
@@ -527,7 +527,7 @@ export function sharedPersistenceTests(test) {
 			localId: "correction-sort-original",
 			senderId: chatId,
 			direction: 0,
-			timestamp: "2026-08-26T12:00:00Z",
+			timeReceived: "2026-08-26T12:00:00Z",
 		});
 		originalBuilder.sortId = "b0";
 		originalBuilder.text = "Original text";
@@ -543,7 +543,7 @@ export function sharedPersistenceTests(test) {
 			localId: "correction-sort-correction",
 			senderId: chatId,
 			direction: 0,
-			timestamp: "2026-08-26T12:01:00Z",
+			timeReceived: "2026-08-26T12:01:00Z",
 		});
 		correctionBuilder.sortId = "b0";
 		correctionBuilder.text = "Corrected text";
@@ -581,7 +581,7 @@ export function sharedPersistenceTests(test) {
 			localId: "correction-sort-original",
 			senderId: chatId,
 			direction: 0,
-			timestamp: "2026-08-26T12:00:00Z",
+			timeReceived: "2026-08-26T12:00:00Z",
 		});
 		originalBuilder.sortId = "b0";
 		originalBuilder.text = "Original text";
@@ -597,7 +597,7 @@ export function sharedPersistenceTests(test) {
 			localId: "correction-sort-correction",
 			senderId: chatId,
 			direction: 0,
-			timestamp: "2026-08-26T12:01:00Z",
+			timeReceived: "2026-08-26T12:01:00Z",
 		});
 		correctionBuilder.sortId = "b1";
 		correctionBuilder.text = "Corrected text";
@@ -752,7 +752,7 @@ export function sharedPersistenceTests(test) {
 			senderId: "teaparty@example.com/hatter",
 			direction: 0,
 			type: borogove.MessageType.MessageChannel,
-			timestamp: "2020-01-01T00:00:01Z",
+			timeReceived: "2020-01-01T00:00:01Z",
 		});
 		builder.sortId = "a0";
 		builder.to = borogove.JID.parse("alice@example.com");
@@ -765,7 +765,7 @@ export function sharedPersistenceTests(test) {
 			senderId: "teaparty@example.com/hatter",
 			direction: 0,
 			type: borogove.MessageType.MessageChannel,
-			timestamp: "2020-01-01T00:00:00Z",
+			timeReceived: "2020-01-01T00:00:00Z",
 		});
 		builder2.sortId = "b0";
 		builder2.to = borogove.JID.parse("alice@example.com");
@@ -778,7 +778,7 @@ export function sharedPersistenceTests(test) {
 			senderId: "teaparty@example.com/hatter",
 			direction: 0,
 			type: borogove.MessageType.MessageChannelPrivate,
-			timestamp: "2020-01-01T00:00:03Z",
+			timeReceived: "2020-01-01T00:00:03Z",
 		});
 		builder3.sortId = "a0";
 		builder3.to = borogove.JID.parse("alice@example.com");
@@ -810,7 +810,7 @@ export function sharedPersistenceTests(test) {
 			senderId: "teaparty@example.com/hatter",
 			direction: 0,
 			type: borogove.MessageType.MessageChannel,
-			timestamp: "2020-01-01T00:00:01Z",
+			timeReceived: "2020-01-01T00:00:01Z",
 		});
 		builder.sortId = "a0";
 		builder.to = borogove.JID.parse("alice@example.com");
@@ -823,7 +823,7 @@ export function sharedPersistenceTests(test) {
 			senderId: "teaparty@example.com/hatter",
 			direction: 0,
 			type: borogove.MessageType.MessageChannel,
-			timestamp: "2020-01-01T00:00:00Z",
+			timeReceived: "2020-01-01T00:00:00Z",
 		});
 		builder2.sortId = "b0";
 		builder2.to = borogove.JID.parse("alice@example.com");
@@ -836,7 +836,7 @@ export function sharedPersistenceTests(test) {
 			senderId: "teaparty@example.com/hatter",
 			direction: 0,
 			type: borogove.MessageType.MessageChannelPrivate,
-			timestamp: "2020-01-01T00:00:03Z",
+			timeReceived: "2020-01-01T00:00:03Z",
 		});
 		builder3.sortId = "Z~";
 		builder3.to = borogove.JID.parse("alice@example.com");
@@ -849,7 +849,7 @@ export function sharedPersistenceTests(test) {
 			senderId: "teaparty@example.com/hatter",
 			direction: 0,
 			type: borogove.MessageType.MessageChannel,
-			timestamp: "2020-01-01T00:00:04Z",
+			timeReceived: "2020-01-01T00:00:04Z",
 		});
 		builder4.sortId = "c0";
 		builder4.to = borogove.JID.parse("alice@example.com");
@@ -880,7 +880,7 @@ export function sharedPersistenceTests(test) {
 			senderId: "teaparty@example.com/hatter",
 			direction: 0,
 			type: borogove.MessageType.MessageChannel,
-			timestamp: "2020-01-01T00:00:00Z",
+			timeReceived: "2020-01-01T00:00:00Z",
 		});
 		builder.sortId = "a0";
 		builder.to = borogove.JID.parse("alice@example.com");
@@ -893,7 +893,7 @@ export function sharedPersistenceTests(test) {
 			senderId: "teaparty@example.com/hatter",
 			direction: 0,
 			type: borogove.MessageType.MessageChannel,
-			timestamp: "2020-01-01T00:00:01Z",
+			timeReceived: "2020-01-01T00:00:01Z",
 		});
 		builder2.sortId = "b0";
 		builder2.to = borogove.JID.parse("alice@example.com");
@@ -906,7 +906,7 @@ export function sharedPersistenceTests(test) {
 			senderId: "teaparty@example.com/hatter",
 			direction: 0,
 			type: borogove.MessageType.MessageChannelPrivate,
-			timestamp: "2020-01-01T00:00:03Z",
+			timeReceived: "2020-01-01T00:00:03Z",
 		});
 		builder3.sortId = "Z~";
 		builder3.to = borogove.JID.parse("alice@example.com");
@@ -919,7 +919,7 @@ export function sharedPersistenceTests(test) {
 			senderId: "teaparty@example.com/hatter",
 			direction: 0,
 			type: borogove.MessageType.MessageChannel,
-			timestamp: "2020-01-01T00:00:04Z",
+			timeReceived: "2020-01-01T00:00:04Z",
 		});
 		builder4.sortId = "c0";
 		builder4.to = borogove.JID.parse("alice@example.com");
@@ -952,7 +952,7 @@ export function sharedPersistenceTests(test) {
 			senderId: "teaparty@example.com/hatter",
 			direction: 0,
 			type: borogove.MessageType.MessageChannel,
-			timestamp: "2020-01-01T00:00:00Z",
+			timeReceived: "2020-01-01T00:00:00Z",
 		});
 		builder.sortId = "a0";
 		builder.to = borogove.JID.parse("alice@example.com");
@@ -965,7 +965,7 @@ export function sharedPersistenceTests(test) {
 			senderId: "teaparty@example.com/hatter",
 			direction: 0,
 			type: borogove.MessageType.MessageChannel,
-			timestamp: "2020-01-01T00:00:01Z",
+			timeReceived: "2020-01-01T00:00:01Z",
 		});
 		builder2.sortId = "b0";
 		builder2.to = borogove.JID.parse("alice@example.com");
@@ -978,7 +978,7 @@ export function sharedPersistenceTests(test) {
 			senderId: "teaparty@example.com/hatter",
 			direction: 0,
 			type: borogove.MessageType.MessageChannelPrivate,
-			timestamp: "2020-01-01T00:00:03Z",
+			timeReceived: "2020-01-01T00:00:03Z",
 		});
 		builder3.sortId = "a1";
 		builder3.to = borogove.JID.parse("alice@example.com");
@@ -1010,7 +1010,7 @@ export function sharedPersistenceTests(test) {
 			senderId: "teaparty@example.com/hatter",
 			direction: 0,
 			type: borogove.MessageType.MessageChannel,
-			timestamp: "2020-01-01T00:00:01Z",
+			timeReceived: "2020-01-01T00:00:01Z",
 		});
 		builder.sortId = "a0";
 		builder.to = borogove.JID.parse("alice@example.com");
@@ -1023,7 +1023,7 @@ export function sharedPersistenceTests(test) {
 			senderId: "teaparty@example.com/hatter",
 			direction: 0,
 			type: borogove.MessageType.MessageChannel,
-			timestamp: "2020-01-01T00:00:00Z",
+			timeReceived: "2020-01-01T00:00:00Z",
 		});
 		builder2.sortId = "b0";
 		builder2.to = borogove.JID.parse("alice@example.com");
@@ -1036,7 +1036,7 @@ export function sharedPersistenceTests(test) {
 			senderId: "teaparty@example.com/hatter",
 			direction: 0,
 			type: borogove.MessageType.MessageChannelPrivate,
-			timestamp: "2020-01-01T00:00:03Z",
+			timeReceived: "2020-01-01T00:00:03Z",
 		});
 		builder3.sortId = "Z~";
 		builder3.to = borogove.JID.parse("alice@example.com");
@@ -1049,7 +1049,7 @@ export function sharedPersistenceTests(test) {
 			senderId: "teaparty@example.com/hatter",
 			direction: 0,
 			type: borogove.MessageType.MessageChannel,
-			timestamp: "2020-01-01T00:00:04Z",
+			timeReceived: "2020-01-01T00:00:04Z",
 		});
 		builder4.sortId = "c0";
 		builder4.to = borogove.JID.parse("alice@example.com");
@@ -1080,7 +1080,7 @@ export function sharedPersistenceTests(test) {
 			senderId: "teaparty@example.com/hatter",
 			direction: 0,
 			type: borogove.MessageType.MessageChannel,
-			timestamp: "2020-01-01T00:00:00Z",
+			timeReceived: "2020-01-01T00:00:00Z",
 		});
 		builder.sortId = "a0";
 		builder.to = borogove.JID.parse("alice@example.com");
@@ -1093,7 +1093,7 @@ export function sharedPersistenceTests(test) {
 			senderId: "teaparty@example.com/hatter",
 			direction: 0,
 			type: borogove.MessageType.MessageChannel,
-			timestamp: "2020-01-01T00:00:01Z",
+			timeReceived: "2020-01-01T00:00:01Z",
 		});
 		builder2.sortId = "b0";
 		builder2.to = borogove.JID.parse("alice@example.com");
@@ -1106,7 +1106,7 @@ export function sharedPersistenceTests(test) {
 			senderId: "teaparty@example.com/hatter",
 			direction: 0,
 			type: borogove.MessageType.MessageChannelPrivate,
-			timestamp: "2020-01-01T00:00:03Z",
+			timeReceived: "2020-01-01T00:00:03Z",
 		});
 		builder3.sortId = "Z~";
 		builder3.to = borogove.JID.parse("alice@example.com");
@@ -1119,7 +1119,7 @@ export function sharedPersistenceTests(test) {
 			senderId: "teaparty@example.com/hatter",
 			direction: 0,
 			type: borogove.MessageType.MessageChannel,
-			timestamp: "2020-01-01T00:00:04Z",
+			timeReceived: "2020-01-01T00:00:04Z",
 		});
 		builder4.sortId = "c0";
 		builder4.to = borogove.JID.parse("alice@example.com");
@@ -2156,7 +2156,7 @@ export function sharedPersistenceTests(test) {
 				{ text: "First correction" },
 				{ text: "Second correction" },
 			])
-			.sort((a, b) => new Date(b.timestamp) - new Date(a.timestamp));
+			.sort((a, b) => new Date(b.timeReceived) - new Date(a.timeReceived));
 
 		const [stored, ...restStored] = await persistence.storeMessages(
 			account,