matrix-js-sdk: the client library behind Element, and the state machine around /sync
Matrix Client-Server SDK for JavaScript
At a glance
- What is it?
- A TypeScript SDK that wraps the Matrix Client-Server API in an object model, handles local echo and retries, and refuses to hide the protocol from you. Sponsored by Element, versioned into the forties, and now shipping breaking changes about OAuth token refresh.
- Who is it for?
- matrix-js-sdk is the right choice when you are writing a Matrix client and want the sync loop, event model and retry behaviour already solved, and the wrong choice when you want a thin HTTP client or a component framework. Two things are worth knowing before you commit.
- Can I use it commercially?
- Yes. Apache-2.0 is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 2 days ago.
- What is it written in?
- Mainly TypeScript, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 11, 2026, and from our analysis. They are not legal advice.
Editorial analysis
A client library that keeps you inside the protocol
The SDK describes itself plainly: it provides a full object model around the Matrix Client-Server API and emits events for incoming data and state changes. The word that matters in that sentence is full. This is not a thin fetch wrapper with convenience methods bolted on, it is a stateful client that maintains a model of rooms, members and timelines that you read from.
The list of what it handles beyond HTTP transport is the actual product. It handles syncing via `/sync`. It generates friendly room and member names. It keeps historical `RoomMember` information such as display names across sync cycles. It manages room member state across multiple events, including typing, power levels and membership changes. It exposes `Rooms`, `RoomState`, `RoomMembers` and `Users` as objects you can listen to for name changes, new messages, membership changes and presence.
It also does the three things every chat client eventually reinvents badly. Local echo: a message you just sent appears in the timeline marked as sending until it completes, so there is no visible gap between hitting the button and the remote echo arriving. Failed sends are marked as not sent rather than vanishing. And requests to send messages are retried automatically on network errors and on rate limiting errors, which is what keeps a client from dropping messages on a flaky mobile connection.
Getting a client from a URL to a synced state in three calls
The README recommends pnpm over npm, and the install is one line. `pnpm` is pinned as the package manager in `package.json` under `devEngines` at version 12.3.4, with `onFail` set to `download`.
pnpm add matrix-js-sdkCreating a client is a factory call against a homeserver base URL:
import * as sdk from "matrix-js-sdk";
const client = sdk.createClient({ baseUrl: "https://matrix.org" });
client.publicRooms(function (err, data) {
console.log("Public Rooms: %s", JSON.stringify(data));
});Then you start it, with an optional sync limit, and wait for the sync state you actually want to render against:
await client.startClient({ initialSyncLimit: 10 });client.once(ClientEvent.sync, function (state, prevState, res) {
if (state === "PREPARED") {
console.log("prepared");
} else {
console.log(state);
process.exit(1);
}
});That `PREPARED` check is the part beginners miss. `startClient` resolves long before the client is usable for display. The sync states are the client's real lifecycle, and treating anything other than `PREPARED` as fatal, as the README's own snippet does, saves a lot of confusion about empty timelines.
Sending, listening, and the MemoryStore default
Sending is a content object plus an event type, and it is callback based:
const content = {
body: "message text",
msgtype: "m.text",
};
client.sendEvent("roomId", "m.room.message", content, "", (err, res) => {
console.log(err);
});Listening uses a constant rather than a string, which is where the object model earns its keep:
client.on(RoomEvent.Timeline, function (event, room, toStartOfTimeline) {
if (event.getType() !== "m.room.message") {
return; // only use messages
}
console.log(event.event.content.body);
});By default the SDK stores received events in a `MemoryStore`, which means events live in process memory and disappear when the process does. That is fine for a bot or a single page session and wrong for anything that needs to catch up after a restart without refetching.
Object.keys(client.store.rooms).forEach((roomId) => {
client.getRoom(roomId).timeline.forEach((t) => {
console.log(t.event);
});
});Note the last argument to `sendEvent` is the transaction ID, passed as an empty string here. Matrix deduplicates sends by transaction ID, so a retry that reuses the same value does not post the message twice. Passing a fresh string on every retry defeats the retry logic the SDK already implements for you.
Authenticated media is the requirement moving under everyone
Servers implementing MSC3916, which arrived in Matrix 1.11, require clients to send an `Authorization` header when downloading or thumbnailing media. In Node.js this is a matter of turning a flag on and fetching with the header:
const downloadUrl = client.mxcUrlToHttp(
/*mxcUrl=*/ "mxc://example.org/abc123",
/*width=*/ undefined,
/*height=*/ undefined,
/*resizeMethod=*/ undefined,
/*allowDirectLinks=*/ false,
/*allowRedirects=*/ true,
/*useAuthentication=*/ true,
);
const img = await fetch(downloadUrl, {
headers: {
Authorization: `Bearer ${client.getAccessToken()}`,
},
});The README flags this as important before the quickstart even starts, because a client that displays images by pointing an `<img />` at a URL will quietly show nothing once the server switches on authenticated media. In a browser you cannot attach a header to an image request, so the documented answer is a Service Worker that appends the header. That is real extra work in a browser client and essentially free in Node.
There is a warning attached, and it is the kind that should shape your code today rather than after an incident. In future the SDK will only return authentication-required URLs, mandating population of the `Authorization` header. Build with `useAuthentication` set to true now and the eventual change is a no-op for you.
Release cadence, a breaking OAuth change, and the version mismatch
Releases arrive quickly. v42.4.0 shipped on 2026-09-15, v42.4.0-rc.0 on 2026-09-09, and v43.0.0-rc.0 on 2026-09-22, with the last push to the default branch on 2026-09-28. The default branch is `develop`, not `main`.
The v43.0.0-rc.0 notes lead with a single breaking change: OAuth token refresh moving into the SDK, contributed from pull request 5520. If your client currently owns refresh logic, that code is now duplicated, and the migration is the thing to read before you take a v43 line. The rest of that release adds the stable room summary endpoint, updates RTC notification and decline events for MSC4075, and lets applications publish their own data in MatrixRTC memberships.
The v42.4.0 feature list shows where the SDK's centre of gravity sits: LiveKit RTC client server API endpoints, MSC4140 delayed events, a signing function for Rust crypto, a participant limit for key rotation, and combining crypto related `receiveSyncChanges` calls into one. That last one is a performance fix inside the crypto path, which tells you the crypto stack is where the optimisation effort goes.
One thing does not line up, and you should not have to guess which number to believe. The `version` field in `package.json` reads 42.3.0, while published releases already include v42.4.0 and a v43.0.0-rc.0. The manifest and the release tags disagree, which usually means the manifest is bumped at publish time and the version you resolve from the registry may be behind what the release page shows. Pin an exact version and read the release notes rather than trusting either number on its own.
What the repository layout says about the toolchain
The tree is organised for a large TypeScript project with generated output kept out of the way. `src/` holds the code, `spec/` the tests, `docs/` the documentation, and there are separate `examples/node/` and `examples/voip/` directories, which the README points at for anything more complex than the quickstart.
The build is Babel for JavaScript output and `tsc` for declarations. `build:compile` runs Babel over `src` with the extensions set to `.ts,.js`, and `build:types` runs `tsc -p tsconfig-build.json --emitDeclarationOnly`. The published entry points are declared explicitly: `main` points at `./lib/index.js`, `browser` at `./lib/browser-index.js`, and `typings` at `./lib/index.d.ts`. The separate browser entry is what lets the same package behave differently under a bundler.
`package.json` requires Node 22 or later, the module is ESM (`"type": "module"`), and linting uses `oxlint` plus `oxfmt` rather than the older ESLint and Prettier pairing. Tests run under Vitest, and the config file `vitest.config.ts` sits at the root next to `knip.ts` for unused code detection and `diff-cover.toml` for coverage of changed lines only. The coverage setup is the interesting one: measuring only the diff is a deliberate choice for a project with a very large existing test suite, because it makes new code answerable to new tests instead of moving a global percentage.
Editorial conclusion
matrix-js-sdk is the right choice when you are writing a Matrix client and want the sync loop, event model and retry behaviour already solved, and the wrong choice when you want a thin HTTP client or a component framework. Two things are worth knowing before you commit. The default branch is `develop` and the version in `package.json` reads 42.3.0 while v42.4.0 and v43.0.0-rc.0 have already been published, so pinning matters and the release notes are the place to read them. Media handling is the sharper edge: the README documents authenticated media against MSC3916 and warns that the SDK will eventually return only authentication-required URLs, so build `mxcUrlToHttp` with `useAuthentication` set to true from the start rather than retrofitting it. Element uses this in its web and desktop clients, which is the strongest available signal that the object model holds up outside a demo.
Frequently asked questions
What is a Matrix in JavaScript?
Matrix is a federated chat protocol, and a Matrix client in JavaScript is a program that talks to a homeserver over the Client-Server API. matrix-js-sdk is the library that wraps that API, exposing rooms, members, timelines and sync as JavaScript objects instead of raw HTTP responses, and it runs in both browsers and Node.js.
What is Matrix API?
The Matrix Client-Server API is the HTTP interface a Matrix homeserver exposes for logging in, syncing, sending events and managing rooms. The SDK calls those endpoints for you, handles `/sync`, and adds local echo, retry on rate limiting and a member and power level model on top, so applications read objects instead of parsing responses.
Do I need to handle media authentication myself in matrix-js-sdk?
Yes, in the sense that you must opt in. Servers supporting MSC3916 from Matrix 1.11 require an Authorization header on media downloads, so call mxcUrlToHttp with useAuthentication set to true and send the bearer token. In a browser you need a Service Worker to attach the header, since an img element cannot carry one. The README warns the SDK will eventually return only authentication-required URLs.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/matrix-org-matrix-js-sdk)