Upgrading Mobius4
August 13, 2026 · View on GitHub
Everything here applies only when upgrading an existing deployment. A clean install on the current version needs none of it — follow docs/installation.md instead.
Two kinds of thing live here:
- Required steps — a DB migration, a new prerequisite. The upgrade is not finished until these are done.
- Known upgrade problems — failures that happen because something from the old version is still around. The source tree is correct; the environment is stale.
Sections are newest first, matching CHANGELOG.md. Upgrading across several versions means working upward from your current version and applying each section in turn.
For what changed in a release and why, read the CHANGELOG. This document only answers "what do I have to do about it."
[Unreleased]
Nothing required: the administrator bypasses access control again
The short-circuit removed in § v4.6.0 below is back. cse.admin is granted every
operation before any <accessControlPolicy> is read, on every resource regardless of policy,
creator or acpi.
No migration, no configuration change. The admin <accessControlPolicy> is still created
at startup and still evaluated for anyone else it names, so a deployment that did the v4.6.0
migration keeps working exactly as it did.
Two things you may now be able to undo:
- If you named
cb_admin_acpin every resource'saccessControlPolicyIDsto keep the administrator's reach, you no longer have to. Removing it is optional and changes nothing for the administrator either way. - If you were writing to the
acpicolumn in PostgreSQL by hand to recover resources the administrator had been locked out of — resources created with noacpi, which no request could repair — that is what this change exists to end.
One thing to check instead: cse.admin is now a full bypass, so its blast radius is wider
than the admin policy's was. If you had come to rely on the policy bounding what the identity
could do, it no longer does. Everything § v4.6.0 below says about treating the value
as a credential applies with more force.
v4.14.0
Required only if your client computes ofst itself: the offset filter is 1-based
ofst=1 now means "start at the first result" rather than "skip one result", per
TS-0004:7.3.3.17.15 ("An offset of 1 shall indicate the first direct child resource") and
TS-0001:8.1.2 ("The offset shall start at 1"). Every X-M2M-CTO the CSE emits is one higher than
it was.
Nothing to do if you echo X-M2M-CTO back as ofst — the value and its meaning changed
together, so the round trip lands where it always did. This is the usage
§ v4.10.0 below already asks for.
If you add lim to a counter of your own, subtract one from the result, or switch to echoing
X-M2M-CTO. Left alone, each page after the first now repeats one result (rcn=4/rcn=8: one
subtree) instead of starting after the last one. It repeats rather than skips, so no data goes
missing and a client that keys on resource ID is unaffected in practice.
One case gets strictly better: for rcn=4/rcn=8, X-M2M-CTO could previously be 0, and
sending ofst=0 back is refused with 4000. That value no longer occurs.
Worth knowing: an expired resource now stops working before it is deleted
If you rely on expirationTime as a lease — set a short et so that a crash cannot leave a
resource behind — the behaviour during the window between expiry and deletion has changed:
- An expired
<subscription>no longer publishes notifications. Previously it kept notifying until the sweep ran, by default up to a day later. - An obsolete
<contentInstance>is no longer returned by<latest>/<oldest>and is not amongrcn=4/rcn=8children. A<container>whose instances are all obsolete answers<latest>with 4004, the same as one that never had any. - Because
maxInstanceAgeis enforced by cappinget(since v4.9.0), the second point meansmianow applies to reads too. A<container>with a shortmiacan start answering<latest>with 4004 where it used to return stale content. If you were reading content older than themiayou configured, raisemiaor drop it.
An expired resource is still retrievable at its own address and still appears in fu=1 discovery
until the sweep removes it. The whole boundary is tabulated in
docs/configuration.md § expirationTime.
Worth knowing: the sweep now runs at startup
expired_resource_cleanup was scheduled with an interval only, so its first run came a full
interval after boot. A deployment restarting more often than that never swept. The first boot on
this version therefore deletes everything that expired while the old version was skipping the
sweep, which can be a much larger batch than a day's worth. It runs in the background and does not
delay readiness, but expect the accompanying database load once.
v4.13.0
Required: DB migration
<AE> gains the ontologyRef attribute, which needs a new column. A clean
install does not need this — db/init.js creates the column — but an existing
database does:
psql -d mobius4 -f db/migrations/v4.13.0.sql
The migration is ALTER TABLE ae ADD COLUMN IF NOT EXISTS "or" VARCHAR(255). It
does not rewrite existing rows and changes no behaviour by itself: every <AE>
that already exists gets or = NULL, which is what "the attribute is not
present" already meant. It is safe to run twice.
Worth knowing: a fanout over an empty group now fails instead of succeeding
A RETRIEVE (or any operation) sent to <group>/fopt when the group has no
members used to answer 2000 with an empty m2m:agr. It now answers 4109
NO_MEMBERS, which is what TS-0004:7.4.14.2.4 requires.
If a client treats "2000 with nothing in it" as normal — polling a group that is filled in later, for instance — it will start seeing an error status. The fix on the client side is to treat 4109 as "not yet", not as a failure.
Worth knowing: memberTypeValidated can now be false, and members are no longer silently dropped
Before this release, a <group> member hosted on another CSE was dropped at
creation time and the group still reported memberTypeValidated = true. Those
members are already gone from the stored memberIDs, and this release does not
go back and repair them — re-send the CREATE, or an UPDATE carrying the full
member list, for any group that was meant to span CSEs. Nothing to do if you have
never put a remote member in a group, which is the common case: it did not work.
Two consequences for clients:
memberTypeValidatedmay now be false, which previously never appeared. It means the CSE could not reach some member's Hosting CSE and has not judged those members yet. The members are in the group regardless.- A
<group>CREATE or UPDATE naming a member whose type the CSE is not allowed to read is now rejected with 5105 RECEIVER_HAS_NO_PRIVILEGE rather than quietly dropping that member.
Worth knowing: forwarded Response Status Codes are numbers
In an aggregated fanout response (m2m:agr/m2m:rsp), a member response coming
from another CSE used to carry rsc as a JSON string ("2000") while a local
one carried a number (2000). Both are numbers now, matching the xs:integer
type in TS-0004. A client that compared rsc against a string literal for
forwarded members must compare against a number.
v4.11.0
Worth knowing: contentSize values change, and with them what mbis/mbs refuse
contentSize counted JavaScript string units (length * 2), not bytes. It now counts UTF-8
bytes, as TS-0001:9.6.7 requires.
"abc" 3 bytes was 6 now 3
"0123456789" 10 bytes was 20 now 10
"한글" 6 bytes was 4 now 6
Nothing needs migrating. Existing <contentInstance> rows keep the cs they were stored
with, and a container's cbs is the sum of those, so a container's figure is a mix of old and new
until its instances turn over.
Two things to check before upgrading:
- A container sitting close to its
maxByteSizeceiling may evict differently for a while, sincecbsis compared againstmbsand half the values were inflated. - Requests that used to be refused will now succeed. If any client depends on a
maxByteSizePerInstancerefusal as a size guard, its effective limit has roughly doubled for ASCII content — halve the configured value to keep the previous behaviour.
Structured content is measured as its JSON form. Which serialization the standard means is undefined and remains an open question.
Worth knowing: a database failure now answers 5000
During a database outage requests came back 4004 "target resource does not exist", for resources
that existed. They now answer 5000 INTERNAL_SERVER_ERROR (TS-0004:6.6.2, 6.6.3.6).
Check your client's error handling. A client that treats 4004 as "it is not there, create it" was being led into recreating live resources; a client that does not retry 4xxx was giving up on a condition that would have cleared. Both are correct behaviours against the new code and wrong against the old.
The create path (previously 4000) and the <CSEBase> retrieve (previously 4103 access denied)
changed with it.
Worth knowing: /health can now fail
It reads one row from the database before answering, and returns 503 with
{"status":"unavailable","db":"unreachable"} when it cannot.
If you have an external monitor treating any non-200 from /health as an incident, it will now
see incidents it previously missed — that is the point. If you were using /health as a liveness
probe specifically (restart only when the process is gone), it is no longer the right endpoint for
that: it now reports readiness.
Compose already used this endpoint as the container healthcheck, so no compose change is needed.
Worth knowing: <subscription> now sets creator by itself
Where notificationURI is not the Originator, creator is filled in automatically
(TS-0004:7.4.8.2.1) and carried in notifications as m2m:sgn.cr (TS-0004:7.5.1.2.2).
If a notification consumer validates the set of members it receives strictly, it will now see one more.
A request can no longer set creator to another entity's identity — that is refused with
4000. Sending an empty value to mean "fill it in for me" still works, as does sending your own ID.
This closes a privilege path: on a resource that defines accessControlPolicyIDs but has none
set, the creator holds full control.
Optional: database pool settings on the container
db.pool.connectionTimeoutMs default is raised from 2000 to 5000. DB_POOL_MAX,
DB_POOL_CONNECTION_TIMEOUT_MS and DB_POOL_STATEMENT_TIMEOUT_MS can now be set in .env. Unset
values fall through to config/default.json as before.
v4.10.0
Required if any client sends rcn=4 or rcn=8: update the response parser
Child resources are now nested inside their own parent. A parser written for the old flat shape finds no children and reports an empty result — it does not raise.
// 4.9.0 and earlier — every descendant at the top level, grouped by type
{"m2m:cnt": {"rn": "sensors",
"m2m:cnt": [{"rn": "humid01"}, {"rn": "temp01"}],
"m2m:cin": [{"rn": "h1"}, {"rn": "t1"}, {"rn": "t2"}]}}
// 4.10.0 — each resource under the parent that owns it
{"m2m:cnt": {"rn": "sensors",
"m2m:cnt": [{"rn": "humid01", "m2m:cin": [{"rn": "h1"}]},
{"rn": "temp01", "m2m:cin": [{"rn": "t1"}, {"rn": "t2"}]}]}}
Walk the tree recursively instead of reading one flat array per type. The parent-child
relationship no longer has to be reconstructed from pi.
Nothing else is affected. Plain retrieves (rcn=1 or no rcn), discovery (fu=1, still a
flat m2m:uril), notifications and CREATE/UPDATE/DELETE responses are unchanged. To find out
whether this applies to you, grep your client for rcn=4, rcn=8, rcn%3D4, or a
resultContent of 4 or 8.
Required if you paginate rcn=4/rcn=8: stop computing ofst yourself
ofst now counts direct children for these two values (it still counts resources for fu=1
discovery), because a nested result cannot be resumed in the middle of a subtree without
duplicating the parent or orphaning its children.
Send back the X-M2M-CTO value from the previous response rather than adding lim to your own
counter. Computing it yourself now skips or repeats whole subtrees, silently.
Worth knowing: a large subtree can make a request return no children
lim now cuts on subtree boundaries, because TS-0001:8.1.2 requires that a direct child whose
descendants cannot all be included is left out entirely.
The consequence to plan for is one direct child whose own subtree is larger than lim.
Retrieving an <AE> that has a <container> holding 250 <contentInstance>s makes that
container's subtree 251 resources; it does not fit in the default lim of 200, so it is dropped
whole and the <AE> comes back with no children at all — where 4.9.0 returned 200 of them.
Raising ofst does not help; only a larger lim does.
Retrieving that <container> directly is fine: its 250 children are 250 subtrees of one resource
each, and 200 of them fit.
The response body gives no clue when this happens, so the server logs a warning:
rcn=4/8 returned no children: the first subtree is larger than lim
Worth knowing: rcn=5 and rcn=6 do something now
Both used to be ignored — they returned the target's attributes with RSC 2000, exactly like
rcn=1, so a client asking which children existed was told "none", successfully. If you worked
around that by falling back to fu=1 discovery, the fallback is no longer needed:
rcn=5adds acharray of{"nm", "typ", "val"}to the target's representation. The member is omitted, not empty, when there are no children.rcn=6returnsm2m:rrlwith anrrfarray and no representation of the target.drt=2makesvalan unstructured ID.
Worth knowing: truncated child-resource results now say so
X-M2M-CTS (Content Status; 1 means partial) and X-M2M-CTO (Content Offset) are set whenever
lim truncates an rcn=4/5/6/8 result. Previously a truncated result was indistinguishable from
a complete one. A client that inferred "fewer than lim means done" can keep working, but reading
these headers is both cheaper and correct.
v4.9.0
Required if you have an existing database: add the mbis column
psql -U "$DB_USER" -d "$DB_NAME" -f db/migrations/v4.9.0.sql
Adds cnt.mbis (maxByteSizePerInstance). Fast — no default value, no rewrite of existing
rows. Skip this if you are on Docker Compose and using its bundled database with a fresh
volume; db/init.js creates the column directly for a new deployment.
Worth knowing: maxInstanceAge is now actually enforced
TS-0004:7.4.7.2.1 step 2 e) requires a <container>'s maxInstanceAge to cap the
expirationTime of its <contentInstance> children. It never did before this release — mia
was stored and returned but nothing compared it against et.
If a container's mia was left at the deployment default, this now costs at most about a day
off what its content instances' lifetime would otherwise have been: the default moved from 30
days to 365 days in the same release that turns enforcement on, specifically so that a
container left at its defaults keeps behaving the way it did before (the 365-day default and
the previous 12-calendar-month et default can differ by up to a day, only in date ranges
that include 29 February).
If a container's mia was set explicitly to something narrower than that, its content
instances now actually get the shorter lifetime that attribute always claimed to promise. If
that is not what you want, widen or clear mia on that container (see the known issue below —
clearing it back to "no limit" does not currently work).
Known issue, not fixed in this release: sending null to clear mni, mbs or mia on a
<container> UPDATE does not work — the request is rejected with 4000 before the code that
would reset it ever runs. If you were relying on this to remove a limit, it has never actually
done so; set an explicit wide value instead.
v4.7.0
Required if you serve HTTPS: turn it on and say where the files are
The listener is now optional and off by default. Before this release it started
unconditionally, reading certs/ca.crt, certs/wdc.key and certs/wdc.crt at module
load with no condition and no error handling — a deployment without those files could
not start, and one with them had no way to turn TLS off.
After upgrading, a deployment that was serving HTTPS serves only plain HTTP until
config/local.json says otherwise:
{
"https": {
"enabled": true,
"port": 7580,
"key": "certs/server.key", // your own key
"cert": "certs/server.crt", // your own certificate
"chain": "" // intermediate CA bundle, if your issuer gives one
}
}
The old hardcoded paths are gone, so the values above are the defaults rather than what you had. Point them wherever your files are; absolute paths work.
If https.enabled is true and a file cannot be read, Mobius4 stops instead of
serving plain HTTP, naming the setting that pointed at it. Check the startup log after
the first restart — HTTPS server listening or HTTPS is disabled.
Full procedure for obtaining, installing and replacing a certificate: docs/tls.md.
Your clients are no longer asked for a certificate
The listener used to set requestCert: true and rejectUnauthorized: true. Clients
that present a certificate still connect — it is simply ignored now.
If you were treating that requirement as authentication, it was not: nothing in
Mobius4 ever read the certificate, so the handshake proved possession of a CA-signed
certificate and never that the holder was the originator named in X-M2M-Origin. Any
client with a certificate from your CA could act as any AE, including the
administrator. Replace that assumption with network-level access control (firewall,
reverse proxy, or mTLS terminated in front of Mobius4) together with oneM2M
<accessControlPolicy> resources.
Treat the keys this repository used to ship as disclosed
certs/ is deleted from the source tree and gitignored. Two private keys were in it —
certs/wdc.key (the server key) and certs/SAE1.key (a sample client key) — and both
remain in the git history, where anyone with a clone can read them.
- If your deployment serves
wdc.crt, issue a new certificate and key (docs/tls.md). - If any client still holds
SAE1.keyand it is used for anything, reissue it.
Nothing else changes. No DB migration, no configuration is newly required for a deployment that does not use HTTPS.
v4.6.5
Nothing is required. No DB migration, no new configuration. Two things are worth checking, both of them workarounds you may now be able to undo.
If you worked around <contentInstance> resources being unreadable
Before this release, a <contentInstance> under a <container> carrying an
accessControlPolicyIDs was refused to every originator, the administrator
included, and was dropped from discovery results without an error. Deployments
hit by this tended to work around it in one of two ways:
- Leaving
acpioff the containers that hold content instances, so that the creator-comparison fallback governed them instead. Those containers can now carry a policy. Note the consequence of adding one: a resource with a policy is no longer governed by its creator, so whoever was reaching it through the creator fallback needs to be named in the policy. - Setting
cse.allow_discovery_for_any: true, which skips access control for discovery entirely. See below — there is now a better reason to turn it off.
If you were not affected — no <accessControlPolicy> on the containers holding
content instances — nothing about your deployment changes.
If you set cse.allow_discovery_for_any: true for speed
That setting exists to skip discovery's access-control filter, and skipping it used to be worth a great deal: the filter evaluated every matching resource one at a time, which measured 18 requests per second over a container holding 150 content instances.
The filter now decides once per policy holder rather than once per resource, and the same measurement is 614 requests per second. The speed argument for turning access control off has largely gone.
The setting is a conformance decision, not a performance knob: TS-0004:6.3.4.2.29
defines 32 = DISCOVERY, so with it on, the DISCOVERY bit in every
<accessControlPolicy> stops meaning anything and any originator can enumerate
the resource tree (identifiers and structure — fu=1 returns m2m:uril only, so
content is still protected). If it is on purely for throughput, this is the
release to turn it back off. The default is, and remains, false.
v4.6.3
Only if you raised db.pool.max: halve it
db.pool.max used to be applied by each of the two connection pools this process runs
separately, so the setting bought twice the connections it named — a process with max: 30
was measured holding 53. It is now the process-wide total, split between the pools.
If config/local.json overrides db.pool.max, the same value now opens half as many
connections as before. Halve your override to keep the behaviour you had, or leave it and use
the smaller number — the size buys very little either way: 10 connections per pool reached
3,069 requests per second against 3,139 for 30.
If you never overrode it there is nothing to do. The default moves from 30 to 20, which is fewer connections than the old default actually opened.
Worth knowing before you run more than one instance
The number of instances a PostgreSQL server can carry is bounded by
max_connections / db.pool.max. The default max_connections is 100, so at db.pool.max: 20
that is five instances. Before this release the arithmetic was invisible — the setting said 30
and opened 60, so a second instance already exceeded the default and failed with
too many clients.
Nothing else changes. No DB migration, no configuration is newly required.
v4.6.1
Nothing to do on the server: no configuration change, no DB migration. One thing to check on the client side.
Check: the group fanout response member is m2m:rsp
A fanout retrieve (.../<grp>/fopt) returns the aggregated response as
{ "m2m:agr": { "m2m:rsp": [ /* one response primitive per member */ ] } }
The member used to be delivered as plain rsp, without the m2m: prefix, while
an issue on the conformance tester side was being worked around. rsp is
m2m:responsePrimitive in the TS-0004 symbol table, so it carries the prefix the
same way the surrounding m2m:agr envelope does, and it is now emitted that way.
A client that reads agr.rsp gets undefined and should read agr["m2m:rsp"].
Nothing inside the CSE consumes this key, so no server-side state is affected.
v4.6.0
Two required steps, and they have to be done together. Mobius4 will not start until the first one is done, by design.
Required: choose an administrator identity
cse.admin names an identity that the administrator <accessControlPolicy>
grants every operation to. Up to v4.5.1 it had a working default, SM, which is
published in this repository — anyone who could reach the port and send
X-M2M-Origin: SM had full control of the CSE, including DELETE, over plain HTTP
as much as over TLS.
There is now no default. Add the key to config/local.json:
{
"cse": {
"admin": "pick-something-unique-to-this-deployment"
}
}
Treat the value as a credential, not a name. Startup refuses to continue when it
is missing, blank, or SM, and warns when it is Superuser — the placeholder
printed in config/local.json.example, which is no more secret than SM is.
A refusal looks like this, and the process exits with status 1:
FATAL: cse.admin is not set. Set it to an identity unique to this deployment in
config/local.json (see config/local.json.example).
Required: DB migration
Configuration alone is not enough. db/init.js writes the administrator identity
into the database when it first creates the <CSEBase> and the default
<accessControlPolicy>, and never rewrites them — so the old value survives in
every resource's creator fields and in that policy's privileges. Without the
migration the new administrator cannot modify the default policy through the
standard path.
Edit new_admin in both DO blocks of the migration to the value you just
configured, then apply it:
psql -d mobius4 -v ON_ERROR_STOP=1 -f db/migrations/v4.6.0.sql
It runs as a single transaction and is idempotent — a second run reports zero rows changed rather than doing anything twice. If you miss the second block, the migration stops with a clear error and commits nothing.
A clean install needs neither step's migration — only the configuration.
db/init.js creates the administrator policy itself on an empty database.
What changes for you
The administrator no longer bypasses access control. It reaches a resource
through a policy that names it, or through the creator fallback when the resource
carries no accessControlPolicyIDs at all. Two consequences are worth checking
against your own data before upgrading:
- A resource whose policy list names only the default policy is no longer
deletable or updatable by the administrator. That policy grants
acop35 — create, retrieve, discovery — and nothing else. - A resource created by someone else with no policy list at all is governed by the creator fallback, and the administrator is not the creator.
The migration attaches the new administrator policy to every resource that already carries a policy list. Resources with an empty list are deliberately left alone: giving them one would switch them from the creator fallback to policy evaluation, and their creator would lose the update and delete rights it has today.
Known upgrade problem: resource names that are now invalid
resourceName is now checked against its ABNF (TS-0004:6.2.4). A name must
start with a letter or a digit; -, . and _ are allowed from the second
character onwards, and @ is not allowed at all. Names that violate this are
refused at CREATE with RSC 4000 where they previously returned 2001.
Existing resources are not touched and stay readable. This release also fixes
a defect that made a name beginning with _ unreachable by its hierarchical path
— created successfully, then answering 4004 to both retrieve and delete — so if
you have such names, this is the version in which you can finally delete them.
To find them before upgrading:
SELECT sid FROM lookup WHERE rn !~ '^[a-zA-Z0-9][-._a-zA-Z0-9]*$';
Clients that mint names from a template are the ones to check: a prefix like
_tmp or an identifier containing @ will start failing.
v4.5.1
Test prerequisite: mosquitto
Only relevant if you run the test suite. The suite now covers the MQTT binding
and starts its own broker, so mosquitto must be installed:
brew install mosquitto # macOS
sudo apt install -y mosquitto # Debian/Ubuntu
Only the binary on PATH is required. Unlike running Mobius4 itself, the
tests need no running instance and no mosquitto.conf — they spawn a broker on
a free port and shut it down afterwards. See test/README.md.
Nothing to do for a runtime upgrade.
v4.5.0
Required: DB migration
<flexContainer> (ty=28) adds a table. Apply:
psql -U <db_user> -d mobius4 -f db/migrations/v4.5.0.sql
The change is backward compatible — existing resources are untouched.
Back up first:
pg_dump -U <db_user> -d mobius4 -F c -f mobius4_backup_$(date +%Y%m%d).dump
v4.4.1
This release added Node 24 support. Node 22 remains supported
(engines: { node: ">=22" }), so no runtime change is forced — but if you do
move to Node 24, the problem below is the one you are most likely to hit.
TypeError: Utils.isRegExp is not a function on Node 24
TypeError: Utils.isRegExp is not a function
at _clone (node_modules/config/lib/config.js:1217:22)
at Config.cloneDeep (node_modules/config/lib/config.js:1255:10)
...
Node.js v24.x
config 1.x calls util.isRegExp, which Node 24 removed, so the server cannot
start. v4.4.1 fixed this by moving to config 3.3.12.
Seeing it after upgrading means the fix is in your source tree but an old
config is still installed. The code is fine; node_modules is stale.
Identify the installed version from the stack trace
The offending call sits on a different line in each release, so the crash names its own cause:
Line of Utils.isRegExp(parent) | Installed version |
|---|---|
| 1217 | 1.31.0 |
| 1087 | 2.0.2 |
| 1008 | 3.3.11 |
| (absent) | 3.3.12+ — this is what you want |
Diagnose
Both causes need the same fix, but they tell you different things about your checkout:
git branch --show-current
node -e "console.log('required :', require('./package.json').dependencies.config)"
node -e "console.log('installed:', require('./node_modules/config/package.json').version)"
git status --short
requiredis^1.30.0→ you are on a branch that predates v4.4.1.git pullupdates the branch you are on, notmaster, so a stale branch stays stale — andnpm installthen correctly installs the oldconfigthat branch'spackage.jsonasks for. This is the common case, and it is why the upgrade can look like it silently failed.requiredis^3.3.12butinstalledis1.31.0→ the branch is right and the install did not take.package.jsonshows as modified → a local edit is overriding the pinned version.
Fix
Commit or stash any work in progress first — this changes branches.
git checkout master
git pull
rm -rf node_modules
npm ci
Use npm ci, not npm install: it installs exactly what package-lock.json
pins and rebuilds node_modules from scratch, which is what clears a
mismatched tree.
Verify
node -v # v24.x
node -e "console.log(require('./node_modules/config/package.json').version)" # 3.3.12
v4.4.0
Required: DB migration
Schema changes including a destructive one (the cnt.cin_list column is
dropped). Read the dedicated guide before running anything:
DB Migration Guide — v4.4.0 — covers the dropped column, a widened column, eight new indexes, step-by-step instructions, and the rollback procedure.
Adding to this document
When a release introduces something an upgrader must do — or a failure mode that only bites people coming from an older version — add a section here rather than burying it in the CHANGELOG. The CHANGELOG explains what changed and why; this document is the actionable counterpart, and it is what the version-history table in README.md points at.
A good section states the symptom in the form the user will actually see it (an exact error message, a specific failure), then the diagnosis, then the fix, then how to confirm it worked.