diff --git a/set_version.sh b/set_version.sh
index 4881d5162260a3f7fc50ae7e1d33c618c30db0f0..3b31b5d75b30e9b633122618d1a58a741bdaf1d8 100755
--- a/set_version.sh
+++ b/set_version.sh
@@ -1,3 +1,3 @@
 #!/bin/bash
-export version=0.5.0-alpha.2
+export version=0.5.0-alpha.3
 echo "pub const VERSION: &str = \"${version}\";" > src/version.rs
diff --git a/src/database/entities/channel.rs b/src/database/entities/channel.rs
index 0ab74fc6bb23019769c7e10a904d61c9d9a79e2b..aefa613316a8d5e204a13d49ff2258eb89ae6baa 100644
--- a/src/database/entities/channel.rs
+++ b/src/database/entities/channel.rs
@@ -128,6 +128,20 @@ impl Channel {
                 operation: "delete_many",
                 with: "channel_invites",
             })?;
+        
+        // Delete any unreads.
+        get_collection("channel_unreads")
+            .delete_many(
+                doc! {
+                    "_id.channel": id
+                },
+                None,
+            )
+            .await
+            .map_err(|_| Error::DatabaseError {
+                operation: "delete_many",
+                with: "channel_unreads",
+            })?;
 
         // Check if there are any attachments we need to delete.
         let message_ids = messages
diff --git a/src/database/entities/server.rs b/src/database/entities/server.rs
index b30c5c4a7b0b64cf52ab49e878e857477c4fcb2a..5726d448b2c48ff859a5f27233567363a1d75a9c 100644
--- a/src/database/entities/server.rs
+++ b/src/database/entities/server.rs
@@ -180,6 +180,8 @@ impl Server {
                 })?;
         }
 
+        // ! FIXME: delete any unreads
+
         for with in &["server_members", "server_bans"] {
             get_collection(with)
                 .delete_many(
diff --git a/src/database/entities/sync.rs b/src/database/entities/sync.rs
index d905c690a7125e1c84155f99fc59759d1d8003e4..07fb161d51f868fb9cc170f820d602913de1f6cb 100644
--- a/src/database/entities/sync.rs
+++ b/src/database/entities/sync.rs
@@ -1,3 +1,19 @@
 use std::collections::HashMap;
+use serde::{Serialize, Deserialize};
 
 pub type UserSettings = HashMap<String, (i64, String)>;
+
+#[derive(Serialize, Deserialize, Debug, Clone)]
+pub struct ChannelCompositeKey {
+    pub channel: String,
+    pub user: String,
+}
+
+#[derive(Serialize, Deserialize, Debug, Clone)]
+pub struct ChannelUnread {
+    #[serde(rename = "_id")]
+    pub id: ChannelCompositeKey,
+
+    pub last_id: String,
+    pub mentions: Option<Vec<String>>,
+}
diff --git a/src/database/entities/user.rs b/src/database/entities/user.rs
index d205ece7b855e3950aad3365406e3bba057aeb6f..b94d7a52be990c454dc020e6fc1412a61fe2af2d 100644
--- a/src/database/entities/user.rs
+++ b/src/database/entities/user.rs
@@ -253,4 +253,23 @@ impl User {
             .flatten()
             .collect::<Vec<String>>())
     }
+
+    /// Utility function to fetch unread objects for user.
+    pub async fn fetch_unreads(&self) -> Result<Vec<Document>> {
+        Ok(get_collection("channel_unreads")
+            .find(
+                doc! {
+                    "_id.user": &self.id
+                },
+                None
+            )
+            .await
+            .map_err(|_| Error::DatabaseError {
+                operation: "find_one",
+                with: "user_settings",
+            })?
+            .filter_map(async move |s| s.ok())
+            .collect::<Vec<Document>>()
+            .await)
+    }
 }
