Skip to content

wekan

v9.88 Breaking

This release includes breaking changes for platform teams planning a safe upgrade.

Published 14d Productivity & Wikis
✓ No known CVEs patched
Read the diff → Tool health → What is this tool? →

✓ No known CVEs patched in this version

Topics

docker javascript kanban meteor real-time sandstorm
+2 more
snapcraft wekan

Summary

AI summary

Broad release touches https://github.com/wekan/wekan/commit/9f23e6384bdfa5ca81be18e4f0426c347e9894eb, wekan/mongo-tools, https://github.com/wekan/wekan/commit/143607ef22ae91803cb611053fd4ce00754e7096, and https://github.com/wekan/wekan/commit/66d8706c4d2afab4d127f8c63e05d43407efaf30.

Full changelog

v9.88 2026-07-13 WeKan ® release

This release adds the following new features:

and fixes the following bugs:

  • Sandstorm build: fixed "not writing through dangling symlink" that failed the
    sandstorm.yml workflow at sandstorm-src/build-deps.sh step [3/7]
    . The
    meteor-spk 0.6.0 base ships some runtime libs in meteor-spk.deps/lib as
    DANGLING symlinks (e.g. libstdc++.so.6 → a library that no longer exists), so
    refreshing them with the host's newer libs failed: cp -fL refuses to write
    through a dangling destination symlink. Added --remove-destination so cp
    deletes the existing destination (dangling symlink included) before copying the
    host's real library. Thanks to xet7.
  • Sandstorm build: write the signing keyring to the path meteor-spk pack
    actually reads
    . The sandstorm.yml "Restore the Sandstorm signing keyring"
    step wrote the decoded SANDSTORM_KEYRING secret to ~/.sandstorm/sandstorm-keyring,
    but meteor-spk / spk read the app private key from ~/.sandstorm-keyring (a
    file directly in $HOME), so packing aborted with
    open(~/.sandstorm-keyring): No such file or directory even when the secret was
    set. Now it writes ~/.sandstorm-keyring, and — since the .spk cannot be signed
    without it — fails early with an actionable message when the secret is missing,
    instead of the cryptic later crash. Thanks to xet7.
  • Sandstorm .spk: trim it back toward Cloudflare's 100 MB upload limit. The
    Sandstorm build no longer bundles the ~300 MB of modern MongoDB Database Tools
    (wekan/mongo-tools) — they are unused in Sandstorm (grains back up/restore
    themselves; the MongoDB 3 → FerretDB migration uses the legacy migratemongo CLIs
    • the .mjs importer). Also set PUPPETEER_SKIP_DOWNLOAD=1 for the Meteor build
      (puppeteer is a test-only devDependency, so its ~150 MB Chromium never belongs in
      the .spk), and the pack step now prints the .spk size and warns if it exceeds
      100 MB. (The Database Tools remain in the WeKan .zip bundle / Docker / Snap,
      which do use them.) Thanks to xet7.
  • Sandstorm .spk: bundle the matching glibc dynamic loader so grains start:
    the .spk bundled the host's new glibc libc.so.6 (Ubuntu 24.04, glibc 2.39)
    but kept the old glibc 2.31 ld-linux from the meteor-spk 0.6.0 base, so node
    failed at startup with libc.so.6: undefined symbol: _dl_audit_symbind_alt, version GLIBC_PRIVATE (HTTP-BRIDGE exit 127) and the grain crash-looped.
    sandstorm-src/build-deps.sh now also copies the host's ld-linux-x86-64.so.2
    so the loader and libc are the same glibc, and adds a [verify] gate that fails
    the build if they differ or the bundled node cannot run under them — turning a
    silent grain crash-loop into a loud build failure. Thanks to xet7.
  • Sandstorm build workflow: enable unprivileged user namespaces and build the dispatched branch:
    Ubuntu 24.04 defaults kernel.apparmor_restrict_unprivileged_userns=1, which
    blocks the unprivileged user namespaces the Sandstorm install and the spk
    supervisor rely on, so sandstorm.yml now relaxes it on the runner. A new ref
    workflow_dispatch input also lets the workflow build a fix branch before it is
    merged, instead of a hardcoded main. Thanks to xet7.
  • Meteor unit tests: fix server-boot crash from __dirname in an ESM test file:
    server/lib/tests/dependencies.openapi.tests.js is an ES module that referenced
    the bare __dirname global; under Node 24 / Meteor 3.5 the compiler injects
    const __dirname = fileURLToPath(import.meta.url), colliding with the __dirname
    the CommonJS module wrapper already provides, so the server bundle failed to boot
    with Identifier '__dirname' has already been declared and the "Meteor unit
    tests" CI job died before any test ran. Drop the __dirname seed
    (process.env.PWD already reaches the repo root under meteor test). Thanks to xet7.
  • Sandstorm .spk: point FerretDB state dir to writable /var so the grain starts:
    FerretDB persists a state.json (version/UUID) via its state provider even with
    telemetry disabled, and its --state-dir defaults to . — which in a Sandstorm
    grain is /, read-only. So FerretDB failed with Failed to create state provider: failed to persist state: open /state.json: read-only file system,
    exited (code 1), and the grain crash-looped. sandstorm-src/start.js now creates
    /var/ferretdb and passes --state-dir=/var/ferretdb (plus FERRETDB_STATE_DIR)
    in startFerret(), covering both the migration and steady-state FerretDB launches.
    Thanks to xet7.
  • Sandstorm .spk: don't crash the grain when capnp.node can't load on Node 24:
    WeKan on Sandstorm bundles Node 24, but capnp.node (node-capnp) is built for an
    older Node ABI (NODE_MODULE_VERSION 83 = Node 14), so Npm.require('capnp') in
    sandstorm.js failed with ERR_DLOPEN_FAILED and crash-looped the whole grain on
    boot. Cap'n Proto is now loaded lazily in a try/catch and degrades gracefully:
    the grain boots and core WeKan works, because login and user identity come from
    the sandstorm-http-bridge X-Sandstorm-* HTTP headers (wekan-accounts-sandstorm)
    and need no Cap'n Proto. Only the two capnp-only features are skipped when the
    addon cannot load — the Powerbox identity-claim method and Sandstorm activity
    notifications — with a clear warning. They can be restored by rebuilding node-capnp
    for Node 24, or reimplemented over the bridge's HTTP/JSON API. Thanks to xet7.
  • Snap release: build amd64+arm64 natively on GitHub, exotic arches non-blocking on Launchpad:
    the single snap job ran snapcraft remote-build for all 5 platforms on Launchpad
    and blocked until the slowest resolved, so the release hung ~3.5h on riscv64's
    Launchpad queue even though amd64+arm64 finished in minutes. It is now split in
    two: snap-native builds amd64 (ubuntu-24.04) and arm64 (ubuntu-24.04-arm)
    natively on GitHub runners via snapcore/action-build; snap-launchpad builds
    s390x/ppc64el/riscv64 on Launchpad in a non-blocking (continue-on-error),
    per-arch matrix (remote-build --build-for <arch>) so a slow or failed exotic
    arch never delays the release or the other arches. Each arch publishes to the
    Snap Store (candidate,beta,edge) and attaches to the GitHub Release the moment it
    finishes — all 5 arches still ship. Thanks to xet7.
  • Sandstorm .spk: fix server-boot crashes from stale globals (Users, HTTP) in sandstorm.js:
    once the capnp load was made non-fatal, the grain reached the rest of
    sandstorm.js and hit two latent Meteor-2.x-isms the Meteor 3.x migration missed
    (this file only runs on Sandstorm, so it was not exercised): it referenced the
    Users/Boards/Swimlanes/Activities collections as implicit globals, but
    those are now ES module default exports, so Users.after.insert threw
    Users is not defined at boot; and it monkey-patched HTTP.methods from the
    removed meteor/http package, so HTTP was undefined. The collections (and
    Accounts) are now imported explicitly, and the obsolete HTTP.methods patch is
    removed. Thanks to xet7.
  • Sandstorm .spk: use boolean index options so FerretDB accepts the users index:
    wekan-accounts-sandstorm created the unique index on services.sandstorm.id
    with {unique: 1, sparse: 1}. Real MongoDB accepts the truthy 1, but FerretDB
    (used by the Sandstorm .spk) is strict and rejects it with The field 'unique' has value unique: 1, which is not convertible to bool, crashing the grain at boot
    during index creation. Now uses real booleans (unique: true, sparse: true).
    Thanks to xet7.
  • Sandstorm .spk: strip Accept-Encoding so the grain doesn't serve corrupted (gzip) content:
    the grain boots, but the page failed to load with a browser "Corrupted Content
    Error" (NS_ERROR_NET_CORRUPTED_CONTENT). sandstorm-http-bridge advertises
    Accept-Encoding: gzip to the app regardless of what the browser actually sent,
    so Meteor served gzip/brotli-encoded responses the browser could not decode. A
    WebApp.rawHandlers middleware now strips Accept-Encoding (it runs before
    Meteor's static/boilerplate serving) so responses go out uncompressed; bandwidth
    is a non-issue behind the local bridge. Thanks to xet7.
  • Sandstorm .spk: bundle a modern sandstorm-http-bridge to fix "Corrupted Content":
    the grain boots, but the page failed with a browser "Corrupted Content Error"
    (NS_ERROR_NET_CORRUPTED_CONTENT) on WeKan's / redirect. The meteor-spk 0.6.0
    base bundles an ancient (~2016) sandstorm-http-bridge that mangles responses
    (it always advertises Accept-Encoding: gzip to the app and mishandles
    redirect/encoding). build-deps.sh now overwrites the bundled
    /sandstorm-http-bridge with the modern one from a Sandstorm install
    (/opt/sandstorm/latest/bin/sandstorm-http-bridge; override with
    SANDSTORM_HTTP_BRIDGE), and sandstorm.yml installs Sandstorm before assembling
    the deps so the bridge is available on the CI runner. Thanks to xet7.
  • Sandstorm .spk: fix the "/" redirect (malformed Location + Content-Length mismatch):
    the grain's / served a broken 301: the Location was .../:6080board because
    FlowRouter.path() does not resolve on the server in Meteor 3.x (it returned the
    bare route name board), and the response advertised Content-Length: 90 while
    sending an empty body — which the browser rejected as a "Corrupted Content Error"
    (NS_ERROR_NET_CORRUPTED_CONTENT). The handler now builds the board path directly
    (/b/:id/:slug) and sends a real HTML body (meta refresh + link) with a matching
    Content-Length, so the redirect to the hard-coded Sandstorm board works. Thanks
    to xet7.
  • Sandstorm .spk: open the grain on the All Boards page, not a hard-coded board:
    WeKan on Sandstorm originally opened a single hard-coded board
    (/b/sandstorm/libreboard) and redirected / to it. It now supports many boards
    and that board is no longer the right destination, so the WebApp.handlers.get("/")
    redirect is removed entirely — / now falls through to WeKan's normal serving,
    whose client home route renders the All Boards list, the right landing page
    for a multi-board grain. This also removes the last server-side redirect the
    browser was rejecting as a Corrupted Content Error. Thanks to xet7.
  • Sandstorm .spk: don't auto-create a board; map grain permissions to global role:
    a new grain/user no longer gets a hard-coded sandstorm/libreboard board
    auto-created — WeKan on Sandstorm is multi-board now, so the user creates their own
    boards from the All Boards page. updateUserPermissions previously added the user
    as a member of that single board (which would now crash since the board no longer
    exists); it now maps the grain's Sandstorm permissions to the user's global
    WeKan role — configure (grain owner) becomes a WeKan admin, everyone else is a
    regular user who can create and manage their own boards. Thanks to xet7.
  • Sandstorm .spk: set SANDSTORM=1 so the header-based auto-login runs:
    WeKan loaded in the grain but every page showed "Must be logged in". The
    wekan-accounts-sandstorm client only starts the automatic X-Sandstorm-* header
    login when __meteor_runtime_config__.SANDSTORM is set, and the package only sets
    that when process.env.SANDSTORM is present — which nothing did. The launcher now
    sets process.env.SANDSTORM = '1' before loading the WeKan bundle, so the client
    auto-logs-in the Sandstorm user from the headers. Thanks to xet7.
  • Sandstorm .spk: rewrite ROOT_URL per request to the grain URL (fixes login + CORS):
    WeKan loaded but stayed on "Must be logged in", and the console showed
    Cross-Origin Request Blocked … http://127.0.0.1:4000/__meteor__/dynamic-import/fetch.
    The launcher sets a fixed ROOT_URL (http://127.0.0.1:4000, the internal bridge
    target), but Sandstorm serves each grain at a per-session host
    (ui-<hash>.<host>), so the client sent its DDP connection and dynamic-import
    fetches to 127.0.0.1:4000 — cross-origin and unreachable — and the header-based
    login handshake (a DDP method call) never completed. A WebApp.addRuntimeConfigHook
    now rewrites ROOT_URL to the grain's real base URL (X-Sandstorm-Base-Path) per
    request, so DDP, dynamic imports and the Sandstorm auto-login work. Thanks to xet7.
  • Sandstorm .spk: bounce from sign-in to the boards list once auto-login lands:
    the Sandstorm login was actually succeeding (Meteor.userId() gets set), but the
    grain stayed on the sign-in page. WeKan's home route checks Meteor.userId() once
    and, because the Sandstorm header login is asynchronous and uses
    connection.setUserId() (bypassing accounts-base, so Accounts.onLogin never
    fires), finds it still null on first render and redirects to atSignIn — where the
    user is stranded even after login completes. A Sandstorm-only reactive autorun now
    sends the user from atSignIn back to home as soon as Meteor.userId() is set,
    so the grain lands on the All Boards page. Thanks to xet7.
  • Sandstorm .spk: keep the grain URL in sync with the in-app route:
    navigating between boards worked but the Sandstorm shell's grain URL never updated
    (it stayed on the grain root), unlike standalone WeKan. The path was synced via a
    global FlowRouter.triggers.enter callback, which does not fire reliably on client
    navigation in this flow-router-extra / Meteor 3 setup; it is now synced from a
    reactive Tracker.autorun on FlowRouter.watchPathChange() (the same mechanism
    the title sync uses), so every route change updates the grain URL to
    /grain/<id><path>. Thanks to xet7.
  • Sandstorm .spk: upload attachments over DDP (bridge strips Meteor-Files' HTTP headers):
    adding a file to a card in the grain failed with HTTP 400 Can't continue upload, session expired [408] and the file silently disappeared. Meteor-Files' HTTP
    upload signals the first chunk with an x-start header and tracks the session
    with x-mtok/x-chunkid/x-fileid/x-eof, but Sandstorm's request-header
    whitelist does not include them, so the sandstorm-http-bridge strips them — the
    server never sees x-start, treats every request as a chunk continuation, cannot
    find the session and returns 408. Attachment uploads now use transport: 'ddp' on
    Sandstorm (DDP method calls, no custom HTTP headers); HTTP transport is kept
    everywhere else. Thanks to xet7.
  • Sandstorm .spk: authorize attachment/avatar downloads via X-Sandstorm-User-Id:
    once uploads worked, the uploaded image still showed as a broken thumbnail,
    minicard cover and slideshow in the grain — the file was on disk but the download
    returned HTTP 403. The download route (server/routes/universalFileServer.js)
    authorizes files on private boards with a Meteor login token (Authorization /
    X-Auth-Token / authToken query / meteor_login_token cookie), but Sandstorm has
    none of these: authentication is via connection.setUserId() and the
    sandstorm-http-bridge X-Sandstorm-* request headers, so extractLoginToken()
    returned null and isAuthorizedForBoard() denied every request. Sandstorm already
    gates grain access at the platform level, so any request that reaches WeKan is an
    authenticated grain user — both isAuthorizedForBoard() and
    isAuthorizedForAvatar() now allow when Meteor.settings.public.sandstorm is set
    and the request carries the bridge-injected X-Sandstorm-User-Id header. Gating on
    the setting means a spoofed header cannot bypass auth on non-Sandstorm deployments.
    Thanks to xet7.
  • Sandstorm .spk: open All Boards (not sign-in) and keep the grain URL in sync:
    two grain-navigation regressions against the last working Sandstorm build (v6.15).
    (1) Opening a grain showed "Must be logged in" instead of the All Boards page: on
    Sandstorm the platform authenticates the user asynchronously over DDP via
    connection.setUserId(), which (unlike a password login) does not set
    Meteor.loggingIn(), so Meteor.userId() is null for the first moments after the
    grain opens — and useraccounts' ensureSignedIn trigger plus renderBoardList()
    both bounced that brief null window to the atSignIn route, a sign-in page that
    does not exist inside a grain. config/router.js now uses a Sandstorm-aware
    ensureSignedInUnlessSandstorm wrapper (a no-op on Sandstorm) and
    renderBoardList() no longer redirects on Sandstorm; the list renders and fills in
    reactively once the login lands. (2) The Sandstorm shell's outer grain URL did not
    update when switching boards — the shell rewrites /grain/<id><path> when the app
    posts a { setPath } message, but the sync was a bare top-level Tracker.autorun
    that could run before flow-router-extra's reactive path tracking was ready and then
    never re-run. Restored v6.15's event-driven FlowRouter.triggers.enter (fresh
    entering path, order-independent) and kept a watchPathChange() autorun wrapped in
    Meteor.startup as a backup. Thanks to xet7.
  • Sandstorm .spk: fix the MongoDB 3 → FerretDB migration of an existing grain:
    importing an old WeKan grain crash-looped in the one-time migration — mongod 3.0
    forked and its child aborted with exit code 14. Two bugs. (1) WiredTiger
    cache_size=0G
    : mongod 3.0 sizes its WiredTiger cache from detected RAM
    (RAM/2 − 1GB), but inside a Sandstorm grain sandbox RAM detection returns 0, so it
    computed cache_size=0G and WiredTiger refused to open (minimum is 1MB), logging
    "Value too small for key 'cache_size'" / "Fatal Assertion 28561" to
    /var/migration-mongod.log before aborting. Both mongod invocations (the niscu →
    3.0 stage and the 3.0 → FerretDB stage) now pass an explicit
    --wiredTigerCacheSizeGB 1 so the cache size never depends on RAM detection
    (mongod 3.0.7 parses this option as an integer number of GB — a decimal like 0.25
    fails with Bad digit ".", fixed to 1
    and it is a cache cap, not a preallocation). The data
    itself is intact (the "unclean shutdown" notice is harmless —
    WiredTiger recovers from the last checkpoint). (2) Wrong source database:
    Sandstorm WeKan grains store their data in the Meteor-default database meteor, but
    the importer was told SRC_DB=wekan, so even once mongod started it would have
    exported zero collections; only the FerretDB target database is wekan. Thanks to
    xet7.
  • Sandstorm/Snap migration: import mongodb and bson as default (CommonJS) exports under Node 24:
    with the WiredTiger cache fixed mongod 3.0 started and the migration importer ran,
    but crashed immediately on its named imports — import { EJSON } from 'bson' and
    import { MongoClient } from 'mongodb' each threw "Named export '…' not found. The
    requested module is a CommonJS module"
    . Both packages, as bundled in WeKan's server
    node_modules, are CommonJS, so Node 24's ESM loader exposes no named exports on
    them. Import the default and destructure, as Node's own error message advises (bson in
    the commit linked above; mongodb in the same way).
    Shared by the Snap MongoDB 3 → FerretDB migration too (same Node 24). Thanks to xet7.
  • Sandstorm/Snap migration: connect mongoexport over IPv4 so it can read the old data:
    with the importer finally running, every mongoexport of a source collection failed
    — at first silently (its own --quiet flag suppressed the reason),
    and once that was
    dropped the real error showed: "error connecting to db server: no reachable
    servers"
    . mongoexport is a Go tool whose --host defaults to localhost, which
    resolves to ::1 (IPv6) first, but the migration mongod listens only on
    --bind_ip 127.0.0.1 (IPv4); the mongo shell defaults its host to 127.0.0.1 and so
    connected fine (it listed all 42 collections), which is why only the shell worked.
    Pass --host 127.0.0.1 explicitly. The one-time progress dashboard also now shows a
    live Activity panel (mongoexport-ready line, per-collection export/insert counts, GridFS
    extraction counts) with a spinner and an auto-updating timestamp,
    so it is
    clear what the migration is doing rather than sitting on "(waiting…)". Thanks to
    xet7.
  • Sandstorm/Snap migration: resolve bson/mongodb from the modern server bundle so EJSON exists:
    once mongoexport connected, every collection failed with "Cannot read properties of
    undefined (reading 'parse')"
    EJSON was undefined. The importer script sits at the
    deps root right next to the OLD meteor-spk 0.6.0 base node_modules (kept only for the
    niscu → 3.0 stage): its bson is 1.x with no EJSON at all, and its mongodb is
    ancient — it has MongoClient (so the connection worked) but no EJSON re-export. A
    bare import/require from the script resolved those adjacent old copies, so every
    way of reaching EJSON (bare bson, mongodb.EJSON) came back undefined. WeKan's
    current bson 7.3 and mongodb driver (with EJSON) live under
    programs/server/npm/node_modules; anchor createRequire
    inside that modern bundle
    first (falling back to the deps root) and load both mongodb and bson through it —
    which also moves the importer to the same modern mongodb driver the WeKan app uses
    against FerretDB. An upfront guard fails loudly with a diagnostic list if EJSON.parse
    is still unreachable. Thanks to xet7.
  • Sandstorm/Snap migration: insert into FerretDB with the modern mongodb driver (OP_MSG):
    text collections exported and "inserted N/N" was logged, but every document actually
    failed with "Unsupported OP_QUERY command: update" — nothing reached FerretDB.
    requireAny('mongodb') had resolved the ancient meteor-spk base driver (v2.x, at the
    deps root) because the modern driver is not directly under
    programs/server/npm/node_modules — Meteor nests it at
    …/meteor/npm-mongo/node_modules/mongodb (v6.16). The 2.x driver speaks legacy
    OP_QUERY, which FerretDB rejects; the 6.x driver speaks OP_MSG (the same driver the
    WeKan app uses against FerretDB). Add the npm-mongo path as the first resolver anchor,
    and log the resolved driver version. Thanks to xet7.
  • Sandstorm/Snap migration: extract Meteor-Files GridFS attachments + parse legacy v1 binary:
    with text migrating, attachments still produced 0 files and "parse attachments.chunks:
    Unexpected Binary Extended JSON format"
    . Two causes. (1) mongo 3.x mongoexport writes
    binary as legacy Extended JSON v1 {"$binary":"<b64>","$type":"00"}, but modern bson
    EJSON.parse only accepts v2 {"$binary":{"base64":…,"subType":…}} — rewrite v1→v2 per
    line before parsing. (2) The grain stores attachments in Meteor-Files' own GridFS buckets
    (attachments.files + attachments.chunks, with the FilesCollection record in the
    attachments collection), not CollectionFS's cfs_gridfs.*. Reassemble those buckets to
    disk, link each GridFS file to its record via metadata.fileId/versionName, then repoint
    the record's versions.<v> at the file and drop versions.<v>.meta.gridFsFileId
    otherwise WeKan's getFileStrategy keeps choosing the now-empty GridFS backend and the
    image 404s. Verified end to end on a real grain: boards/cards/lists/swimlanes migrate and
    all 3 attachments extract and display. Thanks to xet7.
  • Sandstorm/Snap migration: auto-open All Boards after migrating, WeKan-themed dashboard:
    the one-time progress dashboard used to sit on the grain URL for 60s after completion,
    forcing a manual reload to reach WeKan. On success the importer now hands off quickly and
    the done page polls / until WeKan's app shell answers (riding out the brief
    importer→WeKan port hand-off) and then opens All Boards, with the spinner still spinning so
    it is clear the grain is still working. The dashboard is also recoloured to WeKan's
    blue/white/grey. Thanks to xet7.

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

Weekly OSS security release digest.

The CVE patches and breaking changes that affected production tools this week. One email, every Sunday.

No spam, unsubscribe anytime.

Share this release

Track wekan

Get notified when new releases ship.

Sign up free

About wekan

The Open Source kanban, built with Meteor. GitHub issues/PRs are only for FLOSS Developers, not for support, support is at https://wekan.fi/commercial-support/ . New English strings for new features at imports/i18n/data/en.i18n.json . Non-English translations at https://app.transifex.com/wekan/wekan only.

All releases →

Related context

Related tools

Beta — feedback welcome: [email protected]