diff --git a/proposals/1228-removing-mxids-from-events.md b/proposals/1228-removing-mxids-from-events.md
new file mode 100644
index 00000000000..fd38022d1ff
--- /dev/null
+++ b/proposals/1228-removing-mxids-from-events.md
@@ -0,0 +1,327 @@
+# MSC1228: Removing MXIDs from events
+
+## Background
+
+We would like to be able to break the association between a user's ID (such as
+`@richvdh:sw1v.org`) and their activity in a room.
+
+The stretch goal is to also remove the association with server names, since for
+many users, they are the only user on a server and it is reasonable to be able
+to ask for the removal of any history of `sw1v.org`'s involvement with a room.
+
+The general idea presented here is to use a pseudomym in many places where we
+currently use user IDs. The current `@user:server` then becomes a user alias;
+the mapping between alias and the psuedonumous ID is public but can be removed
+in the future.
+
+User IDs currently appear in the following places in a room:
+
+ * `sender` of each event
+ * `state_key` of `m.room.member` events
+ * `users` list in `m.room.power_levels` events
+ * `creatorUserId` in the content of `m.widget`
+
+Server names appear in the following places:
+
+ * `origin` of each event
+ * keys in the `signatures` dict of each event
+ * Room IDs
+ * Room Aliases
+ * `state_keys` in `m.room.aliases` events
+ * `matrix.to` permalinks
+
+## Proposal
+
+[This is v3 of this proposal, which in summary is: do proposal v1, but also
+introduce a requirement for a global `user_key` at the same time, which in
+future will replace mxids as the user's One True Identity. v1 and v2 are
+available for reference at
+https://docs.google.com/document/d/1ni4LnC_vafX4h4K4sYNpmccS7QeHEFpAcYcbLS-J21Q#heading=h.y1krynr6itl4.]
+
+ * Each user (currently identified by an mxid) will also have a `user_key`. In
+ time, this will replace the mxid as your One True Identity; however for now
+ they will live in parallel.
+
+ * A `user_key` is represented like `~1:dV3hr3yE9SxhsWEGBJdTho777S8ompkJTh`,
+ where `1:` is a version (to allow other systems to be used in future) and
+ the rest is an (unpadded urlsafe-base64ed) ed25519 public key.
+
+ * Homeservers are responsible for making up keys for their users.
+
+ * For now, each homeserver maintains a one-to-one mapping between
+ `user_key` and mxid for each of their users. In future, we will look to
+ break this link to allow portability of accounts.
+
+ * Room IDs also become ed25519 public keys.
+
+ * They look like: `!Sr_Vj3FIqyQ2WjJ9fWpUXRdz6fX4oFAjKrDmu198PnI`.
+
+ * The server which creates the room is responsible for creating the keypair.
+
+ * The `m.room.create` event is signed with the room id to stop people making
+ new rooms which look like old ones. After this point, the private key is
+ never needed again. [1](#f1)
+
+ * Define a `user_room_key`, which is yet another ed25519 public key.
+
+ * It looks like `^Noi6WqcDj0QmPxCNQqgezwTlBKrfqehY1u2FyWP9uYw`.
+
+ * Homeservers are responsible for making up user keys for their users. They
+ can (and should) use a different key in each room for each user.
+
+ * This `user_room_key` is used where we currently use an mxid in the DAG:
+ `sender`, `m.room.member`, `m.room.power_levels`,
+ `creatorUserId`. `creator` is removed as per
+ [MSC2175](./2175-remove-creator-field.md).
+
+ * Events are **signed by the `user_room_key` of the sender instead of the
+ server's key**.
+
+ * If a user leaves and rejoins a room, they should use the same
+ `user_room_key` (unless a server admin has manually removed the old
+ mapping). This makes ban evasion harder. (It's up to server owners to
+ ensure this rule is followed - servers which don't respect it and allow a
+ serial abuser to evade bans by issuing different `user_room_keys` are likely
+ to suffer whole-server bans.)
+
+ * Invite and join events include:
+
+ * `mxid_mapping`: field which gives the user's `@user:server` mxid and which
+ must be signed by the server in question, and the signature must be
+ verified before the mapping is considered valid.
+
+ * `user_mapping`: contains the `user_key` giving your True Identity, and
+ signed by that key. The signature must be verified by receiving
+ homeservers for it to be considered a valid invite/join event for a vNext
+ room.
+
+### Examples
+
+`m.room.create`: signed by both the room key and the `user_room_key` of the
+sender:
+
+```json
+{
+ "type": "m.room.create",
+ "state_key": "",
+ "room_id": "!Sr_Vj3FIqyQ2WjJ9fWpUXRdz6fX4oFAjKrDmu198PnI",
+ "event_id": "$Riw5upWofaD4MNGM7bbZIj3bf+Th3fW/tklH4+6+VOg",
+ "sender": "^Noi6WqcDj0QmPxCNQqgezwTlBKrfqehY1u2FyWP9uYw",
+ "content": {},
+ "origin_server_ts": 1459891964497,
+ "prev_events": [],
+ "prev_state": [],
+ "auth_events": [],
+ "signatures": {
+ "!Sr_Vj3FIqyQ2WjJ9fWpUXRdz6fX4oFAjKrDmu198PnI": "<...>",
+ "^Noi6WqcDj0QmPxCNQqgezwTlBKrfqehY1u2FyWP9uYw": "<...>"
+ },
+ "hashes":{"sha256":"3ASU57dV3hr3yE9SxhsWEGBJdTho777S8ompkJTh/Uo"}
+}
+```
+
+`m.room.member`, showing `mxid_mapping` and `user_mapping`:
+
+```json
+{
+ "type": "m.room.member",
+ "state_key": "^Noi6WqcDj0QmPxCNQqgezwTlBKrfqehY1u2FyWP9uYw",
+ "room_id": "!Sr_Vj3FIqyQ2WjJ9fWpUXRdz6fX4oFAjKrDmu198PnI",
+ "event_id": "$k21EhS3j8lhwqTi5NMTUH04oFyvR/1ujBGSWbW27aDs",
+ "sender": "^Noi6WqcDj0QmPxCNQqgezwTlBKrfqehY1u2FyWP9uYw",
+ "content": {
+ "membership": "join",
+ "avatar_url": "...",
+ "displayname": "...",
+ "mxid_mapping": {
+ "user_room_key": "^Noi6WqcDj0QmPxCNQqgezwTlBKrfqehY1u2FyWP9uYw",
+ "user_id": "@richvdh:matrix.org",
+ "signatures": {
+ "matrix.org": { "ed25519:a_zrXW": "<...>" }
+ }
+ },
+ "user_mapping": {
+ "user_key": "~1:dV3hr3yE9SxhsWEGBJdTho777S8ompkJTh",
+ "user_room_key": "^Noi6WqcDj0QmPxCNQqgezwTlBKrfqehY1u2FyWP9uYw",
+ "signatures": {
+ "~1:dV3hr3yE9SxhsWEGBJdTho777S8ompkJTh": "<...>"
+ }
+ }
+ },
+ "origin_server_ts": 1489597048772,
+ "prev_events": [
+ "$Riw5upWofaD4MNGM7bbZIj3bf+Th3fW/tklH4+6+VOg"
+ ],
+ "prev_state": [],
+ "auth_events": [
+ "$Riw5upWofaD4MNGM7bbZIj3bf+Th3fW/tklH4+6+VOg"
+ ],
+ "signatures": {
+ "^Noi6WqcDj0QmPxCNQqgezwTlBKrfqehY1u2FyWP9uYw": "<...>"
+ },
+ "hashes":{"sha256":"KLx4Alfa0QzOihSmUMZ1WQj5QdWnbMHwmqKxmYO8hJE"}
+}
+```
+
+### Handling the mxid mapping
+
+When a server joins a room, it will be presented with a bunch of
+`m.room.member` events which may claim mappings onto mxids. There are three
+reasons we need to know whether the mapping is valid:
+
+ * For clients, to help track users between rooms and to correlate to presence
+ * To authorise other servers to do backfill requests, etc.
+ * For outgoing messages, knowing which servers to send to.
+
+None of these are _particularly_ urgent (they all degrade fairly gracefully in
+the case that a mapping is missed). A lot of the slowness in joining rooms
+currently comes from having to pull server keys so that event signatures can be
+verified during the join process, so it would be nice to be able to consider
+the join complete as soon as the signature on the event is verified, and verify
+the `mxid_mappings` in the background.
+
+#### Implementation
+
+As a homeserver, we make an attempt to verify the sender before sending events
+to our clients.
+
+ * When a new invite/join event turns up for a _room you are already in_ (either
+ via federation push, or because we pulled it via `/event/xxx` due to missing
+ `auth_events`/`prev_events`):
+
+ We make an attempt to verify its `mxid_mapping` before persisting it into
+ our db. If the sig is wrong, we reject the event at that point.
+
+ If we can't get the key (with a shortish timeout), we handle it as normal
+ (and schedule a retry for later).
+
+ Typically we only care about the most recent[2](#f2)
+ `mxid_mapping` for each `user_room_key`; when we see another we can cancel any
+ pending verification of any previous mapping[3](#f3). Any
+ previously-verified mapping should remain in place until another mapping
+ becomes available.
+
+ * When we _backfill_:
+
+ We do the same thing, although in many cases we'll already have active
+ mappings for the users in question, so we can ignore any received that
+ way. By only honouring the most recent mapping, we gain the correct
+ semantics for account portability: authorisation for backfill depends on the
+ current location of the user, rather than wherever they were in the past.
+
+ * When we join a _new room_:
+
+ For now we do the same thing (ie, make an attempt to verify the mxid
+ mappings in the join events we receive, and time them out quickly). In
+ future we might optimise this so the the mappings are verified lazily.
+
+This should mean that in the majority of cases, we'll have a verified mxid by
+the time we send the event to a client.
+
+For each `user_room_key`, we therefore have:
+
+ * zero or one verified mxid mappings.
+ * zero or one incomplete mxid mappings.
+
+We extend the CS API to include a `verified_sender_mxid` field on any events
+sent to a client where we know the user's current **verified** mxid.
+
+ * This is included in the interests of helping simple clients do the right
+ thing most of the time - but it is annoying and dangerous because it will
+ **sometimes** be missing. Still, we don't want to either (a) hold up all
+ traffic in the room while we wait for a verification which may never
+ succeed; (b) hold back some events while we do a verification for a sender;
+ (c) require that all clients always have to wait for an asynchronous
+ verification and match them up.
+
+We also add a **new** field to the `/sync` response which tells clients about
+mxid mappings as they are resolved.
+
+Question: should we remove unverified mxid mappings from join events before
+serving them to the clients, to stop client developers relying on it and
+breaking everything?
+
+### Sending invites
+
+We have a bootstrapping problem for invites in that, until a user joins a room,
+we don't know their `user_room_key`.
+
+Also: invite events are supposed to be signed by the invitee, so that other
+members of the room can be sure that they have actually received a copy of the
+event.
+
+The current invite dance is:
+
+ * inviting server builds a complete invite event, and signs it
+ * inviting server sends a copy to invited server, along with some (unsigned)
+ state about the room: name, avatar, inviting user's join event
+ * invited server checks that request came from server of inviting user
+ * invited server also signs the event, and tells the user about it
+ * inviting server adds the (double-signed) event to the DAG and sends it to
+ the rest of the federation (including the invited server, if it was already
+ in the room)
+
+We could change this to:
+
+ * inviting server builds a partial invite event
+ * inviting server PUTs to `/_matrix/federation/v3/invite//` on invited server
+ * invited server checks that request came from server of inviting user
+ * invited server adds:
+ * `user_room_key` in state_key
+ * mxid attestation
+ * signature
+ * invited server tells the user about it, and returns the completed event to inviting server
+ * inviting server adds its signature to the complete event
+ * inviting server adds the (double-signed) event to the DAG and sends it to
+ the rest of the federation (including the invited server, if it was already
+ in the room)
+
+## Problems
+
+How to handle the state keys in `room_aliases` events?
+
+How to handle server names in `matrix.to` permalinks?
+
+What if somebody does a join with an inappropriate avatar/displayname? If we
+redact their join, we'll redact their identity assertion too :/
+
+## Other things that might fall out nicely
+
+ * Fixes broken backfill due to changed signing keys (matrix-org/synapse#3121)
+ * Case sensitive mxid comparison problems?
+ * Opens a path to killing off perspectives in favour of just asking servers
+ what their keys are (via TLS, with trust coming from X.509 certificates).
+ * Helping with the domain reuse problem
+ * Nicer way of validating redactions by comparing the `user_room_key` of the
+ message and the redaction which addresses the suboptimal solution introduced
+ by [MSC1659](./1659-event-id-as-hashes.md)
+ * …
+
+## Stuff we might want to avoid designing out
+
+* Ability to migrate users between servers by changing their mapping assertions
+
+* Ability to support alternative identity mapping assertions rather than being
+ strictly mxid->user_key (e.g. 3pid->user-key too). This could help
+ decentralised identity mappings in general in future, and possibly unify 3pid
+ invites with normal invites? It could also support discovering servers by
+ key rather than DNS (e.g. via DHT), which would be useful for p2p in future.
+
+## Key management
+
+If a private user key gets lost, they can just start using a new one and
+announce a new mapping; however this may require an update to the
+`power_levels` to give rights to the new user.
+
+If a private user key is compromised, then again we start using a new one and
+announce a new mapping, so that new events from the old key wouldn't look like
+they came from that user. Again it may need `power_level`s updates to remove
+power from the old `user_key`, but I think that is fair: you give away the keys
+to your privileged account, you have to expect some cleanup. Ideally we would
+have a way of revoking the old key properly, but this can be deferred for now.
+
+[1] although we might think about letting its use confer some sort of founder semantics.[↩](#a1)
+
+[2] or more accurately, the one in the current room state.[↩](#a2)
+
+[3] if a user/server spams out mappings so quickly that none of them ever complete, that is their own loss.[↩](#a3)