Introduction to the Matrix Protocol
Matrix is an open, decentralized, and federated real-time communication protocol designed to enable seamless messaging, voice, and video collaboration across different services and platforms. Think of it as the "email for messaging" — just as you can send an email from Gmail to someone using Outlook or a self-hosted server, Matrix allows users on different chat services to communicate with each other natively, without central control or proprietary lock-in.
At its core, Matrix is a set of HTTP-based APIs (collectively known as the Client-Server API) that standardize how clients and servers exchange messages, synchronize conversation history, and manage room state. The protocol is published as an open specification by the Matrix.org Foundation and has been adopted by governments, enterprises, and open-source communities worldwide.
Why Matrix Matters
🚀 Deploy your AI agent in 10 minutes
Managed Hermes hosting. Zero DevOps. 100M tokens/mo included.
Try it free →Understanding the significance of Matrix requires looking at the broader communication landscape. Most messaging platforms today — WhatsApp, Slack, Discord, Telegram — operate as isolated silos. Messages sent on one platform cannot reach users on another unless bridges or third-party integrations are manually configured. Matrix solves this at the protocol level.
True Federation and Decentralization
Matrix servers (called homeservers) can be deployed by anyone. When two homeservers federate, users on each can join shared rooms and exchange messages directly, with no single point of control or failure. This means organizations, communities, and individuals retain ownership of their data while still being able to connect globally.
Interoperability Through Bridges
Matrix's bridging model allows existing platforms like IRC, Slack, Discord, Telegram, and even SMS to be integrated into the Matrix ecosystem. A bridge translates messages between Matrix rooms and third-party channels, making Matrix a universal aggregation layer for all your communication.
End-to-End Encryption
Matrix supports Olm and Megolm, cryptographic protocols based on the Double Ratchet algorithm (the same foundation used by Signal). This provides per-device, per-message encryption that ensures only intended recipients can read message content — even homeserver operators cannot decrypt encrypted rooms.
Open Standard, Royalty-Free
The entire specification is developed openly, with no patent encumbrances. Any developer can implement a client, server, or bot without licensing fees or legal barriers. This fosters a rich ecosystem of interoperable implementations.
Core Concepts and Architecture
Before diving into code, let's establish the foundational concepts that shape every Matrix interaction.
Homeservers
A homeserver is the server-side component that stores message history, manages room state, and handles federation with other servers. Popular implementations include Synapse (the reference implementation in Python), Dendrite (a newer, more scalable implementation in Go), and Conduit (a lightweight Rust implementation). Users register accounts on a homeserver, and their identity takes the form @localpart:domain.tld.
Rooms
All communication in Matrix happens within rooms — persistent conversation containers identified by unique IDs like !abcdef123:example.org. Rooms can be public or invite-only, encrypted or unencrypted, and can contain any number of participants across federated homeservers.
Events
Every message, state change, membership update, or reaction is represented as an event object in Matrix. Events are JSON structures that travel through the Directed Acyclic Graph (DAG) that forms a room's history. The most common event type is m.room.message, but there are dozens of spec-defined event types for typing indicators, read receipts, reactions, edits, and more.
Client-Server API
The Client-Server API (often called the C-S API) is the primary interface developers use. It covers login, registration, room management, message sending, synchronization, and search. The API is RESTful and fully documented at spec.matrix.org.
Matrix URIs and Identifier Syntax
Matrix uses a consistent identifier syntax across all resources:
- User IDs:
@localpart:domain— e.g.,@alice:matrix.org - Room IDs:
!randomstring:domain— e.g.,!xYz123:example.com - Room Aliases:
#roomname:domain— e.g.,#general:matrix.org - Event IDs:
$randomstring:domain— e.g.,$event456:matrix.org - Group/Community IDs:
+groupname:domain
Getting Started: Your First Matrix Client
The quickest way to understand Matrix is to interact with the Client-Server API directly. Let's walk through a complete session: registering an account, logging in, joining a room, sending messages, and retrieving conversation history — all using plain HTTP requests.
Step 1: Choose a Homeserver
For development and testing, you can use the public homeserver matrix.org or spin up your own Synapse instance locally using Docker:
# Run Synapse locally with Docker
docker run -d --name synapse \
-v $(pwd)/data:/data \
-e SYNAPSE_SERVER_NAME=my-local-matrix \
-e SYNAPSE_REPORT_STATS=no \
matrixdotorg/synapse:latest
# Generate a configuration if needed
docker exec synapse generate_config
For the examples below, we'll use https://matrix-client.matrix.org as the base URL for the Client-Server API. Replace it with your own homeserver URL if self-hosting.
Step 2: Register a New User
Registration typically requires specifying a username, password, and optionally an initial device display name. Note that matrix.org may require a registration token or CAPTCHA in production; for local Synapse instances, you can disable these checks in the configuration.
curl -X POST https://matrix-client.matrix.org/_matrix/client/v3/register \
-H "Content-Type: application/json" \
-d '{
"username": "mydevuser",
"password": "s3cur3P@ssw0rd!",
"initial_device_display_name": "My Tutorial Client",
"type": "m.login.dummy"
}'
A successful response returns an access token, device ID, and the fully qualified user ID:
{
"user_id": "@mydevuser:matrix.org",
"access_token": "syt_YWxpY2Vf...",
"device_id": "ABCDEFGHIJ",
"home_server": "matrix.org"
}
Important: Store the access_token securely. This token authenticates all subsequent API calls. For production applications, use the m.login.password or m.login.token login types with proper user-interactive authentication.
Step 3: Authenticated API Calls
Every authenticated request must include the access token either as a query parameter or in the Authorization header. The recommended approach is the header method:
# Using the Authorization header (preferred)
curl -X GET "https://matrix-client.matrix.org/_matrix/client/v3/sync?filter=0&timeout=30000" \
-H "Authorization: Bearer syt_YWxpY2Vf..."
# Alternative: query parameter (for environments where headers are difficult)
curl -X GET "https://matrix-client.matrix.org/_matrix/client/v3/sync?access_token=syt_YWxpY2Vf..."
Step 4: Joining a Room
Let's join the public Matrix HQ room. You can join by room alias (#room:domain) or by room ID:
# Join the Matrix HQ welcome room by alias
curl -X POST "https://matrix-client.matrix.org/_matrix/client/v3/join/%23welcome:matrix.org" \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{}'
Note: The # in room aliases must be URL-encoded as %23. The response contains the full room ID and a summary of the room state:
{
"room_id": "!where:matrix.org",
"servers": ["matrix.org", "elsewhere.org"]
}
Step 5: Sending Messages
To send a message, you need the room ID and must construct a valid event body. The txnId (transaction ID) is a client-generated unique string used for idempotency — retrying with the same txnId won't produce duplicate messages:
curl -X PUT "https://matrix-client.matrix.org/_matrix/client/v3/rooms/%21where%3Amatrix.org/send/m.room.message/unique-txn-id-001" \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"msgtype": "m.text",
"body": "Hello, Matrix world! This is my first message."
}'
The response includes the server-assigned event ID:
{
"event_id": "$abc123def456:matrix.org"
}
Matrix supports several message types beyond plain text:
m.text— Plain text messagem.notice— Notice/announcement (often rendered differently by clients)m.emote— Emote/action message ("* user does something")m.image— Image with thumbnail and URL referencesm.file— Generic file attachmentm.audio— Audio clipm.video— Video clipm.location— Geolocation pin
Step 6: Rich Messages with Formatted Body
Matrix messages can include both a plain text fallback and a formatted HTML representation using the format and formatted_body fields:
curl -X PUT "https://matrix-client.matrix.org/_matrix/client/v3/rooms/%21room%3Amatrix.org/send/m.room.message/txn-002" \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"msgtype": "m.text",
"body": "This is **bold** and _italic_ text (fallback)",
"format": "org.matrix.custom.html",
"formatted_body": "This is bold and italic text
"
}'
Step 7: Syncing and Retrieving Messages
The sync endpoint is the primary mechanism for clients to receive new events. A long-polling sync request returns batched events from all rooms the user belongs to:
curl -X GET "https://matrix-client.matrix.org/_matrix/client/v3/sync?timeout=30000&filter=0" \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN"
The response structure contains three main sections under rooms:
join— Events from rooms the user has joinedinvite— Invitations to new roomsleave— Rooms the user has left but still receives limited events from
A next_batch token is returned for incremental sync. Pass it as the since parameter in subsequent requests to receive only events that occurred after that point:
# First sync to get initial state and a next_batch token
curl -X GET "https://matrix-client.matrix.org/_matrix/client/v3/sync" \
-H "Authorization: Bearer YOUR_TOKEN"
# Subsequent incremental syncs
curl -X GET "https://matrix-client.matrix.org/_matrix/client/v3/sync?since=next_batch_token_here&timeout=30000" \
-H "Authorization: Bearer YOUR_TOKEN"
Building a JavaScript Matrix Client
While raw HTTP requests are educational, production applications typically use the matrix-js-sdk — the official JavaScript/TypeScript SDK maintained by the Matrix.org team. Let's build a simple Node.js bot that listens for messages and responds automatically.
Setting Up the Project
mkdir matrix-bot-tutorial
cd matrix-bot-tutorial
npm init -y
npm install matrix-js-sdk
# Optional: install for TypeScript support
npm install --save-dev @types/node typescript
Bot Implementation
Create a file named bot.js with the following code. This bot logs in, syncs continuously, listens for text messages containing "!ping", and replies with "pong":
const MatrixClient = require('matrix-js-sdk').MatrixClient;
const SimpleFsStore = require('matrix-js-sdk').SimpleFsStore;
const AutojoinRoomsMixin = require('matrix-js-sdk').AutojoinRoomsMixin;
// Configuration - use environment variables in production
const BOT_USERNAME = 'mybotuser';
const BOT_PASSWORD = 'your-secure-password';
const HOMESERVER_URL = 'https://matrix-client.matrix.org';
async function main() {
// Create a client instance
const client = MatrixClient.createClient({
baseUrl: HOMESERVER_URL,
// Use simple file-based storage for session persistence
// In production, consider IndexedDB or a proper database
store: new SimpleFsStore({
filename: './bot-session.json'
})
});
// Enable auto-joining rooms on invite
AutojoinRoomsMixin.setupOnClient(client);
// Login with username and password
const loginResponse = await client.login('m.login.password', {
username: BOT_USERNAME,
password: BOT_PASSWORD,
initialDeviceDisplayName: 'Node.js Tutorial Bot'
});
console.log(`Logged in as ${loginResponse.user_id}`);
console.log(`Access token: ${loginResponse.access_token}`);
// Start the sync loop
await client.startClient({
initialSyncLimit: 10, // Messages per room on initial sync
pollTimeout: 30000, // Long-poll timeout in ms
resolveInvitesSeen: true, // Mark invites as seen
filter: {
room: {
timeline: { limit: 10 },
ephemeral: { limit: 0 }
}
}
});
console.log('Sync started. Bot is now listening for messages...');
// Listen for timeline events in any room
client.on('Room.timeline', async (event, room, toStartOfTimeline, removed) => {
// Ignore events that are historical or removed
if (toStartOfTimeline || removed) return;
// Only process m.room.message events
if (event.getType() !== 'm.room.message') return;
// Ignore messages sent by the bot itself to prevent loops
if (event.getSender() === client.getUserId()) return;
const content = event.getContent();
const body = content.body;
console.log(`[${room.name || room.roomId}] ${event.getSender()}: ${body}`);
// Simple command: respond to "!ping"
if (body.trim().toLowerCase() === '!ping') {
const replyBody = '🏓 pong! (via Matrix bot)';
const replyContent = {
msgtype: 'm.text',
body: replyBody,
format: 'org.matrix.custom.html',
formatted_body: `🏓 pong! (via Matrix bot)
`
};
await client.sendMessage(room.roomId, replyContent);
console.log(` ↳ Replied with: ${replyBody}`);
}
// Command: echo back a message
if (body.startsWith('!echo ')) {
const echoText = body.slice(6);
await client.sendMessage(room.roomId, {
msgtype: 'm.text',
body: echoText
});
console.log(` ↳ Echoed: ${echoText}`);
}
});
// Handle invitations explicitly if needed
client.on('RoomMember.membership', async (event, member) => {
if (member.membership === 'invite' && member.userId === client.getUserId()) {
console.log(`Invited to room: ${event.getRoomId()}`);
await client.joinRoom(event.getRoomId());
console.log(`Joined room: ${event.getRoomId()}`);
}
});
// Graceful shutdown handler
process.on('SIGINT', async () => {
console.log('\nShutting down bot...');
client.stopClient();
await client.logout();
console.log('Logged out. Goodbye!');
process.exit(0);
});
}
main().catch(err => {
console.error('Fatal error:', err);
process.exit(1);
});
Running the Bot
node bot.js
The bot will log in, start syncing, and immediately begin responding to !ping and !echo commands in any room it has joined. The session is persisted to bot-session.json, so restarting the bot won't require re-login — it can reuse the stored access token.
Working with Encrypted Rooms
End-to-end encryption in Matrix uses the Olm and Megolm protocols. To participate in encrypted rooms, your client must set up encryption, upload device keys, and establish Olm sessions with other devices. The SDK handles most of this automatically when you call client.initCrypto().
Here's how to extend the bot to support encrypted messaging:
// After login and before starting the sync
// Initialize the crypto module
await client.initCrypto();
// Wait for crypto to be ready
await client.waitForCryptoReady();
console.log('Crypto subsystem initialized');
// Store crypto keys persistently - critical!
client.on('crypto.keyExport', (exportedKeys) => {
// Save exportedKeys to a secure file or database
// These keys are needed to decrypt historical messages
fs.writeFileSync('./crypto-keys-backup.json', JSON.stringify(exportedKeys));
});
// Start client sync AFTER crypto is ready
await client.startClient({
// ... sync options
});
// For encrypted rooms, send messages normally - encryption is transparent
await client.sendMessage(encryptedRoomId, {
msgtype: 'm.text',
body: 'This message will be automatically encrypted'
});
Critical crypto considerations:
- Always persist encryption keys — losing them means losing access to past encrypted messages
- Call
initCrypto()beforestartClient() - For bots that need to read old messages, implement key backup using the
m.megolm_backup.v1mechanism - Verify device identity using cross-signing keys for sensitive applications
Room Management and State Events
Beyond messaging, Matrix rooms support rich state management. Room state events control the room name, topic, avatar, join rules, guest access, and power levels. Understanding state events is essential for building administrative tools and bots.
Setting Room State
State events are sent to /rooms/{roomId}/state/{eventType}/{stateKey}. Common state keys include an empty string for room-level settings (name, topic) or user IDs for member-specific state:
// Set the room name
curl -X PUT "https://matrix-client.matrix.org/_matrix/client/v3/rooms/%21room%3Adomain/state/m.room.name/" \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{"name": "Developer Tutorial Room"}'
// Set the room topic
curl -X PUT "https://matrix-client.matrix.org/_matrix/client/v3/rooms/%21room%3Adomain/state/m.room.topic/" \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{"topic": "Learning Matrix Protocol together"}'
// Set room avatar (requires a pre-uploaded MXC URI)
curl -X PUT "https://matrix-client.matrix.org/_matrix/client/v3/rooms/%21room%3Adomain/state/m.room.avatar/" \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{"url": "mxc://matrix.org/abcdef123456"}'
Power Levels
Power levels control permissions within a room. The m.room.power_levels event defines thresholds for actions like kicking users, banning, setting state, and sending specific message types:
curl -X PUT "https://matrix-client.matrix.org/_matrix/client/v3/rooms/%21room%3Adomain/state/m.room.power_levels/" \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"users": {
"@admin:matrix.org": 100,
"@moderator:matrix.org": 50
},
"users_default": 0,
"events": {
"m.room.name": 50,
"m.room.power_levels": 100
},
"events_default": 0,
"state_default": 50,
"ban": 50,
"kick": 50,
"redact": 50,
"invite": 50
}'
Content Uploads and Attachments
Sending images, files, and other media requires uploading content to the homeserver first, which returns a Matrix Content URI (MXC URI) that can be referenced in message events.
// Step 1: Upload the file
curl -X POST "https://matrix-client.matrix.org/_matrix/media/v3/upload" \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: image/png" \
--data-binary "@my-image.png"
// Response:
// { "content_uri": "mxc://matrix.org/ABCDefgh123456" }
// Step 2: Send an image message referencing the MXC URI
curl -X PUT "https://matrix-client.matrix.org/_matrix/client/v3/rooms/%21room%3Adomain/send/m.room.message/txn-image-001" \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"msgtype": "m.image",
"body": "Check out this screenshot",
"url": "mxc://matrix.org/ABCDefgh123456",
"info": {
"mimetype": "image/png",
"size": 245760,
"w": 1920,
"h": 1080
}
}'
The info object provides metadata that clients use for responsive rendering before downloading the full image. Always include accurate dimensions and file size when possible.
Creating and Managing Rooms Programmatically
Creating a room via the API allows you to preset configurations like visibility, encryption, and initial state:
curl -X POST "https://matrix-client.matrix.org/_matrix/client/v3/createRoom" \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "My Developer Room",
"topic": "A room for discussing Matrix development",
"preset": "private_chat",
"visibility": "private",
"invite": ["@friend:matrix.org", "@colleague:otherserver.org"],
"initial_state": [
{
"type": "m.room.encryption",
"content": {
"algorithm": "m.megolm.v1.aes-sha2",
"rotation_period_ms": 604800000,
"rotation_period_messages": 100
}
}
],
"creation_content": {
"m.federate": true
}
}'
Key parameters explained:
preset: Can beprivate_chat,trusted_private_chat, orpublic_chat. Affects default join rules and guest access.visibility:privatemeans the room is not listed in the public room directory;publicmakes it discoverable.initial_state: Pre-seed the room with state events like encryption configuration before any messages are exchanged.creation_content.federate: Whether to allow users from other homeservers to join.
Bridging to Other Platforms
Bridges are Matrix's superpower for interoperability. A bridge is a service that connects a Matrix room to a channel on another platform. The Matrix.org ecosystem includes mature bridges for IRC, Slack, Discord, Telegram, WhatsApp, Signal, and more.
Bridge Architecture
Bridges typically operate in two modes:
- Puppeting: The bridge logs into your account on the third-party service and mirrors your activity — messages you send from Matrix appear as if you sent them directly on the other platform.
- Relay: The bridge uses its own account to relay messages, adding a prefix like
[Alice]to indicate the original sender.
Deploying a Simple Bridge (Conceptual)
Most bridges run as separate services alongside your homeserver. Here's a conceptual setup using the popular mautrix-discord bridge (a common pattern for all mautrix-based bridges):
# Example: Setting up mautrix-discord bridge (conceptual)
# 1. Clone the bridge repository
git clone https://github.com/mautrix/discord.git
cd discord
# 2. Configure the bridge
cp example-config.yaml config.yaml
# Edit config.yaml with:
# - Your homeserver URL
# - Registration settings (the bridge appears as a special user on Matrix)
# - Discord bot token
# 3. Generate the registration file for your homeserver
python -m mautrix_discord -g
# 4. Add the registration file to your homeserver's app service config
# In Synapse's homeserver.yaml:
# app_service_config_files:
# - /path/to/discord-registration.yaml
# 5. Run the bridge
python -m mautrix_discord
Once running, users can interact with the bridge bot to link their Discord accounts and bridge specific channels into Matrix rooms. The bridge handles message translation, attachment conversion, and even reaction mapping between platforms.
Best Practices for Matrix Development
1. Token Management and Security
Access tokens grant full account access — treat them like passwords. Never hardcode tokens in client-side code or commit them to version control. Use environment variables or secure credential stores. Implement token refresh logic; tokens can expire or be revoked server-side.
// Good: Load from environment
const ACCESS_TOKEN = process.env.MATRIX_ACCESS_TOKEN;
// Better: Use a secure vault service in production
const token = await secretsManager.getSecret('matrix-access-token');
2. Idempotent Message Sending
Always generate unique transaction IDs for PUT /send requests. Retrying a failed send with the same transaction ID is safe and prevents duplicate messages. Use UUIDs or a combination of timestamp and random string:
// Generate a unique transaction ID
const txnId = `msg_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
const endpoint = `/rooms/${roomId}/send/m.room.message/${txnId}`;
3. Sync Loop Resilience
The sync loop is the backbone of real-time communication. Implement exponential backoff on sync failures, handle HTTP errors gracefully, and never block the sync loop with long-running operations:
let retryDelay = 1000; // Start with 1 second
const MAX_RETRY_DELAY = 300000; // Cap at 5 minutes
client.on('sync.error', async (error) => {
console.error(`Sync error: ${error.message}. Retrying in ${retryDelay}ms...`);
await new Promise(resolve => setTimeout(resolve, retryDelay));
retryDelay = Math.min(retryDelay * 2, MAX_RETRY_DELAY);
client.startClient();
});
client.on('sync.success', () => {
// Reset retry delay on successful sync
retryDelay = 1000;
});
4. Rate Limiting Awareness
Homeservers enforce rate limits to protect resources. Batch operations, respect 429 Too Many Requests responses with Retry-After headers, and spread bulk message sends over time rather than sending hundreds of messages simultaneously:
// Respect rate limiting with a simple delay queue
async function sendWithRateLimit(messages, roomId) {
for (const msg of messages) {
await client.sendMessage(roomId, msg);
// Wait 500ms between messages to stay within limits
await new Promise(resolve => setTimeout(resolve, 500));
}
}
5. Encryption Key Backup
For encrypted rooms, always enable and regularly update key backup. Without it, users cannot decrypt messages received while they were offline if their device is lost. Implement the m.megolm_backup.v1 protocol:
// Enable key backup with a recovery passphrase
await client.enableKeyBackup({
algorithm: 'm.megolm_backup.v1.curve25519-aes-sha2',
// Store a copy of the recovery key securely
// The user must save this to recover their message history
});
// Regularly check backup integrity
client.on('keyBackupStatus', (status) => {
if (status.backedUpCount < status.totalCount) {
console.warn(`Key backup lagging: ${status.backedUpCount}/${status.totalCount} keys backed up`);
}
});
6. Client-Side Filtering
Use sync filters to reduce bandwidth and processing overhead. Filters specify which event types and rooms to include in sync responses:
const filterDefinition = {
room: {
timeline: {
limit: 20,
types: ['m.room.message', 'm.room.member', 'm.room.name']
},
ephemeral: {
types: ['m.typing', 'm.receipt']
},
account_data: {
types: ['m.direct']
}
},
presence: {
types: ['m.presence']
}
};
// Upload the filter
const filterResponse = await client.uploadFilter(filterDefinition);
const filterId = filterResponse.filter_id;
// Use in sync requests
await client.startClient({ filter: { id: filterId } });
7. Error Handling and Logging
Matrix operations can fail for many reasons: network issues, server errors, invalid parameters, or permission denials. Implement comprehensive error handling:
try {
await client.sendMessage(roomId, messageContent);
} catch (error) {
if (error.errcode === 'M_FORBIDDEN') {
console.error('Bot lacks permission to send messages in this room');
} else if (error.errcode === 'M_NOT_FOUND') {
console.error('Room not found or bot not joined');
} else if (error.errcode === 'M_LIMIT_EXCEEDED') {
const retryAfter = error.data?.retry_after_ms || 5000;
console.warn(`Rate limited. Waiting ${retryAfter}ms...`);
await new Promise(r => setTimeout(r, retryAfter));
} else {
console.error(`Unexpected error: ${error.message}`, error);
}
}
8. Testing Against Multiple Homeservers
Federation means your client or bot may interact with users on different homeservers. Test against at least two homeserver implementations (Synapse, Dendrite, or Conduit) to ensure compatibility. The Matrix Federation Tester (available at federationtester.matrix.org) can help verify homeserver federation readiness.
9. Keep Up with Specification Changes
The Matrix specification evolves continuously through a formal proposal process called MSC (Matrix Spec Change). Major clients and servers implement MSCs before they become part of the official spec. Monitor the matrix-spec-proposals repository and the Matrix blog to stay informed about upcoming