Wekan logo

Wekan

Open source kanban board application built with Meteor

Alternative to: trello


About Versions (307)

v11.49

2026-09-04

Binaries in these bundles

Each bundle carries a Node.js, a FerretDB and the MongoDB Database Tools. Which source has a given CPU varies from release to release - nodejs.org builds some architectures, unofficial-builds others, and the wekan/node-patches build the ones neither of them does - and not every source publishes a checksum. This is what went into this release, and which downloads were checked against a published SHA256.

BundleBinaryFromVersionCheckedSHA256
amd64FerretDBwekan/FerretDBv1.67.0verifiedbe7a7c3a7b83dee0…
amd64Node.jsnodejs.orgv24.20.0verified2f2c0da162318f0d…
arm64FerretDBwekan/FerretDBv1.67.0verified8f0b1df0fdff9f26…
arm64Node.jsnodejs.orgv24.20.0verified5f4ddab610c1ab20…
armhfFerretDBwekan/FerretDBv1.67.0verified4e35884b7e826048…
armhfNode.jswekan/node-patchesv24.20.0verifiedb8ed7065d44f0afe…
armv6FerretDBwekan/FerretDBv1.67.0verifiedfc657a2929becd70…
armv6Node.jswekan/node-patchesv24.20.0verifiedd5cefa6f8cc4acb1…
armv7FerretDBwekan/FerretDBv1.67.0verified4e35884b7e826048…
armv7Node.jswekan/node-patchesv24.20.0verifiedc04c81e539347f39…
i386FerretDBwekan/FerretDBv1.67.0verified6e51a7278f850f82…
i386Node.jswekan/node-patchesv24.20.0verifiedbb44927307460dcf…
mac-arm64FerretDBwekan/FerretDBv1.67.0verified9478ab9f907a4106…
mac-arm64Node.jsnodejs.orgv24.20.0verifiedb7bf7707070b950b…
mac-x64FerretDBwekan/FerretDBv1.67.0verified9a052beb1c9d0324…
mac-x64Node.jsnodejs.orgv24.20.0verified26fc30891004603d…
ppc64leFerretDBwekan/FerretDBv1.67.0verified8d96ed0624e0a637…
ppc64leNode.jsnodejs.orgv24.20.0verified341307dcee20d883…
riscv64FerretDBwekan/FerretDBv1.67.0verified791b9294ffb1f48f…
riscv64Node.jsunofficial-builds.nodejs.orgv24.20.0verifieda149c5bf85f98ff1…
s390xFerretDBwekan/FerretDBv1.67.0verified2914f0e00c0d0b5d…
s390xNode.jsnodejs.orgv24.20.0verifiedca381121cb5a8d38…
win-arm64FerretDBwekan/FerretDBv1.67.0verifiedd00ed8f3c5300983…
win-arm64Node.jsnodejs.orgv24.20.0verified31c6799744de8a54…
win64FerretDBwekan/FerretDBv1.67.0verified7e559755fa5b38ce…
win64Node.jsnodejs.orgv24.20.0verified6cac9ffbca8f6a47…

A row saying no checksum published is not a failed check - it is a source that publishes nothing to check against. Those are the ones worth fixing at the source.

v11.49 2026-09-04 WeKan ® release

In short: the single Windows EXE stops running WeKan out of a closed-source virtual filesystem, and stops unpacking the bundle instead. It carries the published win64 ZIP as a checksummed payload, unpacks only the fifteen or so files Windows itself must open, and mounts the remaining ~39,000 in the server process. That ends the crash loop 11.48 shipped with, cuts the download from 690 MB to about 232 MB, and turns a damaged copy into a clear message rather than a restarting server. bundle-trim drops the native prebuilds no bundle can open, and moving a list to a swimlane now binds it there instead of silently doing nothing.

PlatformBinaryFromVersionSHA256
amd64Node.jsnodejs.orgv24.19.014b342e71204f811bde6153be8e04b62aef63c236fef92b55f9c83154b409647
amd64FerretDBwekan/FerretDBv1.53.0eae1f0a8f73bfc979738bfff7284d40fd1bc55de2cc56514721fc155c3624f7d
arm64Node.jsnodejs.orgv24.19.001443c1e1a29e531ccad5a46fefa6df490d2189c49f7955904aecdbb0fe86fdc
arm64FerretDBwekan/FerretDBv1.53.0bdc50caee3ac28495b42d2130b94a042a9dd6d3a38f732cac02b648f36c891da
mac-arm64Node.jsnodejs.orgv24.19.03f1cf157479c1480352083105e13faf9d008ede98e7e157746b6df940d197b94
mac-arm64FerretDBwekan/FerretDBv1.53.0cb14ffe93e285903e5a8a9c1821687ddb5b8a979a11c584bf4af534b272c6d3e
mac-x64Node.jsnodejs.orgv24.19.0d35e95230f46f6f0751df497c56622c6735e05d5e1fb1630996a005b9d328fe4
mac-x64FerretDBwekan/FerretDBv1.53.0d97dfa9afa60aa05f25384327de82efe7b71d958ed24c1f66618284294a65cd3

