# Meteor Pubsub

> Use when authoring or debugging Meteor publications and subscriptions (Meteor.publish, Meteor.subscribe). Triggers on publication strategies (SERVER_MERGE, NO_MERGE, NO_MERGE_NO_HISTORY), the low-level publish API (added/changed/removed), cursor authorization, reactive joins, leaked documents. Use this skill when the user asks about pub/sub or asks about reactive data fetching.

- Skill: `meteor/meteor-pubsub` (Agent Skill, multi-file: 4 files)
- Install (CLI): `npx skillmds@latest add meteor/meteor-pubsub`
- Raw SKILL.md: https://api.skillmd.com/api/skills/meteor/meteor-pubsub/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Integrations & APIs
- License: MIT
- Author: meteor (https://skillmd.com/u/meteor)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/meteor/meteor-pubsub

---


# Meteor publications and subscriptions

Publications stream a set of documents to subscribed clients and keep them
live. The publication is the only place server-side authorization can
filter rows before they reach the client.

## Decision flow

1. Does the client need this data reactively? If no, prefer a method (one-shot
   read). If yes, use a publication.
2. Is the data user-specific? Filter by `this.userId` inside the publish
   function. Without that filter, documents leak across users.
3. Can the publication be expressed as a single cursor? Return it directly.
4. Does it need one async lookup before choosing a cursor? Use an async
   publish handler, await the lookup, then return the cursor.
5. Does it need per-document async joins, custom aggregation output, or an
   external reactive source? Drop to the low-level `this.added` /
   `this.changed` / `this.removed` API.

## Scaffold

```javascript
import { Meteor } from "meteor/meteor";
import { Items } from "/imports/api/items";

Meteor.publish("items.mine", function () {
  if (!this.userId) {
    return this.ready();
  }
  return Items.find(
    { ownerId: this.userId },
    { fields: { title: 1, qty: 1, updatedAt: 1 }, sort: { updatedAt: -1 } },
  );
});
```

Project `fields` whenever the collection contains columns the subscriber
must not receive.

Async publish handlers may also return a cursor:

```javascript
Meteor.publish("items.byTeam", async function (teamId) {
  const member = await Memberships.findOneAsync({
    teamId,
    userId: this.userId,
  });
  if (!member) return this.ready();
  return Items.find({ teamId }, { fields: { title: 1, qty: 1 } });
});
```

Meteor awaits the handler before processing the returned cursor.

## Subscribing

```javascript
import { Meteor } from "meteor/meteor";

const handle = Meteor.subscribe("items.mine");
// React / Blaze / Svelte hooks observe handle.ready() and the local cursor.
handle.stop();
```

## Publication strategies

Set per-collection with `Meteor.server.setPublicationStrategy`. Three options:

```javascript
import { DDPServer } from "meteor/ddp-server";

Meteor.server.setPublicationStrategy(
  "items",
  DDPServer.publicationStrategies.NO_MERGE,
);
```

| Strategy             | When to use                                                                 |
|----------------------|-----------------------------------------------------------------------------|
| `SERVER_MERGE`       | Default. Tracks merged document fields across publications and sends deltas. |
| `NO_MERGE`           | Tracks sent document IDs so unsubscribe can remove them. Use when the collection is owned by one publication. |
| `NO_MERGE_NO_HISTORY`| Remembers nothing and sends no removals on stop. Reserve for send-and-forget queues where stale client documents are intentional. |

See `references/publication-strategies.md`.

## Low-level publish API

For joins or async work:

```javascript
Meteor.publish("feed", async function () {
  const cursor = Posts.find({}, { fields: { title: 1, authorId: 1 } });
  const observer = await cursor.observeChangesAsync({
    added: async (id, doc) => {
      const author = await Users.findOneAsync(doc.authorId, {
        fields: { username: 1 },
      });
      this.added("feed", id, { ...doc, authorName: author?.username });
    },
    changed: (id, changes) => this.changed("feed", id, changes),
    removed: (id) => this.removed("feed", id),
  });

  this.ready();
  this.onStop(() => observer.stop());
});
```

Note: cursor `transform` functions remain synchronous. Use a separate
publication or the low-level API for per-document async joins.

Live observer delivery does not wait for one async callback before later
changes arrive. If ordering or backpressure matters, serialize the work with a
per-subscription Promise queue. Attach an error policy and stop the observer
from `this.onStop`; see `references/low-level-publish-api.md`.

## Anti-patterns

- Publish without a `this.userId` filter when data is user-specific.
- Publish sensitive or unnecessary columns. Add a `fields` projection when
  the full document is not part of the publication contract.
- Confuse an async publish handler with an async cursor transform. Meteor
  awaits the handler Promise, but each transform callback must return a
  document synchronously.
- Use cursor transforms for async joins. Transforms are synchronous; the
  low-level API is the right tool.
- Subscribe to large unbounded collections. Page the data with `limit`/`skip`.

## See also

- `references/publication-strategies.md`
- `references/low-level-publish-api.md`
- `references/eval-cases.md`

