Documentation Index
Fetch the complete documentation index at: https://mintlify.com/meteor/meteor/llms.txt
Use this file to discover all available pages before exploring further.
Overview
Meteor uses a publish-subscribe pattern for data loading, built on the Distributed Data Protocol (DDP). This allows the server to push data to clients in real-time, keeping client-side collections synchronized with the server.
Publications and Subscriptions
In traditional HTTP-based applications, the client makes requests and receives responses. Meteor’s DDP allows bidirectional data flow:
- Publication: A named API endpoint on the server that constructs and sends data to clients
- Subscription: A client connection to a publication that receives data and updates
A subscription creates a “pipe” that connects a server-side MongoDB collection to the client-side Minimongo cache, keeping them synchronized in real-time.
Defining Publications
Publications should be defined in server-only files.
Basic Publication
// Server-side
Meteor.publish('lists.public', function() {
return Lists.find(
{ userId: { $exists: false } },
{ fields: { name: 1, createdAt: 1 } }
);
});
Key points:
- Return a MongoDB cursor to publish data
- Use field projection to limit exposed data
- The publication name is used by clients to subscribe
Publication with Parameters
import SimpleSchema from 'simpl-schema';
Meteor.publish('todos.inList', function(listId) {
// Always validate arguments from the client
new SimpleSchema({
listId: { type: String }
}).validate({ listId });
return Todos.find(
{ listId },
{ fields: { text: 1, checked: 1, listId: 1 } }
);
});
Using this.userId
Meteor.publish('lists.private', function() {
if (!this.userId) {
return this.ready();
}
return Lists.find(
{ userId: this.userId },
{ fields: Lists.publicFields }
);
});
Always call this.ready() when not returning a cursor, otherwise the subscription will never be marked as ready.
Multiple Collections in One Publication
Meteor.publish('dashboard', function() {
if (!this.userId) {
return this.ready();
}
return [
Lists.find({ userId: this.userId }),
Todos.find({ userId: this.userId }),
Statistics.find({ userId: this.userId })
];
});
Subscribing to Data
Subscriptions are created on the client using Meteor.subscribe().
Basic Subscription
// Client-side
const handle = Meteor.subscribe('lists.public');
// Check if subscription is ready
if (handle.ready()) {
console.log('Data is loaded');
}
Subscription with Arguments
const listId = '123abc';
const handle = Meteor.subscribe('todos.inList', listId);
Stopping Subscriptions
Always stop subscriptions when you’re done with them to free up resources on both client and server.
const handle = Meteor.subscribe('todos.inList', listId);
// Later, when done:
handle.stop();
Reactive Subscriptions
In Blaze Templates
Template.Lists_show_page.onCreated(function() {
this.getListId = () => FlowRouter.getParam('_id');
this.autorun(() => {
// Subscription re-runs when getListId() changes
this.subscribe('todos.inList', this.getListId());
});
});
In React with useTracker
import { useTracker } from 'meteor/react-meteor-data';
import { Todos } from '/imports/api/todos';
function TodoList({ listId }) {
const { todos, isLoading } = useTracker(() => {
const handle = Meteor.subscribe('todos.inList', listId);
return {
todos: Todos.find({ listId }).fetch(),
isLoading: !handle.ready()
};
}, [listId]);
if (isLoading) {
return <div>Loading...</div>;
}
return (
<ul>
{todos.map(todo => (
<li key={todo._id}>{todo.text}</li>
))}
</ul>
);
}
Fetching Data
Always Use Specific Queries
// ❌ Bad: Fetches all data in the collection
const todos = Todos.find().fetch();
// ✅ Good: Specific query matching your subscription
const todos = Todos.find({ listId }).fetch();
Fetch Near Subscribe
Keep data fetching close to subscriptions to avoid “action at a distance”:
// ✅ Good: Subscribe and fetch in the same component
Template.TodoList.onCreated(function() {
this.autorun(() => {
this.subscribe('todos.inList', this.listId);
});
});
Template.TodoList.helpers({
todos() {
return Todos.find({ listId: Template.instance().listId });
}
});
Advanced Publication Patterns
Reactive Joins
Publish related data from multiple collections:
import { publishComposite } from 'meteor/reywood:publish-composite';
publishComposite('todos.withLists', function(userId) {
return {
find() {
return Lists.find({ userId });
},
children: [{
find(list) {
return Todos.find({ listId: list._id });
}
}]
};
});
Low-Level Publish API
For complete control over what data is sent:
Meteor.publish('custom.data', function() {
// Add initial data
this.added('collectionName', 'id1', { field: 'value' });
// Update existing data
this.changed('collectionName', 'id1', { field: 'newValue' });
// Remove data
this.removed('collectionName', 'id1');
// Mark subscription as ready
this.ready();
// Optional: Return a stop handler
this.onStop(() => {
console.log('Subscription stopped');
});
});
Counting Records Efficiently
import { Counts } from 'meteor/tmeasday:publish-counts';
Meteor.publish('todos.count', function(listId) {
Counts.publish(this, 'todos.count', Todos.find({ listId }));
});
// Client-side
const count = Counts.get('todos.count');
Publication Strategies
Meteor supports different publication strategies to balance performance and memory usage:
const publicationStrategies = {
// Default: Server maintains copy of all subscribed data
SERVER_MERGE: {
useCollectionView: true,
doAccountingForCollection: true
},
// No history: Server sends data but doesn't track it
NO_MERGE_NO_HISTORY: {
useCollectionView: false,
doAccountingForCollection: false
},
// Track IDs only for removal
NO_MERGE: {
useCollectionView: false,
doAccountingForCollection: true
}
};
Subscription Readiness
Single Subscription
const handle = Meteor.subscribe('lists.public');
Tracker.autorun(() => {
if (handle.ready()) {
console.log('Subscription ready');
}
});
Multiple Subscriptions
const handles = [
Meteor.subscribe('lists.public'),
Meteor.subscribe('lists.private')
];
Tracker.autorun(() => {
const allReady = handles.every(handle => handle.ready());
if (allReady) {
console.log('All subscriptions ready');
}
});
In Blaze Templates
Template.MyTemplate.helpers({
isReady() {
return Template.instance().subscriptionsReady();
}
});
Security Considerations
Always validate publication arguments and never trust client input.
Meteor.publish('todos.inList', function(listId) {
// Validate arguments
check(listId, String);
// Verify permissions
const list = Lists.findOne(listId);
if (!list || (list.userId && list.userId !== this.userId)) {
return this.ready();
}
return Todos.find({ listId });
});
Field Filtering
Never publish sensitive fields:
Meteor.publish('users.public', function() {
return Meteor.users.find({}, {
fields: {
username: 1,
'profile.name': 1,
// Never publish:
// - services (OAuth tokens)
// - emails (unless needed)
// - password hashes
}
});
});
- Use field projection to limit data sent over the wire
- Limit published documents with reasonable query constraints
- Use indexes on MongoDB for publication queries
- Consider denormalization for frequently accessed data
- Use oplog tailing for better performance in production