diff --git a/src/client_server/profile.rs b/src/client_server/profile.rs
index 1313db776118e7b9d8c6c7813d96f3b04b858af4..ebcc7eb8f32b80a0359b852364a283bee9ae75fd 100644
--- a/src/client_server/profile.rs
+++ b/src/client_server/profile.rs
@@ -217,11 +217,8 @@ pub fn get_profile_route(
     db: State<'_, Database>,
     body: Ruma<get_profile::Request>,
 ) -> ConduitResult<get_profile::Response> {
-    let avatar_url = db.users.avatar_url(&body.user_id)?;
-    let displayname = db.users.displayname(&body.user_id)?;
-
-    if avatar_url.is_none() && displayname.is_none() {
-        // Return 404 if we don't have a profile for this id
+    if !db.users.exists(&body.user_id)? {
+        // Return 404 if this user doesn't exist
         return Err(Error::BadRequest(
             ErrorKind::NotFound,
             "Profile was not found.",
@@ -229,8 +226,8 @@ pub fn get_profile_route(
     }
 
     Ok(get_profile::Response {
-        avatar_url,
-        displayname,
+        avatar_url: db.users.avatar_url(&body.user_id)?,
+        displayname: db.users.displayname(&body.user_id)?,
     }
     .into())
 }
diff --git a/src/client_server/room.rs b/src/client_server/room.rs
index b5f1529e592dd3d7f725c80e9167560bdd5a01b0..1b4387337a69399862c76bf1568175be39dfa6d3 100644
--- a/src/client_server/room.rs
+++ b/src/client_server/room.rs
@@ -3,15 +3,15 @@
 use ruma::{
     api::client::{
         error::ErrorKind,
-        r0::room::{self, create_room, get_room_event},
+        r0::room::{self, create_room, get_room_event, upgrade_room},
     },
     events::{
         room::{guest_access, history_visibility, join_rules, member, name, topic},
         EventType,
     },
-    RoomAliasId, RoomId, RoomVersionId,
+    Raw, RoomAliasId, RoomId, RoomVersionId,
 };
-use std::{collections::BTreeMap, convert::TryFrom};
+use std::{cmp::max, collections::BTreeMap, convert::TryFrom};
 
 #[cfg(feature = "conduit_bin")]
 use rocket::{get, post};
@@ -344,3 +344,196 @@ pub fn get_room_event_route(
     }
     .into())
 }
+
+#[cfg_attr(
+    feature = "conduit_bin",
+    post("/_matrix/client/r0/rooms/<_room_id>/upgrade", data = "<body>")
+)]
+pub fn upgrade_room_route(
+    db: State<'_, Database>,
+    body: Ruma<upgrade_room::Request>,
+    _room_id: String,
+) -> ConduitResult<upgrade_room::Response> {
+    let sender_id = body.sender_id.as_ref().expect("user is authenticated");
+
+    // Validate the room version requested
+    let new_version =
+        RoomVersionId::try_from(body.new_version.clone()).expect("invalid room version id");
+
+    if !matches!(
+        new_version,
+        RoomVersionId::Version5 | RoomVersionId::Version6
+    ) {
+        return Err(Error::BadRequest(
+            ErrorKind::UnsupportedRoomVersion,
+            "This server does not support that room version.",
+        ));
+    }
+
+    // Create a replacement room
+    let replacement_room = RoomId::new(db.globals.server_name());
+
+    // Send a m.room.tombstone event to the old room to indicate that it is not intended to be used any further
+    // Fail if the sender does not have the required permissions
+    let tombstone_event_id = db.rooms.append_pdu(
+        PduBuilder {
+            room_id: body.room_id.clone(),
+            sender: sender_id.clone(),
+            event_type: EventType::RoomTombstone,
+            content: serde_json::to_value(ruma::events::room::tombstone::TombstoneEventContent {
+                body: "This room has been replaced".to_string(),
+                replacement_room: replacement_room.clone(),
+            })
+            .expect("event is valid, we just created it"),
+            unsigned: None,
+            state_key: Some("".to_owned()),
+            redacts: None,
+        },
+        &db.globals,
+        &db.account_data,
+    )?;
+
+    // Get the old room federations status
+    let federate = serde_json::from_value::<Raw<ruma::events::room::create::CreateEventContent>>(
+        db.rooms
+            .room_state_get(&body.room_id, &EventType::RoomCreate, "")?
+            .ok_or_else(|| Error::bad_database("Found room without m.room.create event."))?
+            .content,
+    )
+    .expect("Raw::from_value always works")
+    .deserialize()
+    .map_err(|_| Error::bad_database("Invalid room event in database."))?
+    .federate;
+
+    // Use the m.room.tombstone event as the predecessor
+    let predecessor = Some(ruma::events::room::create::PreviousRoom::new(
+        body.room_id.clone(),
+        tombstone_event_id,
+    ));
+
+    // Send a m.room.create event containing a predecessor field and the applicable room_version
+    let mut create_event_content =
+        ruma::events::room::create::CreateEventContent::new(sender_id.clone());
+    create_event_content.federate = federate;
+    create_event_content.room_version = new_version;
+    create_event_content.predecessor = predecessor;
+
+    db.rooms.append_pdu(
+        PduBuilder {
+            room_id: replacement_room.clone(),
+            sender: sender_id.clone(),
+            event_type: EventType::RoomCreate,
+            content: serde_json::to_value(create_event_content)
+                .expect("event is valid, we just created it"),
+            unsigned: None,
+            state_key: Some("".to_owned()),
+            redacts: None,
+        },
+        &db.globals,
+        &db.account_data,
+    )?;
+
+    // Join the new room
+    db.rooms.append_pdu(
+        PduBuilder {
+            room_id: replacement_room.clone(),
+            sender: sender_id.clone(),
+            event_type: EventType::RoomMember,
+            content: serde_json::to_value(member::MemberEventContent {
+                membership: member::MembershipState::Join,
+                displayname: db.users.displayname(&sender_id)?,
+                avatar_url: db.users.avatar_url(&sender_id)?,
+                is_direct: None,
+                third_party_invite: None,
+            })
+            .expect("event is valid, we just created it"),
+            unsigned: None,
+            state_key: Some(sender_id.to_string()),
+            redacts: None,
+        },
+        &db.globals,
+        &db.account_data,
+    )?;
+
+    // Recommended transferable state events list from the specs
+    let transferable_state_events = vec![
+        EventType::RoomServerAcl,
+        EventType::RoomEncryption,
+        EventType::RoomName,
+        EventType::RoomAvatar,
+        EventType::RoomTopic,
+        EventType::RoomGuestAccess,
+        EventType::RoomHistoryVisibility,
+        EventType::RoomJoinRules,
+        EventType::RoomPowerLevels,
+    ];
+
+    // Replicate transferable state events to the new room
+    for event_type in transferable_state_events {
+        let event_content = match db.rooms.room_state_get(&body.room_id, &event_type, "")? {
+            Some(v) => v.content.clone(),
+            None => continue, // Skipping missing events.
+        };
+
+        db.rooms.append_pdu(
+            PduBuilder {
+                room_id: replacement_room.clone(),
+                sender: sender_id.clone(),
+                event_type,
+                content: event_content,
+                unsigned: None,
+                state_key: Some("".to_owned()),
+                redacts: None,
+            },
+            &db.globals,
+            &db.account_data,
+        )?;
+    }
+
+    // Moves any local aliases to the new room
+    for alias in db.rooms.room_aliases(&body.room_id).filter_map(|r| r.ok()) {
+        db.rooms
+            .set_alias(&alias, Some(&replacement_room), &db.globals)?;
+    }
+
+    // Get the old room power levels
+    let mut power_levels_event_content =
+        serde_json::from_value::<Raw<ruma::events::room::power_levels::PowerLevelsEventContent>>(
+            db.rooms
+                .room_state_get(&body.room_id, &EventType::RoomPowerLevels, "")?
+                .ok_or_else(|| Error::bad_database("Found room without m.room.create event."))?
+                .content,
+        )
+        .expect("database contains invalid PDU")
+        .deserialize()
+        .map_err(|_| Error::bad_database("Invalid room event in database."))?;
+
+    // Setting events_default and invite to the greater of 50 and users_default + 1
+    let new_level = max(
+        50.into(),
+        power_levels_event_content.users_default + 1.into(),
+    );
+    power_levels_event_content.events_default = new_level;
+    power_levels_event_content.invite = new_level;
+
+    // Modify the power levels in the old room to prevent sending of events and inviting new users
+    db.rooms
+        .append_pdu(
+            PduBuilder {
+                room_id: body.room_id.clone(),
+                sender: sender_id.clone(),
+                event_type: EventType::RoomPowerLevels,
+                content: serde_json::to_value(power_levels_event_content)
+                    .expect("event is valid, we just created it"),
+                unsigned: None,
+                state_key: Some("".to_owned()),
+                redacts: None,
+            },
+            &db.globals,
+            &db.account_data,
+        )
+        .ok();
+
+    // Return the replacement room id
+    Ok(upgrade_room::Response { replacement_room }.into())
+}
diff --git a/src/database.rs b/src/database.rs
index b43cc5b03bff77a6c0b9df84d39f897d197ebc24..2bb75a58ba72ddbd08bfd047d43c04807a3528fc 100644
--- a/src/database.rs
+++ b/src/database.rs
@@ -112,6 +112,7 @@ pub fn load_or_create(config: &Config) -> Result<Self> {
 
                 userroomid_joined: db.open_tree("userroomid_joined")?,
                 roomuserid_joined: db.open_tree("roomuserid_joined")?,
+                roomuseroncejoinedids: db.open_tree("roomuseroncejoinedids")?,
                 userroomid_invited: db.open_tree("userroomid_invited")?,
                 roomuserid_invited: db.open_tree("roomuserid_invited")?,
                 userroomid_left: db.open_tree("userroomid_left")?,
diff --git a/src/database/rooms.rs b/src/database/rooms.rs
index 8cfb61296f55c51d7180e56ee3039fc0eeacf854..22e61e6945bc35955f81263b41d71ae6e906157b 100644
--- a/src/database/rooms.rs
+++ b/src/database/rooms.rs
@@ -38,6 +38,7 @@ pub struct Rooms {
 
     pub(super) userroomid_joined: sled::Tree,
     pub(super) roomuserid_joined: sled::Tree,
+    pub(super) roomuseroncejoinedids: sled::Tree,
     pub(super) userroomid_invited: sled::Tree,
     pub(super) roomuserid_invited: sled::Tree,
     pub(super) userroomid_left: sled::Tree,
@@ -782,6 +783,91 @@ fn update_membership(
 
         match &membership {
             member::MembershipState::Join => {
+                // Check if the user never joined this room
+                if !self.once_joined(&user_id, &room_id)? {
+                    // Add the user ID to the join list then
+                    self.roomuseroncejoinedids.insert(&userroom_id, &[])?;
+
+                    // Check if the room has a predecessor
+                    if let Some(predecessor) = serde_json::from_value::<
+                        Raw<ruma::events::room::create::CreateEventContent>,
+                    >(
+                        self.room_state_get(&room_id, &EventType::RoomCreate, "")?
+                            .ok_or_else(|| {
+                                Error::bad_database("Found room without m.room.create event.")
+                            })?
+                            .content,
+                    )
+                    .expect("Raw::from_value always works")
+                    .deserialize()
+                    .map_err(|_| Error::bad_database("Invalid room event in database."))?
+                    .predecessor
+                    {
+                        // Copy user settings from predecessor to the current room:
+                        // - Push rules
+                        //
+                        // TODO: finish this once push rules are implemented.
+                        //
+                        // let mut push_rules_event_content = account_data
+                        //     .get::<ruma::events::push_rules::PushRulesEvent>(
+                        //         None,
+                        //         user_id,
+                        //         EventType::PushRules,
+                        //     )?;
+                        //
+                        // NOTE: find where `predecessor.room_id` match
+                        //       and update to `room_id`.
+                        //
+                        // account_data
+                        //     .update(
+                        //         None,
+                        //         user_id,
+                        //         EventType::PushRules,
+                        //         &push_rules_event_content,
+                        //         globals,
+                        //     )
+                        //     .ok();
+
+                        // Copy old tags to new room
+                        if let Some(tag_event) = account_data.get::<ruma::events::tag::TagEvent>(
+                            Some(&predecessor.room_id),
+                            user_id,
+                            EventType::Tag,
+                        )? {
+                            account_data
+                                .update(Some(room_id), user_id, EventType::Tag, &tag_event, globals)
+                                .ok();
+                        };
+
+                        // Copy direct chat flag
+                        if let Some(mut direct_event) = account_data
+                            .get::<ruma::events::direct::DirectEvent>(
+                            None,
+                            user_id,
+                            EventType::Direct,
+                        )? {
+                            let mut room_ids_updated = false;
+
+                            for room_ids in direct_event.content.0.values_mut() {
+                                if room_ids.iter().any(|r| r == &predecessor.room_id) {
+                                    room_ids.push(room_id.clone());
+                                    room_ids_updated = true;
+                                }
+                            }
+
+                            if room_ids_updated {
+                                account_data.update(
+                                    None,
+                                    user_id,
+                                    EventType::Direct,
+                                    &direct_event,
+                                    globals,
+                                )?;
+                            }
+                        };
+                    }
+                }
+
                 self.userroomid_joined.insert(&userroom_id, &[])?;
                 self.roomuserid_joined.insert(&roomuser_id, &[])?;
                 self.userroomid_invited.remove(&userroom_id)?;
@@ -1042,6 +1128,27 @@ pub fn room_members(&self, room_id: &RoomId) -> impl Iterator<Item = Result<User
             })
     }
 
+    /// Returns an iterator over all User IDs who ever joined a room.
+    pub fn room_useroncejoined(&self, room_id: &RoomId) -> impl Iterator<Item = Result<UserId>> {
+        self.roomuseroncejoinedids
+            .scan_prefix(room_id.to_string())
+            .keys()
+            .map(|key| {
+                Ok(UserId::try_from(
+                    utils::string_from_bytes(
+                        &key?
+                            .rsplit(|&b| b == 0xff)
+                            .next()
+                            .expect("rsplit always returns an element"),
+                    )
+                    .map_err(|_| {
+                        Error::bad_database("User ID in room_useroncejoined is invalid unicode.")
+                    })?,
+                )
+                .map_err(|_| Error::bad_database("User ID in room_useroncejoined is invalid."))?)
+            })
+    }
+
     /// Returns an iterator over all invited members of a room.
     pub fn room_members_invited(&self, room_id: &RoomId) -> impl Iterator<Item = Result<UserId>> {
         self.roomuserid_invited
@@ -1126,6 +1233,14 @@ pub fn rooms_left(&self, user_id: &UserId) -> impl Iterator<Item = Result<RoomId
             })
     }
 
