docs/Upgrade/METEOR3_MIGRATION.md
Reference document capturing patterns, constraints, and lessons learned during the async migration of WeKan from Meteor 2.16 toward Meteor 3.0 readiness.
WeKan runs on Meteor 2.16 with Blaze 2.x. The goal is dual compatibility: changes must work on 2.16 now and remain compatible with a future Meteor 3.0 upgrade.
Key constraint: Blaze 2.x does NOT support async template helpers. Client-side code must receive synchronous data.
ReactiveCache dispatches to ReactiveCacheServer (async MongoDB) or ReactiveCacheClient (sync Minimongo).
Rule: Facade methods must NOT be async. They return a Promise on the server and data on the client. Server callers await; client code uses the return value directly.
// CORRECT:
getBoard(boardId) {
if (Meteor.isServer) {
return ReactiveCacheServer.getBoard(boardId); // Returns Promise
} else {
return ReactiveCacheClient.getBoard(boardId); // Returns data
}
}
// WRONG:
async getBoard(boardId) { ... } // Wraps client return in Promise too!
Model helpers defined via Collection.helpers({}) are used by Blaze templates. They must NOT be async.
// CORRECT:
Cards.helpers({
board() {
return ReactiveCache.getBoard(this.boardId); // Promise on server, data on client
},
});
// WRONG:
Cards.helpers({
async board() { // Blaze gets Promise instead of data
return await ReactiveCache.getBoard(this.boardId);
},
});
Server-side callers of these helpers must await the result:
// In a Meteor method or hook (server-only):
const board = await card.board();
Meteor 2.x evaluates allow/deny callbacks synchronously. An async callback returns a Promise:
Rule: Never use async in allow/deny. Replace ReactiveCache calls with direct sync Mongo calls.
// CORRECT:
Cards.allow({
insert(userId, doc) {
return allowIsBoardMemberWithWriteAccess(userId, Boards.findOne(doc.boardId));
},
fetch: ['boardId'],
});
// WRONG:
Cards.allow({
async insert(userId, doc) {
return allowIsBoardMemberWithWriteAccess(userId, await ReactiveCache.getBoard(doc.boardId));
},
});
| Async (broken in allow/deny) | Sync replacement |
|---|---|
await ReactiveCache.getBoard(id) | Boards.findOne(id) |
await ReactiveCache.getCard(id) | Cards.findOne(id) |
await ReactiveCache.getCurrentUser() | Meteor.users.findOne(userId) |
await ReactiveCache.getBoards({...}) | Boards.find({...}).fetch() |
await card.board() | Boards.findOne(card.boardId) |
Note: These sync Mongo calls (findOne, find().fetch()) are available in Meteor 2.x. In Meteor 3.0, they will be replaced by findOneAsync / find().fetchAsync(), which will require allow/deny callbacks to be reworked again (or replaced by Meteor 3.0's new permission model).
Code that runs exclusively on the server can safely use async/await:
Meteor.methods({}) — method bodiesMeteor.publish() — publication functionsJsonRoutes.add() — REST API handlersCollection.before.* / Collection.after.* — collection hooks (via matb33:collection-hooks)Meteor.methods({
async createCard(data) {
const board = await ReactiveCache.getBoard(data.boardId); // OK
// ...
},
});
Array.forEach() does not handle async callbacks — iterations run concurrently without awaiting.
// WRONG:
items.forEach(async (item) => {
await processItem(item); // Runs all in parallel, not sequentially
});
// CORRECT:
for (const item of items) {
await processItem(item); // Runs sequentially
}
Meteor requires client-side collection updates to use _id as the selector:
// CORRECT:
Lists.updateAsync(listId, { $set: { title: newTitle } });
// WRONG - fails with "Untrusted code may only update documents by ID":
Lists.updateAsync({ _id: listId, boardId: boardId }, { $set: { title: newTitle } });
These Meteor 2.x sync APIs will need conversion when upgrading to Meteor 3.0:
| Meteor 2.x (sync) | Meteor 3.0 (async) |
|---|---|
Collection.findOne() | Collection.findOneAsync() |
Collection.find().fetch() | Collection.find().fetchAsync() |
Collection.insert() | Collection.insertAsync() |
Collection.update() | Collection.updateAsync() |
Collection.remove() | Collection.removeAsync() |
Collection.upsert() | Collection.upsertAsync() |
Meteor.user() | Meteor.userAsync() |
Meteor.userId() | Remains sync |
Current status: Server-side code already uses async patterns via ReactiveCache. The sync findOne() calls in allow/deny callbacks will need to be addressed when Meteor 3.0's allow/deny system supports async (or is replaced).
Key files involved in the async migration:
| File | Role |
|---|---|
imports/reactiveCache.js | ReactiveCache facade + Server/Client/Index implementations |
server/lib/utils.js | Permission helper functions (allowIsBoardMember*) |
models/*.js | Collection schemas, helpers, allow/deny, hooks, methods |
server/publications/*.js | Meteor publications |
server/rulesHelper.js | Rule trigger/action evaluation |
server/cronMigrationManager.js | Cron-based migration jobs |
wekan-fullcalendar is currently migrated from legacy Meteor package globals to npm-based FullCalendar 5.11.5 to keep Meteor 2.16 and 3.0 dual compatibility stable.
Why pinned for now:
momentjs:moment Meteor package dependency.After Meteor 3.0 lands (recommended follow-up):