Meteor 3.5-beta.10 is available
Beta 10 of Meteor 3.5 is the last beta (I hope
) before an RC. It lands a new DDP transport architecture, makes Express endpoints first-class citizens of Meteor’s auth model, brings native MongoDB collation to both server and Minimongo, removes one of the longest-standing allocation hotspots in DDP, and fixes a subtle but painful Change Streams bug. Here are the 5 highlights you should know about.
1. Pluggable DDP transport architecture — pick the right trade-off for your app
PR #14231 (by @dupontbertrand)
Until 3.5, the DDP transport was hard-wired to SockJS. Beta 10 refactors the transport layer into a pluggable architecture: each transport is an isolated module behind a common interface, and the client uses native WebSocket whenever a non-SockJS transport is selected.
This unlocks substantial performance gains in Meteor’s real-time layer, modern backends like uws cut round-trip latency. The official benchmarks will come soon.
Available transports:
-
sockjs (default) — maximum compatibility behind strict proxies, polling fallback
-
uws — uWebSockets.js, native C++ implementation
Switch with one env var or one settings flag — no application code changes:
DDP_TRANSPORT=uws meteor run
{ "packages": { "ddp-server": { "transport": "uws" } } }
Documentation
2. Authenticated REST endpoints with accounts-express
PR #14091 (by @nachocodoner)
This closes a long-standing gap: until now, an Express route mounted on a Meteor app couldn’t see Meteor.userId() or Meteor.user() — every team rolled their own token-parsing middleware to bridge the two worlds.
Beta 10 ships a new accounts-express package with an Accounts.auth() middleware that gates routes on authentication (or any custom condition you write). Inside a protected route, the standard Meteor user context Just Works:
import express from 'express';
import { WebApp } from 'meteor/webapp';
import { Accounts } from 'meteor/accounts-base';
const app = express();
app.get('/api/me', Accounts.auth(), (req, res) => {
// Meteor.userId() and Meteor.user() are available here
res.json({ user: Meteor.user() });
});
WebApp.handlers.use(app);
On the client, a new Meteor.fetchWithAuth helper sends the auth token automatically, so consuming your own protected endpoints from a Meteor client no longer means hand-writing header logic.
This makes REST a first-class citizen of Meteor’s auth model — alongside methods and publications — without bespoke glue code.
Documentation · Meteor.fetch / Meteor.fetchWithAuth
3. MongoDB Collation Support — real case-insensitive search, end to end
PR #14188 (by @mvogttech)
If you’ve ever needed case-insensitive lookups in Meteor, you’ve probably bumped into generateCasePermutationsForString — a workaround built on top of giant $or queries that’s been around since MongoDB v2. It exists because collation options passed to Collection.find() were silently dropped by the driver, and Minimongo had no concept of locale-aware string comparison at all.
Beta 10 adds full MongoDB collation support to find, findOne, findOneAsync, and observeChanges — and implements collation-aware comparison in Minimongo using Intl.Collator, so client and server behave identically. Crucially, it’s compatible with oplog tailing: no fallback to polling.
// Case-insensitive find — same result on server and Minimongo
const users = Users.find(
{ email: 'Alice@Example.COM' },
{ collation: { locale: 'en', strength: 2 } }
).fetch();
// Locale-aware sort
Posts.find({}, {
collation: { locale: 'en', strength: 2 },
sort: { title: 1 },
}).fetch();
// Backed by a collation index
await Users.createIndexAsync(
{ email: 1 },
{ collation: { locale: 'en', strength: 2 } }
);
International, accented, and case-mixed text now behaves the way users expect — no regex tricks, no duplicated lowercase fields, no silent fallback to polling on busy collections.
Documentation
4. Zero-clone stringifyDDP — a quiet, big perf win
PR #14213 (by @mvogttech)
Every DDP message sent to every client used to be deep-cloned via EJSON.clone(msg) before serialization, purely to avoid mutating the caller’s object. With 500 clients receiving the same document change, that’s 500 deep clones producing the same string.
Beta 10 replaces this with a wire-format builder that reads the original message directly and uses copy-on-write EJSON.toJSONValue per field — only allocating for subtrees that actually contain EJSON types. It also adds a fast path for payload-free messages (ping, pong, ready, removed, nosub), and fixes a double-serialization bug in Session.send() when Meteor._printSentDDP was enabled.
Benchmark (500 clients × 1,000 iterations):
| Message | Before | After | Speedup |
|—|—|—|—|
| changed with cleared fields | 729 ms | 225 ms | 3.2× |
| changed (all cleared) | 660 ms | 163 ms | 4.1× |
| added (with Date) | 440 ms | 329 ms | 1.3× |
| added (20 fields, 1 Date) | 1164 ms | 968 ms | 1.2× |
| ping / ready | 66–118 ms | 48–84 ms | 1.3–1.4× |
No API change, no opt-in needed — just less CPU time and fewer allocations on every DDP-heavy server.
5. Fix: ObjectID fields sent as binary with projections under Change Streams
PR #14238 (by @italojs)
A bug surfaced after the Change Streams driver shipped: when using observeChanges with a field projection, ObjectID fields were arriving on the client as raw binary buffers instead of Mongo.ObjectID instances.
Root cause: the Change Streams driver receives fullDocument with native BSON types and was passing it directly to LocalCollection._compileProjection, whose internal EJSON.clone coerced ObjectId values into Binary. The oplog driver wasn’t affected because it re-fetches through Meteor’s cursor layer, which already handles type conversion.
The fix applies replaceTypes(doc, replaceMongoAtomWithMeteor) before projecting, so native BSON types become Meteor equivalents before _compileProjection runs. Regression tests cover initial add, insert, and update events.
If you tried Change Streams in earlier 3.5 betas and saw weird ObjectID values on the client when projections were involved — this is the one.
How to try it
meteor update --release 3.5-beta.10
For the full picture, including all bumped packages and other improvements (EJSON copy-on-write, async DDP rate limiter matchers, DDP session resumption, accounts-base asyncification, and more), see the 3.5 changelog.
what’s next?
Thanks
Huge thanks to @dupontbertrand, @nachocodoner, @mvogttech, @alextaaa, and everyone who tested the betas and filed feedback in the forum thread. Beta 10 is the result of that back-and-forth — keep it coming.