diff --git a/fabric-message-api-v1/build.gradle b/fabric-message-api-v1/build.gradle
new file mode 100644
index 000000000..ebfd97c13
--- /dev/null
+++ b/fabric-message-api-v1/build.gradle
@@ -0,0 +1,6 @@
+archivesBaseName = "fabric-message-api-v1"
+version = getSubprojectVersion(project)
+
+moduleDependencies(project, [
+		'fabric-api-base'
+])
diff --git a/fabric-message-api-v1/src/main/java/net/fabricmc/fabric/api/message/v1/ServerMessageDecoratorEvent.java b/fabric-message-api-v1/src/main/java/net/fabricmc/fabric/api/message/v1/ServerMessageDecoratorEvent.java
new file mode 100644
index 000000000..baebadc7c
--- /dev/null
+++ b/fabric-message-api-v1/src/main/java/net/fabricmc/fabric/api/message/v1/ServerMessageDecoratorEvent.java
@@ -0,0 +1,110 @@
+/*
+ * Copyright (c) 2016, 2017, 2018, 2019 FabricMC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.fabricmc.fabric.api.message.v1;
+
+import java.util.Objects;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.CompletionException;
+
+import org.jetbrains.annotations.Nullable;
+
+import net.minecraft.network.message.MessageDecorator;
+import net.minecraft.text.Text;
+import net.minecraft.util.Identifier;
+
+import net.fabricmc.fabric.api.event.Event;
+import net.fabricmc.fabric.api.event.EventFactory;
+
+/**
+ * A class for registering a {@link MessageDecorator}. Check the message decorator documentation
+ * for how message decorators work. Unlike other events, this uses a functional interface that is
+ * provided by the vanilla game.
+ *
+ * <p>This event uses phases to provide better mod compatibilities between mods that add custom
+ * content and styling. Message decorators with the styling phase will always apply after the ones
+ * with the content phase. When registering the message decorator, it is recommended to choose one
+ * of the phases from this interface and pass that to the {@link Event#register(Identifier, Object)}
+ * function. If not given, the message decorator will run in the default phase, which is between
+ * the content phase and the styling phase.
+ *
+ * <p>When implementing a message decorator, it is <strong>very important that the decorator be
+ * pure; i.e. return the same text when called multiple times for the same arguments (message and
+ * sender)</strong> - otherwise the server detects a mismatch between the preview and the actual message,
+ * and discards the message because it was improperly signed.
+ *
+ * <p>Example of registering a content phase message decorator:
+ *
+ * <pre><code>
+ * ServerMessageDecoratorEvent.EVENT.register(ServerMessageDecoratorEvent.CONTENT_PHASE, (sender, message) -> {
+ *     // Add smiley face. Has to copy() to get a MutableText with siblings and styles.
+ *     return message.copy().append(" :)");
+ * });
+ * </code></pre>
+ *
+ * <p>Example of registering a styling phase message decorator:
+ *
+ * <pre><code>
+ * ServerMessageDecoratorEvent.EVENT.register(ServerMessageDecoratorEvent.STYLING_PHASE, (sender, message) -> {
+ *     // Apply orange color to messages sent by server operators
+ *     if (sender != null && sender.server.getPlayerManager().isOperator(sender.getGameProfile())) {
+ *         return CompletableFuture.completedFuture(
+ *             message.copy().styled(style -> style.withColor(0xFFA500)));
+ *     }
+ *     return CompletableFuture.completedFuture(message);
+ * });
+ * </code></pre>
+ */
+public final class ServerMessageDecoratorEvent {
+	private ServerMessageDecoratorEvent() {
+	}
+
+	/**
+	 * The content phase of the event, passed when registering a message decorator. Use this when
+	 * the decorator modifies the text content of the message.
+	 */
+	public static final Identifier CONTENT_PHASE = new Identifier("fabric", "content");
+	/**
+	 * The styling phase of the event, passed when registering a message decorator. Use this when
+	 * the decorator only modifies the styling of the message with the text intact.
+	 */
+	public static final Identifier STYLING_PHASE = new Identifier("fabric", "styling");
+
+	public static final Event<MessageDecorator> EVENT = EventFactory.createWithPhases(MessageDecorator.class, decorators -> (sender, message) -> {
+		CompletableFuture<Text> future = null;
+
+		for (MessageDecorator decorator : decorators) {
+			if (future == null) {
+				future = decorator.decorate(sender, message).handle((decorated, throwable) -> handle(decorated, throwable, decorator));
+			} else {
+				future = future.thenCompose((decorated) -> decorator.decorate(sender, decorated).handle((newlyDecorated, throwable) -> handle(newlyDecorated, throwable, decorator)));
+			}
+		}
+
+		return future == null ? CompletableFuture.completedFuture(message) : future;
+	}, CONTENT_PHASE, Event.DEFAULT_PHASE, STYLING_PHASE);
+
+	private static <T extends Text> T handle(T decorated, @Nullable Throwable throwable, MessageDecorator decorator) {
+		String decoratorName = decorator.getClass().getName();
+
+		if (throwable != null) {
+			if (throwable instanceof CompletionException) throwable = throwable.getCause();
+			throw new CompletionException("message decorator %s failed".formatted(decoratorName), throwable);
+		}
+
+		return Objects.requireNonNull(decorated, "message decorator %s returned null".formatted(decoratorName));
+	}
+}
diff --git a/fabric-message-api-v1/src/main/java/net/fabricmc/fabric/api/message/v1/ServerMessageEvents.java b/fabric-message-api-v1/src/main/java/net/fabricmc/fabric/api/message/v1/ServerMessageEvents.java
new file mode 100644
index 000000000..95a6183a2
--- /dev/null
+++ b/fabric-message-api-v1/src/main/java/net/fabricmc/fabric/api/message/v1/ServerMessageEvents.java
@@ -0,0 +1,235 @@
+/*
+ * Copyright (c) 2016, 2017, 2018, 2019 FabricMC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.fabricmc.fabric.api.message.v1;
+
+import net.minecraft.network.message.MessageType;
+import net.minecraft.network.message.SignedMessage;
+import net.minecraft.server.command.ServerCommandSource;
+import net.minecraft.server.filter.FilteredMessage;
+import net.minecraft.server.network.ServerPlayerEntity;
+import net.minecraft.text.Text;
+import net.minecraft.util.registry.RegistryKey;
+
+import net.fabricmc.fabric.api.event.Event;
+import net.fabricmc.fabric.api.event.EventFactory;
+
+public final class ServerMessageEvents {
+	/**
+	 * An event triggered when the server broadcasts a chat message sent by a player,
+	 * typically from a client GUI or a player-executed command. Mods can use this to block
+	 * the message.
+	 *
+	 * <p>If a listener returned {@code false}, the message will not be broadcast,
+	 * the remaining listeners will not be called (if any), and {@link #CHAT_MESSAGE}
+	 * event will not be triggered.
+	 *
+	 * <p>If the message is from a player-executed command, this will be called
+	 * only if {@link #ALLOW_COMMAND_MESSAGE} event did not block the message,
+	 * and after triggering {@link #COMMAND_MESSAGE} event.
+	 */
+	public static final Event<AllowChatMessage> ALLOW_CHAT_MESSAGE = EventFactory.createArrayBacked(AllowChatMessage.class, handlers -> (message, sender, typeKey) -> {
+		for (AllowChatMessage handler : handlers) {
+			if (!handler.allowChatMessage(message, sender, typeKey)) return false;
+		}
+
+		return true;
+	});
+
+	/**
+	 * An event triggered when the server broadcasts a game message to all players. Game
+	 * messages include death messages, join/leave messages, and advancement messages.
+	 * Mods can use this to block the message.
+	 *
+	 * <p>If a listener returned {@code false}, the message will not be broadcast,
+	 * the remaining listeners will not be called (if any), and {@link #GAME_MESSAGE}
+	 * event will not be triggered.
+	 */
+	public static final Event<AllowGameMessage> ALLOW_GAME_MESSAGE = EventFactory.createArrayBacked(AllowGameMessage.class, handlers -> (message, typeKey) -> {
+		for (AllowGameMessage handler : handlers) {
+			if (!handler.allowGameMessage(message, typeKey)) return false;
+		}
+
+		return true;
+	});
+
+	/**
+	 * An event triggered when the server broadcasts a command message to all players, such as one
+	 * from {@code /me}, {@code /msg}, {@code /say}, and {@code /tellraw}. Mods can use this
+	 * to block the message.
+	 *
+	 * <p>If a listener returned {@code false}, the message will not be broadcast,
+	 * the remaining listeners will not be called (if any), and {@link #COMMAND_MESSAGE}
+	 * event will not be triggered.
+	 *
+	 * <p>If the command is executed by a player and the message is not blocked,
+	 * {@link #ALLOW_CHAT_MESSAGE} and {@link #CHAT_MESSAGE} events will also be
+	 * triggered after triggering {@link #COMMAND_MESSAGE}.
+	 */
+	public static final Event<AllowCommandMessage> ALLOW_COMMAND_MESSAGE = EventFactory.createArrayBacked(AllowCommandMessage.class, handlers -> (message, source, typeKey) -> {
+		for (AllowCommandMessage handler : handlers) {
+			if (!handler.allowCommandMessage(message, source, typeKey)) return false;
+		}
+
+		return true;
+	});
+
+	/**
+	 * An event triggered when the server broadcasts a chat message sent by a player, typically
+	 * from a client GUI or a player-executed command. Is not called when {@linkplain
+	 * #ALLOW_CHAT_MESSAGE chat messages are blocked}.
+	 *
+	 * <p>If the message is from a player-executed command, this will be called
+	 * only if {@link #ALLOW_COMMAND_MESSAGE} event did not block the message,
+	 * and after triggering {@link #COMMAND_MESSAGE} event.
+	 */
+	public static final Event<ChatMessage> CHAT_MESSAGE = EventFactory.createArrayBacked(ChatMessage.class, handlers -> (message, sender, typeKey) -> {
+		for (ChatMessage handler : handlers) {
+			handler.onChatMessage(message, sender, typeKey);
+		}
+	});
+
+	/**
+	 * An event triggered when the server broadcasts a game message to all players. Game messages
+	 * include death messages, join/leave messages, and advancement messages. Is not called
+	 * when {@linkplain #ALLOW_GAME_MESSAGE game messages are blocked}.
+	 */
+	public static final Event<GameMessage> GAME_MESSAGE = EventFactory.createArrayBacked(GameMessage.class, handlers -> (message, typeKey) -> {
+		for (GameMessage handler : handlers) {
+			handler.onGameMessage(message, typeKey);
+		}
+	});
+
+	/**
+	 * An event triggered when the server broadcasts a command message to all players, such as one
+	 * from {@code /me}, {@code /msg}, {@code /say}, and {@code /tellraw}. Is not called
+	 * when {@linkplain #ALLOW_COMMAND_MESSAGE command messages are blocked}.
+	 *
+	 * <p>If the command is executed by a player, {@link #ALLOW_CHAT_MESSAGE} and
+	 * {@link #CHAT_MESSAGE} events will also be triggered after this event.
+	 */
+	public static final Event<CommandMessage> COMMAND_MESSAGE = EventFactory.createArrayBacked(CommandMessage.class, handlers -> (message, source, typeKey) -> {
+		for (CommandMessage handler : handlers) {
+			handler.onCommandMessage(message, source, typeKey);
+		}
+	});
+
+	private ServerMessageEvents() {
+	}
+
+	@FunctionalInterface
+	public interface AllowChatMessage {
+		/**
+		 * Called when the server broadcasts a chat message sent by a player, typically
+		 * from a client GUI or a player-executed command. Returning {@code false}
+		 * prevents the message from being broadcast and the {@link #CHAT_MESSAGE} event
+		 * from triggering.
+		 *
+		 * <p>If the message is from a player-executed command, this will be called
+		 * only if {@link #ALLOW_COMMAND_MESSAGE} event did not block the message,
+		 * and after triggering {@link #COMMAND_MESSAGE} event.
+		 *
+		 * @param message the broadcast message with message decorators applied; use {@code message.raw().getContent()} to get the text
+		 * @param sender  the player that sent the message
+		 * @param typeKey the message type
+		 * @return {@code true} if the message should be broadcast, otherwise {@code false}
+		 */
+		boolean allowChatMessage(FilteredMessage<SignedMessage> message, ServerPlayerEntity sender, RegistryKey<MessageType> typeKey);
+	}
+
+	@FunctionalInterface
+	public interface AllowGameMessage {
+		/**
+		 * Called when the server broadcasts a game message to all players. Game messages
+		 * include death messages, join/leave messages, and advancement messages. Returning {@code false}
+		 * prevents the message from being broadcast and the {@link #GAME_MESSAGE} event
+		 * from triggering.
+		 *
+		 * @param message the broadcast message; use {@code message.raw().getContent()} to get the text
+		 * @param typeKey the message type
+		 * @return {@code true} if the message should be broadcast, otherwise {@code false}
+		 */
+		boolean allowGameMessage(Text message, RegistryKey<MessageType> typeKey);
+	}
+
+	@FunctionalInterface
+	public interface AllowCommandMessage {
+		/**
+		 * Called when the server broadcasts a command message to all players, such as one
+		 * from {@code /me}, {@code /msg}, {@code /say}, and {@code /tellraw}. Returning {@code false}
+		 * prevents the message from being broadcast and the {@link #COMMAND_MESSAGE} event
+		 * from triggering.
+		 *
+		 * <p>If the command is executed by a player and the message is not blocked,
+		 * {@link #ALLOW_CHAT_MESSAGE} and {@link #CHAT_MESSAGE} events will also be
+		 * triggered after triggering {@link #COMMAND_MESSAGE}.
+		 *
+		 * @param message the broadcast message with message decorators applied if applicable; use {@code message.raw().getContent()} to get the text
+		 * @param source  the command source that sent the message
+		 * @param typeKey the message type
+		 * @return {@code true} if the message should be broadcast, otherwise {@code false}
+		 */
+		boolean allowCommandMessage(FilteredMessage<SignedMessage> message, ServerCommandSource source, RegistryKey<MessageType> typeKey);
+	}
+
+	@FunctionalInterface
+	public interface ChatMessage {
+		/**
+		 * Called when the server broadcasts a chat message sent by a player, typically
+		 * from a client GUI or a player-executed command. Is not called when {@linkplain
+		 * #ALLOW_CHAT_MESSAGE chat messages are blocked}.
+		 *
+		 * <p>If the message is from a player-executed command, this will be called
+		 * only if {@link #ALLOW_COMMAND_MESSAGE} event did not block the message,
+		 * and after triggering {@link #COMMAND_MESSAGE} event.
+		 *
+		 * @param message the broadcast message with message decorators applied; use {@code message.raw().getContent()} to get the text
+		 * @param sender  the player that sent the message
+		 * @param typeKey the message type
+		 */
+		void onChatMessage(FilteredMessage<SignedMessage> message, ServerPlayerEntity sender, RegistryKey<MessageType> typeKey);
+	}
+
+	@FunctionalInterface
+	public interface GameMessage {
+		/**
+		 * Called when the server broadcasts a game message to all players. Game messages
+		 * include death messages, join/leave messages, and advancement messages. Is not called
+		 * when {@linkplain #ALLOW_GAME_MESSAGE game messages are blocked}.
+		 *
+		 * @param message the broadcast message; use {@code message.raw().getContent()} to get the text
+		 * @param typeKey the message type
+		 */
+		void onGameMessage(Text message, RegistryKey<MessageType> typeKey);
+	}
+
+	@FunctionalInterface
+	public interface CommandMessage {
+		/**
+		 * Called when the server broadcasts a command message to all players, such as one
+		 * from {@code /me}, {@code /msg}, {@code /say}, and {@code /tellraw}. Is not called
+		 * when {@linkplain #ALLOW_COMMAND_MESSAGE command messages are blocked}.
+		 *
+		 * <p>If the command is executed by a player, {@link #ALLOW_CHAT_MESSAGE} and
+		 * {@link #CHAT_MESSAGE} events will also be triggered after this event.
+		 *
+		 * @param message the broadcast message with message decorators applied if applicable; use {@code message.raw().getContent()} to get the text
+		 * @param source  the command source that sent the message
+		 * @param typeKey the message type
+		 */
+		void onCommandMessage(FilteredMessage<SignedMessage> message, ServerCommandSource source, RegistryKey<MessageType> typeKey);
+	}
+}
diff --git a/fabric-message-api-v1/src/main/java/net/fabricmc/fabric/mixin/message/MinecraftServerMixin.java b/fabric-message-api-v1/src/main/java/net/fabricmc/fabric/mixin/message/MinecraftServerMixin.java
new file mode 100644
index 000000000..9a7b415b3
--- /dev/null
+++ b/fabric-message-api-v1/src/main/java/net/fabricmc/fabric/mixin/message/MinecraftServerMixin.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright (c) 2016, 2017, 2018, 2019 FabricMC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.fabricmc.fabric.mixin.message;
+
+import org.spongepowered.asm.mixin.Mixin;
+import org.spongepowered.asm.mixin.injection.At;
+import org.spongepowered.asm.mixin.injection.Inject;
+import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
+
+import net.minecraft.network.message.MessageDecorator;
+import net.minecraft.server.MinecraftServer;
+
+import net.fabricmc.fabric.api.message.v1.ServerMessageDecoratorEvent;
+
+@Mixin(MinecraftServer.class)
+public class MinecraftServerMixin {
+	@Inject(method = "getMessageDecorator", at = @At("RETURN"), cancellable = true)
+	private void onGetChatDecorator(CallbackInfoReturnable<MessageDecorator> cir) {
+		MessageDecorator originalDecorator = cir.getReturnValue();
+		cir.setReturnValue((sender, message) -> originalDecorator.decorate(sender, message).thenCompose((decorated) -> ServerMessageDecoratorEvent.EVENT.invoker().decorate(sender, decorated)));
+	}
+}
diff --git a/fabric-message-api-v1/src/main/java/net/fabricmc/fabric/mixin/message/PlayerManagerMixin.java b/fabric-message-api-v1/src/main/java/net/fabricmc/fabric/mixin/message/PlayerManagerMixin.java
new file mode 100644
index 000000000..1ea8d2329
--- /dev/null
+++ b/fabric-message-api-v1/src/main/java/net/fabricmc/fabric/mixin/message/PlayerManagerMixin.java
@@ -0,0 +1,68 @@
+/*
+ * Copyright (c) 2016, 2017, 2018, 2019 FabricMC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.fabricmc.fabric.mixin.message;
+
+import java.util.function.Function;
+
+import org.spongepowered.asm.mixin.Mixin;
+import org.spongepowered.asm.mixin.injection.At;
+import org.spongepowered.asm.mixin.injection.Inject;
+import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
+
+import net.minecraft.network.message.MessageType;
+import net.minecraft.network.message.SignedMessage;
+import net.minecraft.server.PlayerManager;
+import net.minecraft.server.command.ServerCommandSource;
+import net.minecraft.server.filter.FilteredMessage;
+import net.minecraft.server.network.ServerPlayerEntity;
+import net.minecraft.text.Text;
+import net.minecraft.util.registry.RegistryKey;
+
+import net.fabricmc.fabric.api.message.v1.ServerMessageEvents;
+
+@Mixin(PlayerManager.class)
+public class PlayerManagerMixin {
+	@Inject(method = "broadcast(Lnet/minecraft/server/filter/FilteredMessage;Lnet/minecraft/server/network/ServerPlayerEntity;Lnet/minecraft/util/registry/RegistryKey;)V", at = @At("HEAD"), cancellable = true)
+	private void onSendChatMessage(FilteredMessage<SignedMessage> message, ServerPlayerEntity sender, RegistryKey<MessageType> typeKey, CallbackInfo ci) {
+		if (!ServerMessageEvents.ALLOW_CHAT_MESSAGE.invoker().allowChatMessage(message, sender, typeKey)) {
+			ci.cancel();
+			return;
+		}
+
+		ServerMessageEvents.CHAT_MESSAGE.invoker().onChatMessage(message, sender, typeKey);
+	}
+
+	@Inject(method = "broadcast(Lnet/minecraft/text/Text;Ljava/util/function/Function;Lnet/minecraft/util/registry/RegistryKey;)V", at = @At("HEAD"), cancellable = true)
+	private void onSendGameMessage(Text message, Function<ServerPlayerEntity, Text> playerMessageFactory, RegistryKey<MessageType> typeKey, CallbackInfo ci) {
+		if (!ServerMessageEvents.ALLOW_GAME_MESSAGE.invoker().allowGameMessage(message, typeKey)) {
+			ci.cancel();
+			return;
+		}
+
+		ServerMessageEvents.GAME_MESSAGE.invoker().onGameMessage(message, typeKey);
+	}
+
+	@Inject(method = "broadcast(Lnet/minecraft/server/filter/FilteredMessage;Lnet/minecraft/server/command/ServerCommandSource;Lnet/minecraft/util/registry/RegistryKey;)V", at = @At("HEAD"), cancellable = true)
+	private void onSendCommandMessage(FilteredMessage<SignedMessage> message, ServerCommandSource source, RegistryKey<MessageType> typeKey, CallbackInfo ci) {
+		if (!ServerMessageEvents.ALLOW_COMMAND_MESSAGE.invoker().allowCommandMessage(message, source, typeKey)) {
+			ci.cancel();
+			return;
+		}
+
+		ServerMessageEvents.COMMAND_MESSAGE.invoker().onCommandMessage(message, source, typeKey);
+	}
+}
diff --git a/fabric-message-api-v1/src/main/resources/assets/fabric-message-api-v1/icon.png b/fabric-message-api-v1/src/main/resources/assets/fabric-message-api-v1/icon.png
new file mode 100644
index 000000000..2931efbf6
Binary files /dev/null and b/fabric-message-api-v1/src/main/resources/assets/fabric-message-api-v1/icon.png differ
diff --git a/fabric-message-api-v1/src/main/resources/fabric-message-api-v1.mixins.json b/fabric-message-api-v1/src/main/resources/fabric-message-api-v1.mixins.json
new file mode 100644
index 000000000..de1a852c8
--- /dev/null
+++ b/fabric-message-api-v1/src/main/resources/fabric-message-api-v1.mixins.json
@@ -0,0 +1,14 @@
+{
+  "required": true,
+  "package": "net.fabricmc.fabric.mixin.message",
+  "compatibilityLevel": "JAVA_16",
+  "mixins": [
+    "MinecraftServerMixin",
+    "PlayerManagerMixin"
+  ],
+  "client": [
+  ],
+  "injectors": {
+    "defaultRequire": 1
+  }
+}
diff --git a/fabric-message-api-v1/src/main/resources/fabric.mod.json b/fabric-message-api-v1/src/main/resources/fabric.mod.json
new file mode 100644
index 000000000..2ac9e4d52
--- /dev/null
+++ b/fabric-message-api-v1/src/main/resources/fabric.mod.json
@@ -0,0 +1,29 @@
+{
+  "schemaVersion": 1,
+  "id": "fabric-message-api-v1",
+  "name": "Fabric Message API (v1)",
+  "version": "${version}",
+  "environment": "*",
+  "license": "Apache-2.0",
+  "icon": "assets/fabric-message-api-v1/icon.png",
+  "contact": {
+    "homepage": "https://fabricmc.net",
+    "irc": "irc://irc.esper.net:6667/fabric",
+    "issues": "https://github.com/FabricMC/fabric/issues",
+    "sources": "https://github.com/FabricMC/fabric"
+  },
+  "authors": [
+    "FabricMC"
+  ],
+  "depends": {
+    "fabricloader": ">=0.10.5",
+    "fabric-api-base": "*"
+  },
+  "description": "Adds message-related hooks.",
+  "mixins": [
+    "fabric-message-api-v1.mixins.json"
+  ],
+  "custom": {
+    "fabric-api:module-lifecycle": "experimental"
+  }
+}
diff --git a/fabric-message-api-v1/src/testmod/java/net/fabricmc/fabric/test/message/ChatTest.java b/fabric-message-api-v1/src/testmod/java/net/fabricmc/fabric/test/message/ChatTest.java
new file mode 100644
index 000000000..7e0946ee2
--- /dev/null
+++ b/fabric-message-api-v1/src/testmod/java/net/fabricmc/fabric/test/message/ChatTest.java
@@ -0,0 +1,101 @@
+/*
+ * Copyright (c) 2016, 2017, 2018, 2019 FabricMC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.fabricmc.fabric.test.message;
+
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.Executor;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import net.minecraft.text.TranslatableTextContent;
+import net.minecraft.util.Util;
+import net.minecraft.util.math.random.Random;
+
+import net.fabricmc.api.ModInitializer;
+import net.fabricmc.fabric.api.message.v1.ServerMessageDecoratorEvent;
+import net.fabricmc.fabric.api.message.v1.ServerMessageEvents;
+
+public class ChatTest implements ModInitializer {
+	private static final Logger LOGGER = LoggerFactory.getLogger(ChatTest.class);
+
+	@Override
+	public void onInitialize() {
+		Executor ioWorkerExecutor = Util.getIoWorkerExecutor();
+
+		// Basic content phase testing
+		ServerMessageDecoratorEvent.EVENT.register(ServerMessageDecoratorEvent.CONTENT_PHASE, (sender, message) -> {
+			if (message.getString().contains("tater")) {
+				return CompletableFuture.completedFuture(message.copy().append(" :tiny_potato:"));
+			}
+
+			return CompletableFuture.completedFuture(message);
+		});
+
+		// Basic styling phase testing
+		ServerMessageDecoratorEvent.EVENT.register(ServerMessageDecoratorEvent.STYLING_PHASE, (sender, message) -> {
+			if (sender != null && sender.getAbilities().creativeMode) {
+				return CompletableFuture.completedFuture(message.copy().styled(style -> style.withColor(0xFFA500)));
+			}
+
+			return CompletableFuture.completedFuture(message);
+		});
+
+		// Async testing
+		ServerMessageDecoratorEvent.EVENT.register(ServerMessageDecoratorEvent.CONTENT_PHASE, (sender, message) -> {
+			if (message.getString().contains("wait")) {
+				return CompletableFuture.supplyAsync(() -> {
+					try {
+						Thread.sleep(Random.create().nextBetween(500, 2000));
+					} catch (InterruptedException ignored) {
+						// Ignore interruption
+					}
+
+					return message;
+				}, ioWorkerExecutor);
+			}
+
+			return CompletableFuture.completedFuture(message);
+		});
+
+		// ServerMessageEvents
+		ServerMessageEvents.CHAT_MESSAGE.register(
+				(message, sender, typeKey) -> LOGGER.info("ChatTest: {} sent \"{}\"", sender, message)
+		);
+		ServerMessageEvents.GAME_MESSAGE.register(
+				(message, typeKey) -> LOGGER.info("ChatTest: server sent \"{}\"", message)
+		);
+		ServerMessageEvents.COMMAND_MESSAGE.register(
+				(message, source, typeKey) -> LOGGER.info("ChatTest: command sent \"{}\"", message)
+		);
+
+		// ServerMessageEvents blocking
+		ServerMessageEvents.ALLOW_CHAT_MESSAGE.register(
+				(message, sender, typeKey) -> !message.raw().getContent().getString().contains("sadtater")
+		);
+		ServerMessageEvents.ALLOW_GAME_MESSAGE.register((message, typeKey) -> {
+			if (message.getContent() instanceof TranslatableTextContent translatable) {
+				return !translatable.getKey().startsWith("death.attack.badRespawnPoint.");
+			}
+
+			return true;
+		});
+		ServerMessageEvents.ALLOW_COMMAND_MESSAGE.register(
+				(message, source, typeKey) -> !message.raw().getContent().getString().contains("sadtater")
+		);
+	}
+}
diff --git a/fabric-message-api-v1/src/testmod/resources/fabric.mod.json b/fabric-message-api-v1/src/testmod/resources/fabric.mod.json
new file mode 100644
index 000000000..4a5a8e645
--- /dev/null
+++ b/fabric-message-api-v1/src/testmod/resources/fabric.mod.json
@@ -0,0 +1,16 @@
+{
+  "schemaVersion": 1,
+  "id": "fabric-message-api-v1-testmod",
+  "name": "Fabric Message API (v1) Test Mod",
+  "version": "1.0.0",
+  "environment": "*",
+  "license": "Apache-2.0",
+  "depends": {
+    "fabric-message-api-v1": "*"
+  },
+  "entrypoints": {
+    "main": [
+      "net.fabricmc.fabric.test.message.ChatTest"
+    ]
+  }
+}
diff --git a/gradle.properties b/gradle.properties
index 88c7787b2..309e731c5 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -32,6 +32,7 @@ fabric-keybindings-v0-version=0.2.15
 fabric-lifecycle-events-v1-version=2.0.8
 fabric-loot-api-v2-version=1.0.0
 fabric-loot-tables-v1-version=1.1.0
+fabric-message-api-v1-version=1.0.0
 fabric-mining-level-api-v1-version=2.1.6
 fabric-models-v0-version=0.3.14
 fabric-networking-api-v1-version=1.0.25
diff --git a/settings.gradle b/settings.gradle
index dbed1f2cc..ac0d45fab 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -30,6 +30,7 @@ include 'fabric-item-groups-v0'
 include 'fabric-key-binding-api-v1'
 include 'fabric-lifecycle-events-v1'
 include 'fabric-loot-api-v2'
+include 'fabric-message-api-v1'
 include 'fabric-mining-level-api-v1'
 include 'fabric-models-v0'
 include 'fabric-networking-api-v1'