Hello community!
I recently pushed a PR that introduces a dedicated package, accounts-express, for authenticating Express endpoints defined through Meteor’s WebApp package. It makes it possible to protect specific REST endpoints in a way similar to how DDP endpoints like methods and subscriptions already work, and it also ships an auth-aware fetch so the client (and the server) can call those endpoints with the user’s token attached.
The final shape splits cleanly into two pieces: an auth middleware for protecting endpoints, and an authenticated fetch for calling them.
Auth middleware
createAuthMiddleware reads a Meteor login token from the Authorization: Bearer header (or the meteor_login_token cookie when HttpOnly cookies are enabled) and populates req.userId plus Meteor.userId() for the rest of the chain.
As an example, on the server you can define the following:
import { WebApp } from 'meteor/webapp';
import { createAuthMiddleware } from 'meteor/accounts-express';
// Apply auth middleware to /api routes
WebApp.handlers.use('/api', createAuthMiddleware());
// Protected endpoint under /api
WebApp.handlers.use('/api/users', async (req, res) => {
try {
// User context is available
console.log('Authenticated user ID:', Meteor.userId(), req.userId);
console.log('Authenticated user:', await Meteor.userAsync());
if (!Meteor.userId()) {
res.status(401).json({ error: 'Unauthorized' });
return;
}
const users = await Meteor.users
.find({}, { fields: { username: 1, createdAt: 1, updatedAt: 1 } })
.fetchAsync();
res.json(users);
} catch (error) {
console.log(error);
res.status(500).json({ error: error.message });
}
});
By default, createAuthMiddleware does not require authentication, but it can detect the logged-in user. This is closer to how methods and subscriptions handle authorization logic, as it lets you customize authorization once you have access to the userId that attempted to reach the endpoint.
If you want to protect endpoints unconditionally, you can apply the middleware with createAuthMiddleware({ required: true }), which will reject unauthenticated requests automatically:
WebApp.handlers.use('/api/admin', createAuthMiddleware({ required: true }));
Authenticated fetch
On the client side, three fetch entry points become available so you can choose how aggressively the login token is attached:
| Entry point | Default | When to reach for it |
|---|---|---|
Meteor.fetch |
auth: false (opt-in) |
Neutral fetch that becomes auth-aware when you pass auth: true. |
import { fetch } from 'meteor/fetch' |
auth: false (opt-in) |
Generic fetch shim. Stays neutral so installing accounts-express doesn’t silently start attaching the user’s token to arbitrary URLs. |
import { fetch } from 'meteor/accounts-express' |
auth: true (opt-out) |
Auth-on-by-default. Pass auth: false to opt out for a single call. |
The asymmetry is deliberate: importing from meteor/accounts-express is the opt-in. The other two entry points are shared with non-auth code paths and stay neutral until you ask for auth.
So the protected call from the example becomes:
import { fetch } from 'meteor/accounts-express';
async function getUsers() {
const response = await fetch('/api/users');
return response.json();
}
console.log(await getUsers());
When logged in and authorized the console logs:
[
{
"_id": "Ystii57EhB6aFyXup",
"createdAt": "2026-01-20T16:11:09.406Z",
"username": "meteorite"
}
]
When logged out, given the error handled within the endpoint:
{
"error": "Unauthorized"
}
If you want full control over which calls carry the token, prefer the opt-in form:
import { fetch } from 'meteor/fetch';
const protectedRes = await Meteor.fetch('/api/users', { auth: true });
const alsoProtected = await fetch('/api/users', { auth: true });
const publicRes = await Meteor.fetch('/api/public');
On the server you can also pass an explicit { token } to call an endpoint as a specific user, instead of relying on the current request context.
Documentation:
Coming next: REST login/logout endpoints
The next iteration introduces drop-in factories createLoginMiddleware and createLogoutMiddleware that turn the password login flow into proper REST endpoints, issuing the same login tokens DDP login produces, and firing the standard validateLoginAttempt / onLogin / onLoginFailure / onLogout hooks.
I opened this forum discussion because, even though the implementation is done, covered by tests, and already used for a long time in my own projects, I would like feedback on the developer experience when moving it to the core. The goal is to discuss details early and make decisions together.
This is an experimental feature planned for a future minor release, likely Meteor 3.5+ (with a beta launched as soon as possible). I think it is worth bringing up now and discussing it ahead of time, so the deliver can coverage all planned usage scenarios.
