Server-Authoritative Reactivity with Meteor Pub/Sub and Minimongo
Meteor’s pub/sub with Minimongo gives server-authoritative real-time updates without polling, but requires narrow publications and careful handling of client cache and server state.
23 Jul 2025, 02:19 UTC

Building a collaborative UI without polling quickly turns into manual sync code: refetch loops, stale lists, and race conditions between clients. Meteor’s opinionated answer is server-authoritative pub/sub with reactive cursors and a client-side Minimongo cache. It removes boilerplate for real-time updates, at the cost of coupling data shape to the client and keeping per-subscriber server state.
Assumption: Meteor 3.x with async/await core APIs. Older Fiber-based patterns do not apply.
Publications define shape and security, subscriptions manage interest
A publication is server code that returns a Mongo cursor. It is the security boundary. The client cannot ask for more than the publication yields, and it should never rely on client-side checks alone.
Subscriptions declare client interest and lifecycle. When a subscription parameter changes, Meteor tears down the old subscription and starts a new one. On the server, the merge box reconciles multiple subscriptions into a single client cache, de-duplicating documents that arrive from different publications.
Risk: an overly broad cursor leaks data and increases bandwidth. Minimongo is a client subset of MongoDB; unsupported operators cause silent divergence between server and client queries.
Client reactivity via Minimongo and explicit writes via methods
Minimongo mirrors the documents sent by publications locally. Reactive queries run against Minimongo, so templates and computations update automatically via Tracker when DDP messages arrive.
Writes go through Meteor.methods. Methods integrate with latency compensation: the client can apply a local optimistic change immediately, then reconcile when the server confirms or rejects. This keeps UI snappy without exposing direct collection writes from the client.
Verification approach: create a minimal app with a publication returning a cursor and a client subscription. Log collection changes to observe reactive updates. Inspect DDP messages in the browser dev tools network tab to see added, changed, removed messages flow on mutations. Call a method that modifies a document and observe the local collection update before server acknowledgement to see optimistic UI.
Worked example: user-scoped todos
Server publication, run in /server/publications.js with server permissions:
// server/publications.js\nimport { Meteor } from 'meteor/meteor';\nimport { Todos } from '/imports/api/todos';\n\nMeteor.publish('todos.forUser', async function(userId) {\n if (!this.userId || this.userId !== userId) {\n return this.ready();\n }\n return Todos.find(\n { userId },\n { fields: { userId: 1, text: 1, completed: 1, createdAt: 1 } }\n );\n});The publication filters by userId and limits fields. Changing the filter or adding fields changes what the client can see.
Client subscription, run in /client/main.js in a browser context:
// client/main.js\nimport { Meteor } from 'meteor/meteor';\nimport { Todos } from '/imports/api/todos';\n\nconst userId = Meteor.userId();\nconst sub = Meteor.subscribe('todos.forUser', userId);\n\n// Reactive query\nTracker.autorun(() => {\n if (sub.ready()) {\n const list = Todos.find({}, { sort: { createdAt: -1 } }).fetch();\n // render list\n }\n});UI updates automatically when the server pushes changes. The subscription parameter is reactive, so changing the filter recreates the subscription.
Write path via method:
// server/methods.js\nMeteor.methods({\n async 'todos.toggle'(todoId) {\n const todo = await Todos.findOneAsync({ _id: todoId, userId: this.userId });\n if (!todo) throw new Meteor.Error('not-found');\n await Todos.updateAsync(todoId, { $set: { completed: !todo.completed } });\n }\n});Client call:
Meteor.call('todos.toggle', todoId);The client sees the local update optimistically, then the server change is merged via DDP.
Cost of fine-grained reactivity
Fine-grained reactivity adds memory and CPU overhead per subscriber on the server. Each active subscription tracks oplog changes for its query. Over publishing large collections or publishing without field limits increases bandwidth and client memory.
Pub/sub is less efficient than paginated REST for large read-only datasets where real-time updates are not needed. Coupling data shape to the client makes schema changes harder to evolve.
Start narrow and verify
Start with narrow publications using explicit fields limits and user-scoped filters. Prefer methods for mutations rather than allowing client writes. Monitor subscription counts and oplog traffic in production, and test reactivity under simulated latency to validate UX.
Check results by confirming the client cache matches the publication filter and that DDP messages only contain allowed fields. If a query works on the server but not on the client, review Minimongo operator support.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.