diff --git a/src/database/migrations/init.rs b/src/database/migrations/init.rs
index dcb06308fdd23988967c766bb8b759b23ce34a62..1bb7277063d62aee17f7a2123674407465cd34bb 100644
--- a/src/database/migrations/init.rs
+++ b/src/database/migrations/init.rs
@@ -41,6 +41,10 @@ pub async fn create_database() {
         .await
         .expect("Failed to create channel_invites collection.");
 
+    db.create_collection("channel_unreads", None)
+        .await
+        .expect("Failed to create channel_unreads collection.");
+
     db.create_collection("migrations", None)
         .await
         .expect("Failed to create migrations collection.");
@@ -49,10 +53,6 @@ pub async fn create_database() {
         .await
         .expect("Failed to create attachments collection.");
 
-    db.create_collection("channel_unreads", None)
-        .await
-        .expect("Failed to create channel_unreads collection.");
-
     db.create_collection("user_settings", None)
         .await
         .expect("Failed to create user_settings collection.");
diff --git a/src/notifications/events.rs b/src/notifications/events.rs
index 6ed1aaa897b8f59fc9eeda0258845aa9a1cbfbb4..6218b92e24892b8a4a55c9b719d84ef5517ed705 100644
--- a/src/notifications/events.rs
+++ b/src/notifications/events.rs
@@ -63,7 +63,7 @@ pub enum ClientboundNotification {
     Ready {
         users: Vec<User>,
         servers: Vec<Server>,
-        channels: Vec<Channel>,
+        channels: Vec<Channel>
     },
 
     Message(Message),
@@ -103,6 +103,11 @@ pub enum ClientboundNotification {
         id: String,
         user: String,
     },
+    ChannelAck {
+        id: String,
+        user: String,
+        message_id: String
+    },
 
     ServerUpdate {
         id: String,
diff --git a/src/notifications/payload.rs b/src/notifications/payload.rs
index ac5c45395b27e9ddeafd2a1be9aa892eb5985a44..6a279f0f873017e9ad65ee1778e852e099072969 100644
--- a/src/notifications/payload.rs
+++ b/src/notifications/payload.rs
@@ -113,6 +113,6 @@ pub async fn generate_ready(mut user: User) -> Result<ClientboundNotification> {
     Ok(ClientboundNotification::Ready {
         users,
         servers,
-        channels,
+        channels
     })
 }
diff --git a/src/notifications/websocket.rs b/src/notifications/websocket.rs
index 6e275e1a97b20f459a6db5524710154fc266e85f..5046f777a1517dce077154b3966c1bbe14277033 100644
--- a/src/notifications/websocket.rs
+++ b/src/notifications/websocket.rs
@@ -244,7 +244,8 @@ pub fn publish(ids: Vec<String>, notification: ClientboundNotification) {
             // Block certain notifications from reaching users that aren't meant to see them.
             match &notification {
                 ClientboundNotification::UserRelationship { id: user_id, .. }
-                | ClientboundNotification::UserSettingsUpdate { id: user_id, .. } => {
+                | ClientboundNotification::UserSettingsUpdate { id: user_id, .. }
+                | ClientboundNotification::ChannelAck { user: user_id, .. } => {
                     if &id != user_id {
                         continue;
                     }
diff --git a/src/routes/channels/channel_ack.rs b/src/routes/channels/channel_ack.rs
new file mode 100644
index 0000000000000000000000000000000000000000..dfd45e457f34a90205d9d9a95478b3e35e648e0a
--- /dev/null
+++ b/src/routes/channels/channel_ack.rs
@@ -0,0 +1,53 @@
+use crate::notifications::events::ClientboundNotification;
+use crate::util::result::{Error, Result};
+use crate::database::*;
+
+use mongodb::bson::doc;
+use mongodb::options::UpdateOptions;
+
+#[put("/<target>/ack/<message>")]
+pub async fn req(user: User, target: Ref, message: Ref) -> Result<()> {
+    let target = target.fetch_channel().await?;
+
+    let perm = permissions::PermissionCalculator::new(&user)
+        .with_channel(&target)
+        .for_channel()
+        .await?;
+
+    if !perm.get_view() {
+        Err(Error::MissingPermission)?
+    }
+
+    let id = target.id();
+    get_collection("channel_unreads")
+        .update_one(
+            doc! {
+                "_id.channel": id,
+                "_id.user": &user.id
+            },
+            doc! {
+                "$unset": {
+                    "mentions": 1
+                },
+                "$set": {
+                    "last_id": &message.id
+                }
+            },
+            UpdateOptions::builder()
+                .upsert(true)
+                .build()
+        )
+        .await
+        .map_err(|_| Error::DatabaseError {
+            operation: "update_one",
+            with: "channel_unreads",
+        })?;
+    
+    ClientboundNotification::ChannelAck {
+        id: id.to_string(),
+        user: user.id.clone(),
+        message_id: message.id
+    }.publish(user.id);
+    
+    Ok(())
+}
diff --git a/src/routes/channels/mod.rs b/src/routes/channels/mod.rs
index cb7d0b4eea31e29bff3b67e403589808d3b27a44..ad2b70011b50d66d0d2c76dbf449c51a54e65427 100644
--- a/src/routes/channels/mod.rs
+++ b/src/routes/channels/mod.rs
@@ -1,5 +1,6 @@
 use rocket::Route;
 
+mod channel_ack;
 mod delete_channel;
 mod edit_channel;
 mod fetch_channel;
@@ -18,6 +19,7 @@ mod message_send;
 
 pub fn routes() -> Vec<Route> {
     routes![
+        channel_ack::req,
         fetch_channel::req,
         fetch_members::req,
         delete_channel::req,
diff --git a/src/routes/sync/get_unreads.rs b/src/routes/sync/get_unreads.rs
new file mode 100644
index 0000000000000000000000000000000000000000..4d59bebfbb403952d59ce4db2340d2c4df2e7254
--- /dev/null
+++ b/src/routes/sync/get_unreads.rs
@@ -0,0 +1,10 @@
+use crate::database::*;
+use crate::util::result::Result;
+
+use rocket_contrib::json::JsonValue;
+use mongodb::bson::doc;
+
+#[get("/unreads")]
+pub async fn req(user: User) -> Result<JsonValue> {
+    Ok(json!(user.fetch_unreads().await?))
+}
diff --git a/src/routes/sync/mod.rs b/src/routes/sync/mod.rs
index 872fef1d6fae2e3f1a0dd65ded923eda738ad179..3bfe36d93f7453037cf8e6cf23be9b3780da3dd2 100644
--- a/src/routes/sync/mod.rs
+++ b/src/routes/sync/mod.rs
@@ -2,7 +2,8 @@ use rocket::Route;
 
 mod get_settings;
 mod set_settings;
+mod get_unreads;
 
 pub fn routes() -> Vec<Route> {
-    routes![get_settings::req, set_settings::req]
+    routes![get_settings::req, set_settings::req, get_unreads::req]
 }
diff --git a/src/version.rs b/src/version.rs
index b8bf57cf8bcf060ad8f632a10031e3b9ec680b2b..133b97d198631cd5ac1261f15c4406a9507da78f 100644
--- a/src/version.rs
+++ b/src/version.rs
@@ -1 +1 @@
-pub const VERSION: &str = "0.5.0-alpha.1";
+pub const VERSION: &str = "0.5.0-alpha.3";