Filtering Data in Meteor: How Publish‑Subscribe Lets You Curate What the Client Sees
Learn how Meteor’s publish‑subscribe pattern lets you filter data on the server, keep the UI fast with Minimongo, and avoid bandwidth limits. See a concrete example, pagination tips, and practical trade‑offs.
23 Oct 2026, 19:17 UTC

Problem: Exposing Too Much Data
When you build a Meteor app that uses a shared MongoDB collection, the default is to publish the entire collection to every connected client. For small apps this is fine, but in real‑world scenarios you quickly hit three pain points:
- Security – users should only see data they’re allowed to read.
- Performance – the browser downloads a lot of JSON that it never uses.
- Reactivity – the UI updates when the entire collection changes, even if the change isn’t relevant.
The Meteor publish‑subscribe pattern solves all of these by letting the server decide what to send and the client cache it reactively.
Thesis: Use MongoDB‑style selectors and reactive subscriptions to deliver just the right data slice while keeping the UI fast and secure.
Below we walk through how to create a role‑based publication, how the client reacts to changes, and the practical limits that can surface when you push large datasets through the wire.
1. Server‑Side Publishing with Selectors
Publications are server functions that return a MongoDB cursor. The cursor can include any query, sort, projection, or limit you’d normally use with Posts.find. Meteor then streams only the matching documents to the client.
// server/publications.js
import { Meteor } from 'meteor/meteor';
import { Posts } from '../imports/api/posts.js';
Meteor.publish('posts.byAuthor', function (authorId) {
check(authorId, String);
// Only publish posts the logged‑in user owns or has "view" role for
if (!this.userId) return this.ready();
const user = Meteor.users.findOne(this.userId);
const canViewAll = user?.roles?.includes('admin');
const selector = canViewAll
? {} // admins see everything
: { authorId }; // others see only their posts
return Posts.find(selector, {
sort: { createdAt: -1 },
fields: { content: 1, title: 1, createdAt: 1 },
limit: 50,
});
});
Key points:
- Use
checkto validate incoming arguments. - Always guard with
this.userIdto prevent anonymous access. - The
fieldsprojection limits the data size. - Set a sensible
limitto avoid sending thousands of rows.
2. Client‑Side Subscription and Latency Compensation
On the client, you subscribe to the publication. The subscription is reactive – if the selector changes, Meteor automatically re‑runs the publication and updates Minimongo, the client‑side cache.
// client/subscriptions.js
import { Meteor } from 'meteor/meteor';
import { Posts } from '../imports/api/posts.js';
Template.userPosts.onCreated(function () {
this.authorId = new ReactiveVar(this.data.authorId);
this.autorun(() => {
const authorId = this.authorId.get();
this.subscribe('posts.byAuthor', authorId);
});
});
Template.userPosts.helpers({
posts() {
return Posts.find(); // reads from Minimongo
},
});
When the user switches to a different author, the autorun re‑runs, the server re‑evaluates the query, and the UI updates instantly. Meteor’s stub methods provide latency compensation – the UI behaves as if the data were already there, making the app feel snappy even over slow connections.
Beware: if a stub method throws or times out, Minimongo is cleared for that collection, and the UI may show an error or empty state. Always provide graceful fallbacks in templates.
3. Practical Limits and Pagination Strategies
Two Meteor‑specific limits can bite you:
- Publish bandwidth cap (~1 MB) – If the cursor’s total size exceeds this, the server logs a warning and truncates the data. Use
limitandfieldsto stay below the threshold. - MongoDB cursor size – The underlying driver caps the number of documents in a cursor to 10 000 by default. For very large result sets, implement pagination or server‑side aggregation.
Typical mitigation: combine limit with skip or use Posts.find(selector, { sort, fields, skip, limit }). For infinite scrolling, trigger a new subscription with an updated skip when the user scrolls near the bottom.
4. Worked Example: Role‑Based Post Feed with Pagination
Below is a minimal, end‑to‑end snippet that demonstrates:
- Server publication that filters by author and role.
- Client subscription that reacts to user selection.
- Simple pagination controls.
// server/publications.js
Meteor.publish('posts.paginated', function (authorId, page) {
check(authorId, String);
check(page, Number);
const user = Meteor.users.findOne(this.userId);
if (!user) return this.ready();
const selector = user.roles?.includes('admin') ? {} : { authorId };
const limit = 20;
const skip = (page - 1) * limit;
return Posts.find(selector, {
sort: { createdAt: -1 },
fields: { title: 1, excerpt: 1, createdAt: 1 },
skip,
limit,
});
});
// client/templates/posts.js
Template.posts.onCreated(function () {
this.page = new ReactiveVar(1);
this.authorId = new ReactiveVar(this.data.authorId);
this.autorun(() => {
this.subscribe('posts.paginated', this.authorId.get(), this.page.get());
});
});
Template.posts.helpers({
posts() {
return Posts.find();
},
hasMore() {
return Posts.find().count() === 20; // simple heuristic
},
});
Template.posts.events({
'click .next'(event, instance) {
instance.page.set(instance.page.get() + 1);
},
'click .prev'(event, instance) {
const newPage = Math.max(1, instance.page.get() - 1);
instance.page.set(newPage);
},
});
Verification steps:
- Start the app:
meteor run. - Open the browser console and watch the
SubscribedandReadylogs. - Switch authors via
Template.posts.authorId.set('newAuthorId')and confirm the UI updates without a full reload. - Check
console.log(Posts.find().fetch().length)to ensure the client only holds 20 documents.
Trade‑off: Client‑Side vs Server‑Side Filtering
In some cases you might be tempted to publish the entire collection and filter on the client. That approach is simple but:
- Exposes more data than necessary.
- Increases bandwidth and memory usage.
- Can lead to stale data if the server changes the underlying documents after the client has cached them.
Server‑side filtering keeps the data flow tight and the UI consistent, but it requires careful design to avoid over‑fetching and to respect MongoDB limits.
Actionable Take‑away
When building a Meteor app that needs to show user‑specific data:
- Write publications that accept parameters and use
checkfor validation. - Use
fieldsandlimitto keep payloads small. - On the client, subscribe reactively, and rely on Minimongo for instant UI updates.
- Plan for pagination or chunked sends if you anticipate >10 000 documents or >1 MB payloads.
- Test with the Meteor logs and browser dev tools to confirm that the subscription is ready and that the UI reflects the latest server data.
With these patterns, Meteor’s publish‑subscribe system becomes a powerful tool for delivering secure, performant, and reactive data slices to your users.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.