Skip to main content

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

Publications are Meteor’s way of sending data from the server to clients. They define what data clients can access and automatically keep that data synchronized in real-time through DDP (Distributed Data Protocol).

How Publications Work

A publication:
  1. Runs on the server when a client subscribes
  2. Returns MongoDB cursors or uses low-level API calls
  3. Sends initial data to the client
  4. Monitors for changes and sends updates automatically
  5. Cleans up when the client unsubscribes
// Server: Define what data to publish
Meteor.publish('todos', function() {
  return Todos.find({ userId: this.userId });
});

// Client: Subscribe to receive data
Meteor.subscribe('todos');

// Client: Query the local cache
const todos = Todos.find().fetch();

Defining Publications

Basic Publication

// Server-side only
Meteor.publish('lists.public', function() {
  return Lists.find(
    { public: true },
    {
      fields: { name: 1, description: 1, createdAt: 1 },
      sort: { createdAt: -1 },
      limit: 100
    }
  );
});

Publication with Parameters

import { check } from 'meteor/check';

Meteor.publish('todos.inList', function(listId) {
  // Always validate parameters
  check(listId, String);
  
  return Todos.find(
    { listId },
    { fields: { text: 1, checked: 1, createdAt: 1 } }
  );
});

User-Specific Publications

Meteor.publish('lists.private', function() {
  // Check if user is logged in
  if (!this.userId) {
    return this.ready();
  }
  
  return Lists.find(
    { userId: this.userId },
    { fields: Lists.publicFields }
  );
});
Always call this.ready() when not returning a cursor to signal that initial data has been sent.

Publishing Multiple Collections

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 })
  ];
});

Publication Context

Inside a publication function, this provides:
Meteor.publish('example', function(param) {
  // Current user ID
  console.log(this.userId);
  
  // Connection information
  console.log(this.connection.id);
  console.log(this.connection.clientAddress);
  
  // Signal that initial data is sent
  this.ready();
  
  // Add/change/remove documents manually
  this.added('collectionName', id, fields);
  this.changed('collectionName', id, fields);
  this.removed('collectionName', id);
  
  // Clean up when subscription stops
  this.onStop(() => {
    console.log('Subscription stopped');
  });
  
  // Check if subscription is still active
  if (this._isDeactivated()) {
    return;
  }
});

Subscribing to Publications

Basic Subscription

// Client-side
const handle = Meteor.subscribe('lists.public');

// Check if ready
Tracker.autorun(() => {
  if (handle.ready()) {
    console.log('Subscription ready');
    const lists = Lists.find().fetch();
  }
});

// Stop subscription when done
handle.stop();

Subscription with Parameters

const listId = '123abc';
const handle = Meteor.subscribe('todos.inList', listId);

Reactive Subscriptions in Blaze

Template.TodoList.onCreated(function() {
  this.autorun(() => {
    const listId = FlowRouter.getParam('_id');
    this.subscribe('todos.inList', listId);
  });
});

Template.TodoList.helpers({
  todos() {
    return Todos.find();
  },
  isLoading() {
    return !Template.instance().subscriptionsReady();
  }
});

Subscriptions in React

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>
  );
}

Low-Level Publish API

For complete control over published data:
Meteor.publish('custom.stream', function() {
  const self = this;
  let counter = 0;
  
  // Add initial document
  self.added('stream', 'counter', { value: counter });
  self.ready();
  
  // Update every second
  const interval = setInterval(() => {
    counter++;
    self.changed('stream', 'counter', { value: counter });
  }, 1000);
  
  // Clean up on stop
  self.onStop(() => {
    clearInterval(interval);
  });
});

Publishing from External Data Sources

Meteor.publish('external.weather', function(city) {
  check(city, String);
  
  const self = this;
  let stopped = false;
  
  // Fetch from external API
  async function updateWeather() {
    if (stopped) return;
    
    try {
      const response = await fetch(`https://api.weather.com/${city}`);
      const data = await response.json();
      
      self.added('weather', city, data);
      self.ready();
    } catch (error) {
      self.error(new Meteor.Error('api-error', error.message));
    }
  }
  
  updateWeather();
  
  // Update every 10 minutes
  const interval = setInterval(updateWeather, 10 * 60 * 1000);
  
  self.onStop(() => {
    stopped = true;
    clearInterval(interval);
  });
});

Security and Validation

Always Validate Parameters

import { check, Match } from 'meteor/check';

