Meteor Methods: The Secure Path for Client-Initiated Database Writes
Meteor Methods provide a secure server-side path for client writes. Learn how to define handlers, validate arguments, authorize with this.userId, and avoid common security mistakes.
04 Feb 2026, 03:59 UTC

If a Meteor client can write directly to a collection, any user can modify any document. The supported fix is a Meteor Method: a named server-side function that the client invokes over DDP (Distributed Data Protocol). The server copy is authoritative, arguments are validated, and authorization is checked against the authenticated session rather than client-supplied fields.
How Meteor Methods work
A Meteor Method is a remote procedure call. You define it with Meteor.methods() on the server, and the client invokes it with Meteor.callAsync() (or Meteor.call() with a callback on older releases). Because the handler runs on the server, it can trust this.userId—the id of the logged-in user making the call—and reject unauthorized requests before any database work happens.
Methods are not REST endpoints. Arguments and return values must be JSON-serializable, there is no streaming, and calls are subject to DDP rate limiting through DDPRateLimiter.
A worked insert example
Suppose you want to let a logged-in user create a task. The server handler validates the argument, checks the session, and inserts a document whose owner comes from the server-side session, never from the client.
// File: server/methods.js
Meteor.methods({
async 'tasks.insert'(text) {
check(text, String);
if (!this.userId) {
throw new Meteor.Error('not-authorized', 'You must be logged in to create a task');
}
return TasksCollection.insertAsync({
text,
createdAt: new Date(),
owner: this.userId
});
}
});
The check call rejects malformed input before any database work. The owner field is taken from the authenticated session, so a forged argument cannot assign ownership to another user.
On the client, call the method and await the result:
// File: client/task-form.js
const id = await Meteor.callAsync('tasks.insert', 'Buy milk');
console.log('Created task with _id:', id);
callAsync returns a Promise, so it composes with async/await. The resolved value is whatever the server method returns—in this case the new document _id. On older Meteor versions that lack callAsync, use Meteor.call with a callback instead.
Latency compensation with client stubs
To make the UI feel instant, register a client-only stub with the same method name. The stub runs immediately in the browser, performs the same Minimongo insert, and returns a temporary _id. When the server result arrives, Meteor reconciles the stub with the authoritative document.
// File: client/methods.js
if (Meteor.isClient) {
Meteor.methods({
'tasks.insert'(text) {
check(text, String);
return TasksCollection.insert({
text,
createdAt: new Date(),
owner: Meteor.userId()
});
}
});
}
Keep stubs deterministic and free of side effects such as emails or HTTP calls, because a stub may run without ever reaching the server. Authorization and secret logic belong only in the server handler.
Limits and common mistakes
Several mistakes turn a secure method into a security or consistency bug:
- Shipping with
insecureandautopublishinstalled. These legacy packages let any client write any collection and publish every document. Runmeteor listin your project directory and confirm both are absent from the production app. - Trusting client-supplied fields. Never accept an
owner,role, orpriceargument and write it directly. Derive sensitive fields fromthis.userIdor server-side logic. - Throwing plain
Errorobjects. UseMeteor.Errorso the client can distinguish error types via theerrorandreasonfields. A plainErroris opaque to the caller. - Omitting
this.unblock()on long-running methods. Without it, subsequent method calls from the same client wait for the current one to finish, needlessly blocking the UI. - Returning whole server documents. A method return value is sent to the client. Map the result to a client-safe shape before returning it so private fields are not exposed.
check validates argument shape and type, but it is not a full schema validator. For nested objects or cross-field rules, use a schema package such as aldeed:simple-schema or zod.
Promise-based helpers such as Meteor.callAsync and insertAsync are version-sensitive. Verify they exist in your Meteor release before relying on them; older projects use callback-style collection operations and Meteor.call.
How to verify the setup
Scaffold the method, call it from the client, and confirm the server log shows the handler executing with the expected this.userId. Add a temporary check failure or unauthorized throw and confirm the client receives a Meteor.Error with the expected error and reason fields.
Inspect the DDP traffic in browser devtools to confirm the method call and its arguments, then verify the returned _id matches the inserted document. Finally, write a server-only test that calls the method with a forged owner argument and assert the stored document owner equals this.userId, not the argument.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.