Route Meteor writes through server methods
Use Meteor Methods for all database writes instead of client-side collection calls to enforce validation and authorization.
08 Oct 2025, 22:13 UTC

Problem: client-side writes bypass your business logic
In a default Meteor application, a `Mongo.Collection` is available on both the server and the client. A user can open browser DevTools and call `Tasks.insert({ text: 'injected' })` directly. If the collection has permissive `allow`/`deny` rules or none at all, the write reaches MongoDB without server validation. The useful takeaway is to treat the client as untrusted: route every create, update, and remove operation through a server Method, and keep client code limited to reading reactive data and rendering UI.
How Meteor methods and publications work
Meteor uses DDP (Distributed Data Protocol) to send method calls and subscription messages between client and server. A Method is a server function registered with `Meteor.methods` that the client invokes with `Meteor.call`. The server validates arguments, checks authentication, and performs the database write. A publication selects which documents the server pushes to a subscribed client; the client stores them in Minimongo, Meteor's client-side cache, and updates the UI reactively.
Worked configuration: guarded task creation
Define the collection in a shared module so both client and server import the same name.
import { Mongo } from 'meteor/mongo';
export const Tasks = new Mongo.Collection('tasks');
Register the write path on the server only.
import { Meteor } from 'meteor/meteor';
import { check } from 'meteor/check';
import { Tasks } from '../imports/api/tasks';
Meteor.methods({
'tasks.insert'(text) {
check(text, String);
if (!this.userId) {
throw new Meteor.Error('not-authorized', 'Log in to create tasks');
}
if (!text || text.length > 200) {
throw new Meteor.Error('invalid-argument', 'Task text is required and must be under 200 characters');
}
Tasks.insert({
text,
userId: this.userId,
createdAt: new Date(),
});
},
});
Publish only the documents the current user should see.
import { Meteor } from 'meteor/meteor';
import { Tasks } from '../imports/api/tasks';
Meteor.publish('tasks', function () {
if (!this.userId) {
return this.ready();
}
return Tasks.find({ userId: this.userId });
});
Call the method from the client instead of touching the collection directly.
import { Meteor } from 'meteor/meteor';
import { Tasks } from '../imports/api/tasks';
Meteor.subscribe('tasks');
export function createTask(text) {
Meteor.call('tasks.insert', text, (error) => {
if (error) {
console.error(error.reason);
return;
}
});
}
The client never calls `Tasks.insert`. After the server method completes, the publication pushes the new document to subscribed clients and the UI updates through the reactive data stream.
Where to run and what to check
- Server files: run in the Meteor server bundle; require ordinary filesystem access, not root. Do not place method definitions in client-only directories.
- Development server: run `meteor` from the project root. Risk: the dev server binds to a local port and is not hardened for internet exposure.
- Expected check: open browser DevTools, run `Meteor.call('tasks.insert', 'hello')` in the console, and confirm the server log shows the method invocation and the document appears in the client collection.
- Security check: inspect the Network panel for DDP `method` messages and confirm no `insert` operations originate from client collection calls.
Limits and common mistakes
- Methods are not automatically reactive. A Method writes to the database, but clients only see the change if a matching publication is active. Pair writes with `Meteor.publish` / `Meteor.subscribe`.
- Client-side validation is not enforcement. Checking field length in the UI improves experience, but an attacker can bypass it. Validation must run inside the Method.
- Skipping `check`. Without argument validation, malformed or unexpected types can reach the database or cause server errors.
- Missing authentication checks. `this.userId` is undefined for unauthenticated calls. Always verify it inside Methods that modify user-owned data.
- Over-subscribing. Publishing large collections to every client increases DDP traffic and memory use. Publish filtered subsets by user or scope.
Practical verification
- Start the Meteor app and open two browser windows logged in as different users.
- In window A, call `Meteor.call('tasks.insert', 'visible only to owner')`.
- Confirm window A shows the task reactively.
- Confirm window B does not show the task because the publication filters by `userId`.
- Attempt `Tasks.insert({ text: 'direct write' })` from the console; it should have no effect or throw because the client should not have direct insert privileges in this pattern.
- Review server console output for method entry and any thrown `Meteor.Error` messages.
Version assumption: this guide targets Meteor applications using the default `meteor` package set and `Mongo.Collection`. Custom authentication, GraphQL layers, or alternate database adapters may require different implementation details and should be reviewed separately.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.