Mobius4 is the next version of Mobius which basically implements the global IoT middleware standard, oneM2M. This new version provides the new code base with modern Javascript async-await syntax for better readibility and maintenance. Also, the database has been changed from MySQL to PostgreSQL with PostGIS.
In oneM2M Release 5, TR-0071 is defining candidate solutions for AIoT applications. Mobius4 implements those features in advance before the public release so developers can try them.
Mobius4 is the first product to be certified as a oneM2M Release 2 compliant Common Services Entity (CSE, also known as an IoT platform). By the configuration, it runs as ASN/MN-CSE as well as IN-CSE.

Two generated files sit alongside this prose and are worth reading when the answer has to be precise:
features/capabilities.jsonrecords what a running instance actually answered (regenerated bynpm run probe-capabilities, checked in CI), andfeatures/support-matrix.mdshows how far the implementation reaches across the standard, area by area.
Mobius4 implements oneM2M Common Services Entity (CSE) which is the IoT middleware. By the configuration, it runs as ASN/MN-CSE as well as IN-CSE.
oneM2M protocol bindings:
- HTTP
- MQTT — as a server, and outbound to any broker a notificationURI or a
<remoteCSE>'s pointOfAccess names, not only the one this CSE listens on
oneM2M primitive serialization:
- JSON
oneM2M resource types (until Release 4):
- CSEBase, AE, remoteCSE
- accessControlPolicy
- container, contentInstance, latest, oldest
- flexContainer (specializations; how to use · configuration)
- subscription
- group, fanOutPoint
oneM2M resource types (oneM2M TR-0071, next release):
- modelRepo, mlModel, modelDeployList, modelDeployment
- mlDatasetPolicy, dataset, datasetFragment
Other features:
- discovery with Filter Criteria parameter
- geo-query with location common attribute (Rel-4 feature)
- children resources retrieval with Result Content parameter — nested representations
(
rcn=4/rcn=8) and child resource references (rcn=5/rcn=6), paginated with Filter Criterialvl/lim/ofst. The same four values work on DELETE, returning what was removed
Beyond the oneM2M standard, Mobius4 includes the following operational capabilities for production deployments.
Observability:
- Structured JSON logging via Pino with daily log rotation (
logging.*) - Health check endpoint
GET /health— for load balancers and container liveness probes - Prometheus-compatible metrics endpoint
GET /metrics(disabled by default; enable viametrics.enabled)
Security:
- HTTP security headers via Helmet (disabled by default; enable via
security.helmet.enabled) - Per-IP rate limiting (disabled by default; enable via
security.rateLimit.enabled)
Resilience:
- Graceful shutdown on
SIGTERM/SIGINT— ordered teardown of HTTP, MQTT, and database connections with a 30-second forced-exit fallback - MQTT exponential backoff reconnection — configurable initial delay, multiplier, jitter, and max attempts (
mqtt.reconnect.*)
Operations:
- Local configuration override via
config/local.json(gitignored) — credentials and environment-specific settings never committed - PM2 process management via
ecosystem.config.js— auto-restart, environment profiles, graceful stop integration
A terminal tool for exploring a running Mobius4 — walk the resource tree, read attributes with their long names decoded, and watch resources change in real time over subscriptions.
It is a standard oneM2M client, not a Mobius4 add-on: everything it shows comes from ordinary
RETRIEVE, discovery and subscription requests, so it works against other CSEs too. Short names are
resolved from the TS-0004 tables (3 = container), timestamps are rendered in your local time, and
attributes that point at other resources are marked so you can follow them.
Download a build for your OS from the Releases page — no Python needed. See docs/operations.md for setup and the macOS note.
Try oneM2M APIs over HTTP binding with Postman client. You can download Postman script collection and import it on your Postman. There are two variables set in the collection mp_url for Mobius4 platform URL and cb for CSEBase resource name, so please add in your Postman variable settings.
There are some modifications from the previous version so please check Mobius4 how-to for the Mobius developers. If you're trying the new oneM2M features on AI, check Rel-5 features how-to.
For developers adding new oneM2M resource types to Mobius4: Adding a new resource type — covers every file to create or modify (enums, model, DDL, CRUD handler, dispatch switches, discovery maps, validation schema) with copy-paste code patterns.
For upgrading an existing deployment: Upgrading Mobius4 — the required steps (DB migrations, new prerequisites) and the known upgrade problems, per version. None of it applies to a clean install.
Since Mobius4 is developed with Node.js and PostgreSQL, any operating system that supports them can run Mobius4.
- Node.js v22 or v24 — both are supported (CI runs both on every change; see
engines). New installs should use v24, the current LTS and this repository's development default (see .nvmrc); existing v22 deployments continue to work unchanged. - PostgreSQL v17 — developed and CI-tested on 17.4.
- PostGIS v3.6 — required, not optional.
db/init.jsdeclaresGEOMETRY(GEOMETRY, 4326)columns on the resource tables, so schema creation fails without the extension even if you never issue a geo-query. Developed on 3.6.4; CI runs thepostgis/postgis:17-3.6-alpineimage. 3.x releases below 3.6 are expected to work but are not tested here. Enable it per database withCREATE EXTENSION postgis;. - MQTT broker (e.g. Mosquitto)
Node dependencies come with npm install and are not on the list above, with one worth naming:
fast-xml-parser— reads specialization XSDs inscripts/build-specializations.js. Nothing on a request path uses it; the script is an operator command, run when a<flexContainer>specialization is added. It is a runtime dependency rather than a development one because the deployment image carries the script, so a Docker operator can build the registry in a container instead of installing Node on the host:docker compose run --rm --no-deps --entrypoint node -v "$PWD/config:/app/config" mobius4 scripts/build-specializations.js. See docs/examples/specializations/ for what the script is for and why the command looks like that.
With Docker, there is nothing on this list to install and no database to create by hand — see Installation → With Docker Compose below.
For OS-specific installation instructions (Windows, macOS, Linux): docs/installation.md
Two ways. Docker Compose is the shorter one — it brings up Mobius4, PostgreSQL with PostGIS and an MQTT broker together, so nothing on the Prerequisites list has to be installed by hand. Install natively when you want Mobius4 running directly on the host, or already have a database and broker to point at.
git clone https://github.com/iotketi/mobius4
cd mobius4
cp .env.example .env
# edit .env: DB_PW and CSE_POA at least
docker compose up -d
curl localhost:7599/healthThat is the whole install — no Node, no database, no manual createdb. The schema is created on
first boot.
| Service | Image | Published to the host |
|---|---|---|
mobius4 |
built from this repository | 7599 (HTTP), 7580 (HTTPS, when enabled) |
postgres |
postgis/postgis:17-3.5 |
no |
mosquitto |
eclipse-mosquitto:2 |
no |
The database and the broker are reachable from the compose network and nowhere else. For a psql
session, use docker compose exec postgres psql -U "$DB_USER" "$DB_NAME".
To upgrade later:
git pull
docker compose build
docker compose up -dOn Apple Silicon and other arm64 hosts, the official PostGIS image is
linux/amd64only and the pull fails withno matching manifest for linux/arm64. SetPOSTGRES_IMAGEin.envto a multi-architecture build —.env.examplenames two.
Everything else — HTTPS, MQTT, the administrator identity, backup and restore, registering with another CSE, and what to check before upgrading — is in docs/docker.md.
-
Create a database named
mobius4on PostgreSQL -
Get Mobius4 source codes from this git repository
git clone https://github.com/iotketi/mobius4- Install node packages in the 'mobius4' folder
cd mobius4
npm installIf you manage Node.js versions with nvm, run nvm use inside this folder first — it picks up the version pinned in .nvmrc (v24) automatically.
- Set Mobius4 configuration file
cp config/local.json.example config/local.json
# edit config/local.json with your DB credentials and local settings- Run Mobius4
node mobius4.jsFull configuration reference: docs/configuration.md
For deployment details (health check, metrics endpoint, PM2, resource browser): docs/operations.md
To serve the HTTP binding over TLS — obtaining a certificate, installing it, replacing it before it expires, and what it does and does not prove about the client: docs/tls.md
Full detail for every release is in CHANGELOG.md. The Upgrading column links to what an existing deployment has to do — required steps and known upgrade problems. A clean install needs none of it.
| Version | Date | Description | Upgrading |
|---|---|---|---|
| 4.0.0 | 2025-09-22 | Initial release of Mobius4 | — |
| 4.1.0 | 2026-03-13 | oneM2M Rel-2 certification | — |
| 4.2.0 | 2026-04-05 | logging module update | — |
| 4.3.0 | 2026-04-09 | performance improvements | — |
| 4.4.0 | 2026-04-19 | conformance updates for performance improvements | DB migration required |
| 4.4.1 | 2026-08-01 | Node.js 22/24 CI, dead dependency cleanup, DAS/jose removal, installation docs update |
Node 24 notes |
| 4.5.0 | 2026-08-02 | <flexContainer> (ty=28) with a specialization registry; response-status fallback in the HTTP binding |
DB migration required |
| 4.5.1 | 2026-08-02 | MQTT binding test coverage | test prerequisite |
| 4.6.0 | 2026-08-02 | Breaking: cse.admin has no default and SM is refused; the administrator's privileges now come from an <accessControlPolicy> rather than a bypass; resourceName is checked against its ABNF. Closes a full access-control bypass |
Will not start until configured; DB migration required |
| 4.6.1 | 2026-08-05 | Conformance: <CSEBase> UPDATE/DELETE answer 4005 for every originator; the group fanout member is named m2m:rsp |
client-side check |
| 4.6.2 | 2026-08-05 | <contentInstance> creation and retention each become a single SQL statement — roughly 3.3× the write throughput, 2.5× with retention active; stateTag no longer collides under concurrent creates |
— |
| 4.6.3 | 2026-08-05 | db.pool.max is the connection total for the process rather than a figure each of two pools applied separately; default 30 → 20. Unblocks running more than one instance |
only if you overrode db.pool.max |
| 4.6.4 | 2026-08-05 | A name that is already taken is refused with 4105 rather than 4000, including under concurrency; MQTT subscription and expired-resource cleanup run on one instance | — |
| 4.6.5 | 2026-08-06 | Conformance: a <contentInstance> under a <container> carrying an <accessControlPolicy> was refused to every originator, the administrator included, and was missing from discovery results — it now follows the parent's policy as TS-0001:9.6.7 requires. Discovery decides access once per policy holder instead of once per resource: 18 → 614 requests per second over 150 content instances. Development logging settings carried into a deployment now say so at startup |
workarounds you can undo |
| 4.7.0 | 2026-08-07 | HTTPS is now optional and off by default — an existing deployment must set https.enabled and point https.key/https.cert at its own files to keep serving TLS. The listener no longer asks clients for certificates: it set requestCert but nothing ever read the certificate, so it never proved the originator. The certificates this repository shipped are deleted and must be treated as disclosed |
Set https.enabled to keep TLS; reissue the shipped keys |
| 4.8.0 | 2026-08-07 | docker compose up brings up mobius4, PostgreSQL/PostGIS and an MQTT broker in one command — see docs/docker.md. No change for an existing source deployment |
— |
| 4.9.0 | 2026-08-07 | A <container>'s maxInstanceAge now actually caps its <contentInstance> children's expirationTime (TS-0004:7.4.7.2.1 step 2 e), and its default widens from 30 to 365 days so a default container keeps behaving as before; new maxByteSizePerInstance (mbis) refuses oversized content independently of maxByteSize |
DB migration required |
| 4.10.0 | 2026-08-07 | Breaking for rcn=4/rcn=8 clients: child resources are now nested inside their own parent instead of grouped by type at the top level (TS-0004:8.4.3 EXAMPLE 3), and lim cuts on subtree boundaries while ofst counts direct children. Both fail silently against an old client. rcn=5/rcn=6 are implemented — they previously returned attributes only, with RSC 2000. Truncated child-resource results now set X-M2M-CTS/X-M2M-CTO |
Client-side changes required |
| 4.11.0 | 2026-08-08 | contentSize now counts bytes (TS-0001:9.6.7) instead of JavaScript string units — a 10-byte payload was being refused by a maxByteSizePerInstance of 10, and cbs/mbs/sizeAbove/sizeBelow read the same figure. A database failure answers 5000 instead of 4004/4000/4103, so a client retries rather than trying to recreate live resources. /health became a readiness check and now fails when the database is unreachable. <subscription> sets creator and notifications carry it (TS-0004:7.4.8.2.1, 7.5.1.2.2); creator can no longer name another entity |
Read before upgrading — contentSize values change |
| 4.11.1 | 2026-08-08 | Forwarding to a <remoteCSE> kept the remote CSE's response status instead of replacing it with 2000, and now tries every pointOfAccess before answering 5103 TARGET_NOT_REACHABLE; an mqtt: access point is refused rather than reported as success. A generated resourceName is checked for collision before use. <AE> mandatory-attribute validation moved into the Joi schema. Dead module cse/routing.js removed |
— |
| 4.12.0 | 2026-08-08 | DELETE honours rcn 4/5/6/8 (TS-0001:8.1.2 Table 8.1.2-1) — the response can now carry the child resources, or references to them, as they were just before removal. Notifications go to the broker their mqtt:// URL names instead of always to this CSE's own (TS-0010:6.6.2/6.6.4), and a <remoteCSE> whose pointOfAccess is mqtt:// can now be forwarded to (TS-0010:6.4.2/6.4.3). All additive |
— |
| 4.13.0 | 2026-08-08 | Group members hosted on another CSE now work. A member the CSE did not host resolved to resource type 0, was dropped by the default consistency strategy, and the group was still returned with memberTypeValidated = true — it now retrieves the member's type from its Hosting CSE and distinguishes readable / no-privilege / unreachable as TS-0004:7.4.13.2.1 requires. <AE> accepts ontologyRef, and an <AE> UPDATE no longer discards contentSerialization. A fanout over a group with no members answers 4109 NO_MEMBERS instead of 2000 with an empty list. Forwarded responses carry the Response Status Code as a number, not a string. Conformance tests are now transcribed from TS-0018 test purposes (209 → 282) |
DB migration required |
| 4.13.1 | 2026-08-08 | A containerised CSE could not be told to register with another CSE: docker/entrypoint.js assembled NODE_CONFIG without a cse.registrar block and without cse_type, and it overwrites NODE_CONFIG, so the settings could not be injected from outside either. Six optional variables added, plus a working two-CSE compose example. No change for a standalone deployment |
— |
| 4.14.0 | 2026-08-11 | Expired resources stop acting like live ones before the sweep deletes them: an expired <subscription> no longer publishes notifications, and an obsolete <contentInstance> is no longer served by <latest>/<oldest> or listed among rcn=4/rcn=8 children (TS-0001:10.2.4.4) — which also means maxInstanceAge is now enforced on reads, not only on writes. The sweep runs at startup, so a deployment restarting more often than its interval no longer skips it entirely. Breaking if you compute ofst yourself: the offset filter is 1-based, so ofst=1 is the first result rather than the second (TS-0004:7.3.3.17.15); X-M2M-CTO moves with it and is no longer ever 0. Internal error text no longer reaches clients in m2m:dbg |
Only if you compute ofst |
| 4.15.0 | 2026-08-15 | The administrator bypasses access control again. cse.admin is granted every operation before any <accessControlPolicy> is read, reversing the v4.6.0 change above: a resource created with no accessControlPolicyIDs is governed by its creator, which left the administrator unable to reach it and unable to attach a policy to it — no request could undo the state. Deliberately non-conformant; cse.admin must be treated as a credential. Five of the seven AI/ML resource types were unusable past CREATE and now work; <mlModel> stores decoded bytes, so mlModelSize stops over-reporting by a third |
DB migration required |
| 4.15.1 | 2026-08-15 | Discovery and rcn=4/rcn=8 return child resources newest first, in an order that no longer changes between identical requests. oneM2M specifies no order (TS-0001:8.1.2), and the old oldest-first sequence was a side effect of a query with no ORDER BY — which also meant ofst paging could skip or repeat a resource |
only if you relied on oldest-first |
| 4.16.0 | 2026-08-16 | <timeSeries> (ty=29) and <timeSeriesInstance> (ty=30): CRUD, retention (maxNrOfInstances/maxByteSize/maxInstanceAge), the mandatory <latest>/<oldest> children, and dataGenerationTime unique within a parent. Missing-data detection records absent data points into missingDataList (TS-0001:10.2.4.29) — a periodic sweep, since a gap is the absence of a request. Not yet: the missingData subscription condition and notificationEventType=8, so nothing is notified when a point goes missing |
DB migration required |
| 4.16.1 | 2026-08-19 | The cr (creator) discovery filter answered 5000 instead of narrowing by creator — it now filters on the cr column (TS-0004:7.3.3.17). No change for a deployment that does not use the cr filter |
— |
| 4.16.2 | 2026-08-24 | <latest>, <oldest> and <fanOutPoint> could not be reached under a parent whose resourceName starts with la, ol or fopt — a container named lamp answered 404 for lamp/la. The virtual resource name is now matched as a whole path segment. No change for a deployment whose resource names do not begin with those three |
— |
| 4.17.0 | 2026-08-26 | The MQTT registration topic TS-0010:6.4.4 defines (/oneM2M/reg_req and /oneM2M/reg_resp) is now served — an Originator that does not yet know its AE-ID can register there, where before the CSE did not subscribe and sent no answer at all. It accepts <AE> and <remoteCSE> creation only, and it authenticates nothing: the Credential-ID segment is an opaque string. Registration over the ordinary /oneM2M/req topic is unchanged |
nothing required, but read the caveat |
| 4.17.1 | 2026-08-27 | Six answers the CSE did not mean. An unimplemented resource type says 5001 NOT_IMPLEMENTED rather than 5000, which meant "the server broke"; a <container>'s mni/mbs/mia/acpi/lbl/loc can now be cleared with null as oneM2M intends, having been refused 4000; MQTT xml/cbor requests are refused instead of vanishing; and a CSE-created <dataset> notifies subscribers as <datasetFragment> already did. Breaking for anyone creating <dataset>/<datasetFragment> directly — TR-0071 defines no such API and it is now refused |
only if you create datasets directly |
| 4.18.0 | 2026-08-28 | node scripts/build-specializations.js builds config/specializations.json from a manifest, reading each <flexContainer> specialization's custom attributes out of its XSD instead of having them transcribed by hand. A failure names the cnd and leaves the existing registry byte-for-byte unchanged, and a cnd the manifest no longer lists stops the build rather than vanishing from it. Nothing the CSE answers changes: same registry format, still read once at startup, cse/ untouched. The build overwrites the registry, so entries added by hand have to move into the manifest first |
only if you edited config/specializations.json by hand |
| 4.19.0 | 2026-09-01 | <subscription> filtering actually filters. Eleven eventNotificationCriteria conditions that used to be refused now work: atr (notify only when named attributes change), the ten value comparisons (crb cra ms us sts stb exb exa sza szb) and fo to combine them with AND/OR/XOR. A notificationEventType outside 1–8 is refused 4000, and one this CSE does not implement 5001 — before, such a subscription was created and then never fired. Breaking for anyone sending enc.om, which was accepted and silently ignored and is now refused. Discovery's ms/us filters were returning the opposite set and are fixed; sts/stb answered 5000; sza/szb never worked. <flexContainer> specialization validation now enforces mandatory attributes — rebuild the registry to turn it on |
only if you send enc.om |
| 4.20.0 | 2026-09-01 | A <subscription> on a <timeSeries> can ask to hear about missing data points: enc.md with notificationEventType 8 notifies when the number missing reaches a threshold inside a renewable window. Detection has been recorded since 4.16.0; this reports it. notificationContentType is now range-checked to 1–5 |
a migration is required |
| 4.20.1 | 2026-09-02 | Notifications were missing the X-M2M-RVI header. It is mandatory on a request primitive, and a receiver that validates request parameters rejected the notification for it — which looks like a lost notification from the sending side. Fix it if anything subscribes over HTTP |
nothing required |
| 4.21.0 | 2026-09-02 | node scripts/reset-resources.js empties a test deployment: every oneM2M resource goes, and configuration, the schema, PostGIS and the administrator identity stay. Safe by default — it prints what it would delete and needs --yes, and refuses while the CSE is connected |
nothing required |
| 4.22.0 | 2026-09-02 | scripts/reset-resources.js ships in the deployment image, so a Docker deployment can empty a test environment without a Node toolchain on the host. The entrypoint now runs the arguments it is given — it used to ignore them and start a CSE instead, minting an administrator identity on the way |
nothing required |
| 4.22.1 | 2026-09-03 | Request forwarding sent the wrong To and the wrong Request Identifier. The target CSE-ID was cut out of To, which breaks the hop after it and, for two To shapes, sent the request somewhere else entirely; and the Originator got back a Request Identifier it never sent, so it could not correlate the response. Forwarded requests are also logged now |
nothing required |
| 4.22.2 | 2026-09-03 | notificationContentType now does what it says. "Modified Attributes" sends the attributes that actually changed — including lt and st, which an UPDATE moves without being asked; "ResourceID" sends a URI instead of the whole resource; subscribedTo is sent where the spec requires it. Also: ?atrl= partial retrieve never worked, and a forwarded DELETE carried {} as its body |
read if you subscribe with nct |
| 4.22.3 | 2026-09-03 | A CSE that never answers no longer holds a forwarded request open. The forwarding call had no timeout at all, so an unresponsive <remoteCSE> held the request until the operating system gave up — indistinguishable from a lost request. Bounded now, and both timeouts are configuration |
nothing required |
| 4.22.4 | 2026-09-03 | cse.keep_alive_timeout had never taken effect. It was assigned to the wrong property name, so every deployment ran on the 5-second default whatever it configured, and a client holding an HTTP session open had it closed under it. Raise the setting to hold sessions longer |
read if you set keep_alive_timeout |
| 4.22.5 | 2026-09-03 | A subscription that omitted notificationContentType stored the wrong default for "Report on missing data points" -- notifications were correct, but retrieving the subscription reported a value the standard marks n/a for that event type. Also adds the first tests for a notificationURI given as a resource ID rather than a URL |
nothing required |
| 4.23.0 | 2026-09-03 | <timeSeries> periodicInterval, periodicIntervalDelta and missingDataDetectTimer are milliseconds, not seconds. They were read as seconds, which no longer matches what a conformance tester expects. A migration multiplies stored values so existing resources keep the intervals they have |
run the migration |
| 4.24.0 | 2026-09-03 | Subscription verification, both directions: a <subscription> can be made to prove its notification target accepts it before being created, and an incoming verification request is now answered on its merits instead of being accepted unread. Off by default — the standard says "may" |
nothing required |
| 4.24.1 | 2026-09-03 | <timeSeries> missing-data detection was up to 30 seconds late. A gap that the standard makes detectable at expected + missingDataDetectTimer was only reported when the next fixed sweep came round; the sweep now paces itself from the data. cse.missing_data_sweep_interval_seconds changes meaning — it is a ceiling now, not a cadence |
read if you tuned the sweep interval |
| 4.24.2 | 2026-09-03 | Completes v4.24.1. That release still missed detection on a CSE that had been running: with nothing detecting, the sweep slept the full configured interval and did not notice a <timeSeries> created during it. It is now woken where a resource becomes detectable |
nothing required |
| 4.24.3 | 2026-09-03 | The subscription verification notification was missing subscriptionReference, which TS-0004 table 6.3.5.13-1 makes mandatory on every notification. A receiver validating the data type rejected it. Only affects deployments with cse.subscription_verification on |
nothing required |
| 4.24.4 | 2026-09-03 | A <subscription> sending notificationContentType as a JSON string was refused, with a message naming a combination the standard allows. Only the validity check was affected — the value would have been stored correctly |
nothing required |
| 4.24.5 | 2026-09-07 | A notification sent to an <AE> by resource ID carried an empty To — it went to the bare pointOfAccess, which is where to send it, not what it addresses. Affects subscription verification too. Outgoing notifications are now readable in the log, and one that cannot be delivered at all no longer passes in silence |
read if a notificationURI names a resource |