This release fixes the following bugs:

The single Windows EXE - what the one downloadable file is made of, what it checks before it starts WeKan, and how little of it ever reaches the disk.

It carries a checksummed copy of the win64 ZIP instead of a virtual filesystem. Thanks to xet7.

WeKan-11.48-win64.exe died on every start, restarted, and died again:

...\accounts-password\node_modules\bcrypt\promises.js:1
MZ......  !.L.!This program cannot be run in DOS mode.
SyntaxError: Invalid or unexpected token
    at Object.<anonymous> (...\bcrypt\bcrypt.js:6:18)

Both published files were correct. wekan-11.48-win64.zip holds the real 1123-byte promises.js, and so did the EXE: in its packed image bcrypt.node (195584 bytes) is entry 0x5c9c and promises.js is 0x5c9d, stored back to back, each with the right bytes at the offset its own record gives. What was wrong was the READ. Enigma Virtual Box served all 44,401 bundle files from a virtual filesystem inside the EXE, and once Node.js had loaded the native addon out of it (bcrypt.js line 2), the next read - require('./promises') on line 6, the blob immediately after that addon - came back as the addon’s own PE bytes.

So that packer is gone. The EXE is now the compiled launcher with the published ZIP appended to it and an 80-byte trailer saying where that payload starts, how long it is, its SHA-256 and which WeKan it is. The download drops from 690 MB to about 232 MB, because the payload is the compressed ZIP rather than an uncompressed image, and a damaged copy - a truncated download, a half-written file - now stops at the checksum with the expected and actual hashes and a line saying to download it again, instead of reaching Node.js as a crash loop.

Its release smoke test is why a broken EXE was published at all. start-wekan.bat restarts WeKan every three seconds when it exits, and the smoke test polled http://localhost:8080/sign-in for three minutes: an EXE that crashed on nine starts out of ten still answered inside that window, and the job went green. It now starts the EXE twice - the run that unpacks and the run that must find its files already there - and fails if either log contains WeKan exited; restarting or SyntaxError. It also no longer trips over PowerShell’s read-only $pid while freeing the ports FerretDB needs.

tests/windowsSingleExe.test.cjs pins the trailer format from both ends - every TRAILER_* the launcher defines must equal the constant releases/append-windows-payload.mjs exports under that name - round-trips a packed file through pack and verify, and requires a flipped byte, a truncated payload, a missing trailer and an over-long version to be refused. A negative test reads every file under .github/workflows and releases and fails on any Enigma Virtual Box download, console or .evb project, so that virtual filesystem cannot come back through a second packing path.

It reads the bundle from inside itself instead of unpacking 39,035 files. Thanks to xet7.

Replacing the packer left the EXE writing its whole bundle - 39,035 files, 685 MB - beside itself on the first run. It does not any more.

Only what cannot be virtual is unpacked, and the reason is the same in every case: something other than WeKan’s own module loader has to open it. node.exe and ferretdb.exe are separate processes, so Windows needs a real path to start one; .node addons are loaded with LoadLibrary, same reason; main.js is the entry Node resolves before any hook could see it; start-wekan.bat is read by cmd.exe; and wekan-vfs.cjs is read by node --require before anything is mounted. main.js also chdir()s into programs/server, and a working directory is a kernel concept no hook can answer, so that one directory is created for real. That is about thirty files out of 39,035, and the list is computed from the archive rather than written by hand, so a bundle that gains an addon or a database tool cannot silently lose it.

The mount is releases/single-exe/wekan-vfs.cjs, and it needs two mechanisms because Node needs both. module.registerHooks() answers require: the CJS loader resolves through internal C++ bindings, so patching fs cannot make require see a virtual file, and Module._stat is captured as a module-local inside Module._findPath, so replacing it does nothing either. Resolution is therefore reimplemented, and checked against Node’s own answer for every package in a real bundle - 1407 package directories and 1865 bare specifiers, all matching. fs patching covers everything that is not a module: Meteor’s boot.js reads program.json and every server package with fs.readFileSync and runs them through vm.runInThisContext, and webapp serves the client files with fs.createReadStream. Below both sits one more: Meteor’s runtime.js hands reify a resolver that calls Module._resolveFilename directly, which registerHooks never sees.

Two traps are worth naming because both were hit here. A .node addon must be resolved with NO format declared - saying commonjs for one makes Node compile the binary as JavaScript and die with SyntaxError: Invalid or unexpected token on its own header, which is the 11.48 crash reproduced from the other side. And a directory that exists both really and in the archive has to merge the two listings, or wekan-app/programs/server would list the two unpacked files and hide the entire server.

