
Description
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.md
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
- Does the client need this data reactively? If no, prefer a method (one-shot read). If yes, use a publication.
- Is the data user-specific? Filter by
this.userIdinside the publish function. Without that filter, documents leak across users. - Can the publication be expressed as a single cursor? Return it directly.
- Does it need one async lookup before choosing a cursor? Use an async publish handler, await the lookup, then return the cursor.
- 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.removedAPI.
Scaffold
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:
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
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:
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:
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.userIdfilter when data is user-specific. - Publish sensitive or unnecessary columns. Add a
fieldsprojection 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.mdreferences/low-level-publish-api.mdreferences/eval-cases.md
More skills from the agent-skills repository
View all 14 skillsmeteor-accounts
implement authentication in Meteor apps
Aug 28AuthAuthenticationMeteorOAuthmeteor-blaze
build and debug Meteor Blaze interfaces
Aug 28FrontendMeteorWeb Developmentmeteor-community-packages
manage Meteor community packages
Aug 28EngineeringMeteormeteor-debugging
diagnose failures in Meteor 3 applications
Aug 28DebuggingMeteorWebSocketsmeteor-deployment
deploy Meteor 3 applications
Aug 28DeploymentDockerKubernetesMeteormeteor-methods
author and debug Meteor methods
Aug 28API DevelopmentBackendMeteor
More from Meteor
View publishermeteor-modern-build-stack
configure Meteor 3 modern build stacks
agent-skills
Aug 28BuildMeteorPerformancemeteor-mongo-minimongo
author and debug Meteor MongoDB queries
agent-skills
Aug 28DatabaseDebuggingMeteorMongoDBmeteor-react
build and debug Meteor React interfaces
agent-skills
Aug 28FrontendMeteorReactWeb Developmentmeteor-security
audit and harden Meteor 3 applications
agent-skills
Aug 28AuthCode AnalysisMeteorSecuritymeteor-testing
write and repair Meteor test harnesses
agent-skills
Aug 28MeteorQATesting