Meteor.publish('todos.search', function(query, options) {
  check(query, String);
  check(options, Match.Optional({
    limit: Match.Optional(Number),
    sort: Match.Optional(Object)
  }));
  
  // Sanitize options
  const limit = Math.min(options?.limit || 20, 100);
  
  return Todos.find(
    { 
      text: { $regex: query, $options: 'i' },
      userId: this.userId 
    },
    { limit, sort: options?.sort || { createdAt: -1 } }
  );
});

Check User Permissions

Meteor.publish('admin.users', function() {
  // Check if user is admin
  if (!Roles.userIsInRole(this.userId, 'admin')) {
    // Return empty result
    return this.ready();
    // Or throw error
    // throw new Meteor.Error('not-authorized');
  }
  
  return Meteor.users.find({}, {
    fields: { emails: 1, profile: 1, roles: 1 }
  });
});

Field Filtering

Never publish sensitive fields like password hashes or API tokens.
Meteor.publish('users.profile', function(userId) {
  check(userId, String);
  
  return Meteor.users.find(
    { _id: userId },
    {
      fields: {
        username: 1,
        'profile.name': 1,
        'profile.avatar': 1,
        // Exclude sensitive fields:
        // services: 0,
        // 'emails.address': 0
      }
    }
  );
});

Publication Strategies

Different strategies for different use cases:

SERVER_MERGE (Default)

Server maintains a copy of all subscribed data:
// Good for: Most publications
// Pro: Efficient delta updates
// Con: Uses server memory

Meteor.publish('todos', function() {
  return Todos.find({ userId: this.userId });
});

NO_MERGE

No delta tracking, just ID tracking:
// Good for: Single-use publications
// Pro: Less server memory
// Con: No delta updates

Meteor.publish('logs', function() {
  return Logs.find(
    { userId: this.userId },
    { 
      strategy: 'NO_MERGE',
      limit: 100 
    }
  );
});

Reactive Joins

Using reywood:publish-composite:
import { publishComposite } from 'meteor/reywood:publish-composite';

publishComposite('lists.withTodos', function(listId) {
  check(listId, String);
  
  return {
    find() {
      return Lists.find(
        { _id: listId, userId: this.userId },
        { limit: 1 }
      );
    },
    children: [
      {
        find(list) {
          return Todos.find({ listId: list._id });
        }
      },
      {
        find(list) {
          return Meteor.users.find(
            { _id: list.userId },
            { fields: { profile: 1, username: 1 } }
          );
        }
      }
    ]
  };
});

Counting Records

Using tmeasday:publish-counts:
import { Counts } from 'meteor/tmeasday:publish-counts';

Meteor.publish('todos.count', function(listId) {
  check(listId, String);
  
  Counts.publish(
    this,
    'todos.count.' + listId,
    Todos.find({ listId })
  );
});

// Client-side
const count = Counts.get('todos.count.' + listId);

Performance Optimization

Use Indexes

// Ensure MongoDB indexes match publication queries
Todos.createIndex({ userId: 1, listId: 1 });
Todos.createIndex({ createdAt: -1 });

Meteor.publish('todos.recent', function() {
  // This query uses indexes efficiently
  return Todos.find(
    { userId: this.userId },
    { sort: { createdAt: -1 }, limit: 20 }
  );
});

Limit Published Data

Meteor.publish('todos.paginated', function(page = 1, limit = 20) {
  check(page, Number);
  check(limit, Number);
  
  // Enforce maximum limit
  limit = Math.min(limit, 100);
  
  return Todos.find(
    { userId: this.userId },
    {
      sort: { createdAt: -1 },
      limit,
      skip: (page - 1) * limit
    }
  );
});

Use Field Projection

Meteor.publish('todos.list', function() {
  // Only send needed fields
  return Todos.find(
    { userId: this.userId },
    {
      fields: {
        text: 1,
        checked: 1,
        createdAt: 1
        // Don't send large fields like descriptions, attachments
      }
    }
  );
});

Testing Publications

import { assert } from 'chai';
import { PublicationCollector } from 'meteor/johanbrook:publication-collector';
import './publications';

if (Meteor.isServer) {
  describe('todos.inList', function() {
    it('publishes todos for a list', function(done) {
      const collector = new PublicationCollector({ userId: 'testUser' });
      
      collector.collect('todos.inList', 'testList', (collections) => {
        assert.equal(collections.todos.length, 3);
        assert.equal(collections.todos[0].listId, 'testList');
        done();
      });
    });
    
    it('requires authentication', function(done) {
      const collector = new PublicationCollector();
      
      collector.collect('todos.private', (collections) => {
        assert.equal(collections.todos, undefined);
        done();
      });
    });
  });
}