This was verified by running it, not by reading it. With 23 real files on disk and 38,931 served from the ZIP, WeKan boots, connects to FerretDB and answers 200 on /sign-in and on its 6.9 MB client bundle. tests/bundleArchiveVfs.test.cjs builds a small archive and pins the three things that have to stay right - the ZIP reader, the resolution and the declared format - including the two traps above, and the release smoke test now counts what reached the disk, so a change that quietly went back to unpacking everything fails the job instead of passing it.

It drops the native prebuilds the target platform can never open. Thanks to xet7.

bcrypt and argon2 (Meteor’s accounts-password) each ship one prebuilds/<platform>-<arch>/ directory per platform they support: 21 native binaries in every WeKan bundle, of which exactly one is ever opened. Their loader is node-gyp-build, and its resolve() reads a single directory - readdirSync('prebuilds'), filtered by matchTuple(platform, arch) with platform and arch from os.platform() and os.arch().

That is the same argument releases/bundle-trim.mjs already makes about uWebSockets.js, so it is the same code path. --trim-prebuilds keeps the directories node-gyp-build would match and drops the rest, using that loader’s own tuple parsing - multi-arch names such as darwin-x64+arm64 included - and keeps both libc flavours of the target, because glibc versus musl is decided at runtime and not here.

Three things make it safe rather than merely smaller. It is off by default and refuses to run without an explicit --platform and --arch, since the defaults are linux/x64 and a Windows or macOS bundle trimmed with those would lose the only addon it can load - the fault the single EXE was just fixed for. build-amd64 deliberately does not pass it, because every other bundle WeKan ships is that bundle repacked and trimming there would take the prebuilds away from architectures not yet built; each final per-platform job passes its own target. And every decision is made before anything is deleted, so a package whose prebuilds cannot be reasoned about is left whole rather than half-trimmed - which is how bare-fs, bare-path and bare-url were found, shipping prebuilds/ of .bare files for the Bare runtime. Those are left untouched, and say so.

On the mac-arm64 bundle 19 of the 21 go and the two for darwin-arm64 stay. For the single Windows EXE it takes the files that have to be unpacked from 33 down to about fifteen.

Swimlanes - which swimlane a list belongs to, and whether that can be set.

Moving a list to a swimlane binds it to that swimlane. Thanks to TawsTm and xet7.

Moving a list to another swimlane on the same board did nothing at all. The list kept its empty swimlaneId, so it went on rendering under every swimlane, and deleting “the one in the second swimlane” deleted the only list document there was. A board backup showed the empty swimlaneId on lists that the report describes.

List.move(boardId, swimlaneId) asked does the target board already have a list with this name? without first asking whether the target board IS this list’s board:

const boardList = await ReactiveCache.getList({
  boardId, title: this.title, archived: false });
if (boardList) { ...merge, never writes a swimlaneId... }
else           { ...insert a new list WITH the swimlaneId... }

On a same-board move that search finds THIS LIST, so the merge branch ran - and the merge branch is the one branch that never writes a swimlaneId. The chosen swimlane was discarded every time, which is also why the binding could not be put back by hand.

Merging is only meaningful across boards; on the same board a move is a re-bind, and that is what it does now. The decision lives in models/lib/listMovePlan.js, where it is unit-tested, and models/lists.js applies it: same board re-binds this list, another board with the name already taken merges the cards into that list, another board creates it there and binds it. An empty swimlaneId remains a deliberate un-bind back to board-wide, which is still a legitimate layout, and moveList now refuses a swimlane that is not on the board the list is moving to.

The same branch also called card.move(boardId, this._id, boardList._id). Card.move’s second argument is a swimlaneId, so this set every card’s swimlaneId to a LIST id - a swimlane that does not exist - and those cards became the orphaned cards the board-open repair has to rescue. The cards were selected as this.cards(swimlaneId) as well, filtering the SOURCE list’s cards by a swimlaneId belonging to the TARGET board, which on a cross-board move matches nothing and left every card behind. Both are gone: a list’s own cards travel with it, into the chosen swimlane. A negative test scans models/lists.js for any card.move() whose second argument is not a swimlane, so the argument order cannot go wrong at another call site.

This does not restore bindings already lost. Boards opened under the versions before #6515 had every list un-bound automatically, and which swimlane each list belonged to is not recoverable from the data - a list’s cards can be spread across several swimlanes, and on a board whose second swimlane is new they are all in the first, so guessing would bind every list to one swimlane and hide it from the others. With this fix the binding can at least be set again from Move List.

Thanks to above GitHub users for their contributions and translators for their translations.