+    pub fn once_joined(&self, user_id: &UserId, room_id: &RoomId) -> Result<bool> {
+        let mut userroom_id = user_id.to_string().as_bytes().to_vec();
+        userroom_id.push(0xff);
+        userroom_id.extend_from_slice(room_id.to_string().as_bytes());
+
+        Ok(self.roomuseroncejoinedids.get(userroom_id)?.is_some())
+    }
+
     pub fn is_joined(&self, user_id: &UserId, room_id: &RoomId) -> Result<bool> {
         let mut userroom_id = user_id.to_string().as_bytes().to_vec();
         userroom_id.push(0xff);
diff --git a/src/main.rs b/src/main.rs
index 96d0e99a4d5b3c99b493d5d0f102af594081d9be..eb060e3eca8f09b961a0bfae3baa11375e15da57 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -118,6 +118,7 @@ fn setup_rocket() -> rocket::Rocket {
                 client_server::get_key_changes_route,
                 client_server::get_pushers_route,
                 client_server::set_pushers_route,
+                client_server::upgrade_room_route,
                 server_server::well_known_server,
                 server_server::get_server_version,
                 server_server::get_server_keys,
diff --git a/sytest/sytest-whitelist b/sytest/sytest-whitelist
index 158523307bcba5bfdf28fd8e3b25dbb35779549b..e1f4e5cd86494653e7608e64d441e07d25baa177 100644
--- a/sytest/sytest-whitelist
+++ b/sytest/sytest-whitelist
@@ -89,6 +89,7 @@ POST /rooms/:room_id/join can join a room
 POST /rooms/:room_id/leave can leave a room
 POST /rooms/:room_id/state/m.room.name sets name
 POST /rooms/:room_id/state/m.room.topic sets topic
+POST /rooms/:room_id/upgrade can upgrade a room version
 POSTed media can be thumbnailed
 PUT /device/{deviceId} gives a 404 for unknown devices
 PUT /device/{deviceId} updates device fields