Mobius 3.0.1

oneM2M IN-CSE · Guide

From installation to everyday use

Mobius is an IoT server platform that implements the oneM2M international standard. This guide walks through what kind of software Mobius is and how it is put together inside, how to install and start it, and how to work with it in a real service.

Version 3.0.1 Database MySQL · SQLite Runtime Node.js License BSD 3-Clause

01 — Introduction

Introduction

What Mobius is

Mobius is server software that receives the data an IoT device sends, stores it, and makes it available for applications to read.

When every project builds that server for itself, the addressing rules, the data format and the way permissions are handled all end up different from product to product. oneM2M is the international standard that settles those questions, and Mobius implements the role that sits at the top of it: the IN-CSE, or Infrastructure Node Common Services Entity. Because Mobius follows the standard closely, devices and applications written against it also work against any other oneM2M platform without modification.

Mobius is written in Node.js and its source is open. It can store resources in either MySQL or SQLite, and which one it uses comes down to a single setting.

In one sentence

Devices push data up and applications pull it back down, while Mobius sits between them handling storage, identity and notifications.

Where it sits

Mobius is middleware placed between devices and applications. When a device uploads a reading, Mobius stores it, and an application then reads that value through the standard REST API. The same holds in the other direction: when an application writes a control value, any device that subscribed to the resource receives the change as a notification.

IoT devices sensors · gateways HTTP notify Mobius oneM2M IN-CSE store · authorize · notify REST API Applications web · analytics Database MySQL or SQLite MQTT broker for notifications
Figure 1. Devices upload, Mobius stores, and applications read over REST. When a subscribed resource changes, a notification goes out, optionally through a broker.

The tree becomes the address

oneM2M data is stored as a tree, much like folders in a file explorer, and the path down that tree becomes the URL. This means you never have to design an API for a resource you have just created: the moment the resource exists, so does the address you reach it at.

RESOURCE TREE TYPE ADDRESS Mobius CSEBase · ty=5 /Mobius myAE AE · ty=2 /Mobius/myAE sensor container · ty=3 /Mobius/myAE/sensor 4-2026… contentInstance ty=4 …/sensor/la latest one via la watch subscription · ty=23 …/sensor/watch
Figure 2. Every node of the tree corresponds to one segment of the URL. Create a resource and it has an address straight away, which you work with through POST, GET, PUT and DELETE.

There are very few structural constraints. A container may hold another container, and the depth is not limited. Every resource also carries the attributes the standard defines, among them the creation time (ct), the name (rn), the expiration time (et) and the access policy in effect (acpi).

Resource types

The table below lists the resource types Mobius can create. The ty number goes into the request's Content-Type to tell the server which kind of resource you mean to create.

tyNameWhat it does
1accessControlPolicyDecides who is allowed to do what
2AEA single device or application, and its entry point into the tree
3containerHolds data, with optional retention limits
4contentInstanceOne reading, written once and never modified
5CSEBaseThe root of the tree, which is Mobius itself
9groupTreats several resources as one
10locationPolicyDefines how location data is collected
13mgmtObjDevice management objects such as firmware, battery and device info
14nodeA physical node
16remoteCSEAnother CSE this one is registered with
23subscriptionWatches for change and sends notifications
24semanticDescriptorCarries semantic annotation
27multimediaSessionA multimedia session
28flexContainerA container whose attributes you define freely
91–98hd_*Home-domain moduleClasses: lighting, colour, door lock, temperature, battery
Running on SQLite

The SQLite backend supports six types: accessControlPolicy, AE, container, contentInstance, CSEBase and subscription. Creating a type outside that list is refused with 501 before anything is written, so the resource tree is never left half-built. If you need every type, run on MySQL instead.

02 — Architecture

Architecture

The modules of Mobius are divided along the path a single request takes. Once you know that path, it becomes clear which file to open when you want to change a particular behaviour.

How a request is handled

An incoming HTTP request moves through the stages below. Each stage finishes only the part it owns and then hands the request on.

HTTP request app.js body · header checks · type resolution · routing borrows a connection, opens the settler mobius/resource.js create · retrieve · update · delete · discovery security.js checks access policy sql_action.js builds the query sgn.js sends notifications mobius/db/ MySQL · SQLite adapters responder.js JSON · single exit
Figure 3. A request clears its checks in app.js and moves on to resource.js. The response always leaves through responder.js and nowhere else.

Two rules hold this flow together. First, sending the response and returning the database connection happen exactly once per request. Second, the code that actually writes a response lives in responder.js and nowhere else. Sending twice, or failing to send at all, would leave a connection stranded, so both rules are enforced by the structure of the code rather than by convention.

How the modules divide

These are the files you will find yourself opening most often.

FileResponsibility
mobius.jsThe entry point. It only sequences the steps: read the configuration, check the port, write the boot record, start the server
app.jsThe HTTP server and the routing. It forks the workers and receives incoming requests
mobius/resource.jsThe core of resource handling, including per-type attribute checks and the create and update paths
mobius/sql_action.jsWhere every SQL statement is built, assembled by the query builder with values passed as bindings
mobius/db/The database facade together with the backend adapters and the schema files
mobius/security.jsChecks the access policy to decide whether a requester may touch a resource
mobius/sgn.js · sgn_man.jsFinds the subscriptions, builds the notification body and sends it over HTTP, CoAP or MQTT
mobius/responder.js · shape.jsThe single exit a response leaves by, and the place the response body is assembled
mobius/settle.jsThe per-request settler that guarantees the response and the connection return happen only once
mobius/conf_load.js · conf_schema.jsWhere the configuration is read, and the single source of truth for which settings exist
tools/The first-run wizard, the settings commands and the schema migration runner
admin/The admin console, which runs as a process of its own

Master and workers

Mobius forks one worker process per CPU core and spreads incoming requests across them. The master process takes no part in handling requests; it is responsible only for two jobs that run on a schedule.

  • Retention sweep: deletes the oldest data once a container passes the count or size limit set on it. Because it counts the live rows again immediately before deleting, an inflated stored counter can never cause it to remove data that is still within the limit.
  • Counter reconciliation: brings the count and size a container records back in line with the data actually stored.

If a worker exits unexpectedly, the master starts a replacement right away. When the exit was caused by a configuration problem, however — the port already in use, a missing configuration file, or a secret whose seal no longer matches — no replacement is started and the master exits with the same code. This is deliberate: a problem that a person has to fix should not be papered over by restarts.

The database layer

Building SQL and running it are kept apart. Once sql_action.js has finished a statement, the facade in mobius/db/ passes it to the adapter for whichever backend is currently selected. As a result, core code never needs to know whether it is running on MySQL or on SQLite.

Because of that separation, switching backends takes a single setting, and adding a new one amounts to dropping in a single mobius/db/<name>.js file. Every value that goes into a query travels as a binding without exception, which is what makes SQL injection through discovery parameters structurally impossible.

Directory layout

Mobius/
mobius.js            # entry point — this is what you run
app.js               # HTTP server, workers, routing
package.json         # dependencies and npm commands
conf.json            # settings, written on the first start

mobius/              # the core
  resource.js        # resource handling
  sql_action.js      # SQL construction
  security.js        # access control
  sgn.js sgn_man.js  # subscription notifications
  responder.js       # response exit
  conf_load.js …     # configuration loading
  ae.js cnt.js …     # one file per resource type
  db/
    index.js         # backend facade
    mysql.js         # MySQL adapter
    sqlite.js        # SQLite adapter
    mobiusdb.sql     # MySQL schema — import this at install time
    mobiusdb_sqlite.sql

tools/               # setup wizard, settings commands
admin/               # admin console (separate process)
log/                 # access log and boot record

03 — Installation

Installation

What you need

ComponentRequiredNotes
Node.jsYesThe runtime Mobius executes on. An LTS release is recommended
MySQLOptionalThe store for production. Needed if you want every resource type
SQLiteAutomaticInstalled together with the modules, so there is nothing to prepare
MQTT brokerOptionalOnly needed when notifications go to mqtt:// addresses, such as Mosquitto
Trying it for the first time

Start with SQLite. There is no database server to install, and the store is created for you on the first start. Node.js is the only thing you need in place.

Order of work

Node.js install runtime Get the source npm install Using MySQL create DB, import schema Using SQLite nothing to do First start setup wizard
Figure 4. Only the middle step changes depending on the database. With SQLite there is nothing to prepare at all.

Installing Node.js

Download an LTS release from nodejs.org and install it. Once that is done, check in a terminal that both commands below print a version.

terminal
node -v
npm -v

Preparing the database

Using SQLite

There is nothing to prepare. Running npm install in the next step brings in the SQLite module, and the store file along with its tables is created the first time the server starts.

Using MySQL

Install a MySQL server, create one database and import the schema file into it. That single import is the entire installation. The schema file already contains the tables, the indexes and the migration ledger, so nothing is left to apply afterwards.

terminal
# 1. create the database
mysql -u root -p -e "CREATE DATABASE mobiusdb DEFAULT CHARACTER SET utf8mb3;"

# 2. import the schema (run this from the Mobius source folder)
mysql -u root -p mobiusdb < mobius/db/mobiusdb.sql

If you prefer a tool such as MySQL Workbench, create the mobiusdb schema and then use Data Import → Import from Self-Contained File with mobius/db/mobiusdb.sql. The result is exactly the same.

Database name

The default is mobiusdb. To use a different name, run npm run conf -- set dbName <name> once the installation is finished.

MQTT broker (optional)

A broker is needed only if notifications will go to mqtt:// addresses. In that case, install Mosquitto and leave its service running. If your notifications go over HTTP only, you can skip this step entirely.

Source and modules

  1. Get the source

    terminal
    git clone https://github.com/IoTKETI/Mobius.git
    cd Mobius

    If Git is not installed, download the ZIP from the GitHub page and unpack it instead.

  2. Install the modules

    terminal
    npm install

    When the command finishes and a node_modules folder has appeared, you are done. Node.js has no build step, so the installation ends here.

04 — Running

Running

Starting for the first time

When there is no conf.json, Mobius runs the setup wizard by itself, so there is no separate install command to remember.

terminal
node mobius.js

It asks seven questions in turn, saves your answers to conf.json, and then starts the server straight away.

QuestionDefaultNotes
DatabasemysqlChoose either mysql or sqlite
DB password—Asked only when you chose MySQL, and hidden while you type
CSE nameMobiusThe name of the root. Every address begins with /<name>
CSE-ID/Mobius2The identifier of this CSE
SP-ID//keti.re.krThe identifier of the service provider
Super-user originSpondeA request sent with this origin passes every access check
HTTP port7579The port the server listens on
The super-user origin is a master key

Any request carrying that value in its header passes every access check. On a server reachable from outside, do not leave the default in place: change it to something hard to guess and keep the number of people who know it as small as possible.

If you kept the CSE name and the port at their defaults of Mobius and 7579, the top of your resource tree is now http://localhost:7579/Mobius.

Starting it again

terminal
node mobius.js          # use the settings in conf.json
npm start               # the same command

node mobius.js sqlite   # force SQLite for this run only
node mobius.js mysql    # force MySQL for this run only
Running under a supervisor

Started for the first time under pm2 or systemd, where no terminal is attached, Mobius cannot show the wizard and so exits without writing a configuration file. Run it once from a terminal to create conf.json, then register it with the supervisor.

Checking that it works

Retrieve the top-level resource once. If a response like the one below comes back, the server is running correctly.

terminal
curl -i http://localhost:7579/Mobius \
  -H "Accept: application/json" \
  -H "X-M2M-RI: check1" \
  -H "X-M2M-Origin: Sponde"
response
HTTP/1.1 200 OK
X-M2M-RSC: 2000
Content-Type: application/json

{"m2m:cb": {"rn": "Mobius", "ty": 5, "csi": "/Mobius2", …}}

Here X-M2M-RSC: 2000 is the success code defined by oneM2M. It is separate from the HTTP status code, so it is worth reading both when you check a response.

Reading and changing settings

Settings are handled from the command line rather than through a web page. Since conf.json holds sensitive values such as the database password and the super-user origin, this is deliberate: only someone who can reach the server should be able to change them.

terminal
npm run conf                      # list every key and whether it is applied
npm run conf -- csebaseport       # look at one key in detail
npm run conf -- set cseBase Vita  # change a value
npm run conf -- unset maxBodyBytes # restore the default
npm run conf -- edit              # walk through the main keys again
npm run conf -- --all             # include the advanced keys

npm run status                    # is it running, is a restart pending

The configuration file is read once, when the server starts. A value you change therefore takes effect only after a restart, and the npm run conf listing tells you which keys are still waiting for one.

Keys that would cut existing devices off if set wrongly, such as the CSE name or the port, are marked as gated. Changing one of them prints a warning first and asks you to type the key name before the change is written.

Sealed secrets

The database password and the super-user origin are only ever changed through their own command. If you open conf.json in an editor and edit either of them by hand, the next start is refused. A companion file in the same folder, conf.seal.json, keeps a fingerprint of the two values and catches the mismatch before the server comes up.

terminal
npm run setup -- --dbpass      # re-enter the DB password
npm run setup -- --superuser   # re-enter the super-user origin

At either prompt you can simply press Enter, which keeps the current value and only recreates the seal. If you have upgraded from an older version and have no seal yet, doing this once is all that is needed.

Admin console

The admin console ships in the same repository as Mobius but runs as a separate process. It is an operator screen: it finds and clears expired resources and resources whose parent is gone, lets you review access policies, and shows subscription state and statistics.

terminal
# set a password first; without one the console will not start
npm run conf -- set adminPassword 'your-password'

# start the console
node admin/server.js

# or have Mobius start it as a child of the master process
npm run conf -- set adminAutoStart on

Changing settings and controlling the process were deliberately left out of the console. Both of those are done with the commands described above.

When it will not start

Mobius reports why it failed to start through its exit code, so that it never ends up in the awkward state of being alive as a process while unable to serve any request.

CodeMeaningWhat to do
12The port is already in useStop whatever is holding it, or change csebaseport
13There is no conf.jsonRun node mobius.js once from a terminal to create it
14The seal on a secret does not matchRecreate it with npm run setup -- --superuser
1The database could not be reachedCheck that the database is up and that the password and name are right

Per-request timings are not printed to the console; they are written as the last field of log/access-*.log. The configuration a running server actually applied is recorded in log/mobius-boot.jsonl, and npm run status reads that record and presents it for you.

05 — Usage

Usage

The examples below work as written if you run them from top to bottom. They assume a server running at localhost:7579 with the CSE named Mobius.

The shape of a request

Every request sent to Mobius needs at least two headers.

HeaderPurpose
X-M2M-RIThe request identifier. Any value will do as long as it differs per request; it comes back unchanged in the response
X-M2M-OriginWho is asking. Access control decides on the basis of this value
Content-TypeNeeded only when creating a resource, as application/json; ty=<number>, which says what to create
AcceptSet to application/json. Mobius speaks JSON and nothing else

Registering an AE

Before anything else, a device or application registers itself as an AE, which you can think of as claiming its own place inside the resource tree. Send exactly S in the X-M2M-Origin header and Mobius will generate an identifier and return it to you.

create an AE
curl -X POST http://localhost:7579/Mobius \
  -H "Accept: application/json" \
  -H "X-M2M-RI: r-ae-1" \
  -H "X-M2M-Origin: S" \
  -H "Content-Type: application/json; ty=2" \
  -d '{"m2m:ae": {"rn": "myAE", "api": "A.company.myapp", "rr": true}}'
response (201 · RSC 2001)
{"m2m:ae": {
  "rn": "myAE",
  "ty": 2,
  "aei": "S2026090812345601a3",   <-- keep this value
  "ri":  "2-2026090812345601a3",
  …
}}

The aei you get back serves as this AE's identity. From now on, put it in X-M2M-Origin on every request this AE makes. If you would rather use an identifier of your own, send that instead of S and it becomes the aei as it is.

  • rn: the resource name, which becomes one segment of the address
  • api: the application identifier, a value the standard requires
  • rr: whether this AE is able to receive requests

Creating a container

A container is the vessel that holds your data, and it is created under the AE you just registered.

create a container
curl -X POST http://localhost:7579/Mobius/myAE \
  -H "Accept: application/json" \
  -H "X-M2M-RI: r-cnt-1" \
  -H "X-M2M-Origin: S2026090812345601a3" \
  -H "Content-Type: application/json; ty=3" \
  -d '{"m2m:cnt": {"rn": "temperature", "mni": 1000}}'

mni sets how many entries this container will keep. Once that number is passed, the master's retention sweep removes the oldest ones. To limit by size instead of by count, give mbs in bytes. If you set neither, entries accumulate essentially without bound, so it is worth deciding on one.

Storing and reading data

Actual readings are stored as a contentInstance. Since it is a record that is written once and never modified, the normal pattern is to add a new one each time a sensor reports a value.

store one reading
curl -X POST http://localhost:7579/Mobius/myAE/temperature \
  -H "Accept: application/json" \
  -H "X-M2M-RI: r-cin-1" \
  -H "X-M2M-Origin: S2026090812345601a3" \
  -H "Content-Type: application/json; ty=4" \
  -d '{"m2m:cin": {"con": "23.5"}}'

The content itself goes in con. That field is a string, so to store JSON you first serialise it and put the string in.

Reading the latest value

most recent reading
curl http://localhost:7579/Mobius/myAE/temperature/la \
  -H "Accept: application/json" \
  -H "X-M2M-RI: r-la-1" \
  -H "X-M2M-Origin: S2026090812345601a3"

la points at the most recently stored value and ol at the oldest one. This is the address a dashboard uses most often when it needs to show a current reading.

Reading the container itself

container state
curl http://localhost:7579/Mobius/myAE/temperature \
  -H "Accept: application/json" \
  -H "X-M2M-RI: r-cnt-2" \
  -H "X-M2M-Origin: S2026090812345601a3"

In the response, cni is the number of entries currently stored and cbs is how much space they take up in bytes.

Discovering resources

When you do not know what is there, you can walk the tree. Adding fu=1 to the query returns a list of addresses for the resources that match.

find every container
curl "http://localhost:7579/Mobius?fu=1&ty=3" \
  -H "Accept: application/json" \
  -H "X-M2M-RI: r-dis-1" \
  -H "X-M2M-Origin: Sponde"
response
{"m2m:uril": ["Mobius/myAE/temperature", "Mobius/myAE/humidity"]}
FilterMeaningExample
tyFilters by resource typety=3 · ty=3&ty=4
rnMatches the resource name exactlyrn=temperature
lblMatches resources carrying that labellbl=outdoor
cra · crbCreated after, and created beforecra=20260101T000000
lvlHow many levels deep to walklvl=2
lim · ofstHow many to return, and how many to skiplim=100&ofst=100
When the result is truncated

A response carrying X-M2M-CTS: 1 means there are more results still to come. Pass the value in X-M2M-CTO as the ofst of your next request, and repeat until you have collected them all.

Subscriptions and notifications

To be told whenever a new value arrives in a container, attach a subscription to it and put the address you want notified in nu.

create a subscription
curl -X POST http://localhost:7579/Mobius/myAE/temperature \
  -H "Accept: application/json" \
  -H "X-M2M-RI: r-sub-1" \
  -H "X-M2M-Origin: S2026090812345601a3" \
  -H "Content-Type: application/json; ty=23" \
  -d '{"m2m:sub": {
        "rn": "watch",
        "nu": ["http://192.168.0.10:9000/noti"],
        "nct": 2
      }}'

From now on, every value added to that container triggers a POST to the address you gave. Since nu is an array you can notify several places at once, and the form of each address decides how the notification is delivered.

AddressHow it is delivered
http://…Sent as an HTTP POST. The receiver's response is read and logged as accepted, rejected or failed
mqtt://…Published over MQTT, which requires a broker to be configured
coap://…Sent as a CoAP request
A notification is sent once and then forgotten

Mobius does not retry a notification, and it does not delete a subscription because delivery failed. If the receiver happened to be down, those notifications are simply gone. For values you cannot afford to miss, have the application poll la periodically as a backstop.

Access control

Out of the box, Mobius does not filter requests by who sent them. To control who may work with a particular resource, create an accessControlPolicy and attach it to that resource.

create a policy
curl -X POST http://localhost:7579/Mobius \
  -H "Accept: application/json" \
  -H "X-M2M-RI: r-acp-1" \
  -H "X-M2M-Origin: Sponde" \
  -H "Content-Type: application/json; ty=1" \
  -d '{"m2m:acp": {
        "rn": "readonly",
        "pv":  {"acr": [{"acor": ["S2026090812345601a3"], "acop": 51}]},
        "pvs": {"acr": [{"acor": ["Sponde"], "acop": 63}]}
      }}'

acop expresses the operations you allow as a sum of bit values.

ValueOperationValueOperation
1CREATE8NOTIFY
2RETRIEVE16DELETE
4UPDATE32DISCOVERY

So 51 means create, retrieve, delete and discovery together (1+2+16+32), while 63 allows everything. pv governs the resources the policy is attached to, and pvs governs who may modify the policy itself.

Attach the policy you created to the container from earlier.

attach the policy
curl -X PUT http://localhost:7579/Mobius/myAE/temperature \
  -H "Accept: application/json" \
  -H "X-M2M-RI: r-acp-2" \
  -H "X-M2M-Origin: Sponde" \
  -H "Content-Type: application/json" \
  -d '{"m2m:cnt": {"acpi": ["/Mobius/readonly"]}}'

From this point on, only requesters the policy allows can work with that container. If you would like to see which requests a policy would block before you attach it, the access-policy screen in the admin console can simulate it for you.

Reading response codes

Every response carries both an HTTP status code and a oneM2M result code (X-M2M-RSC). These are the ones you will meet most often.

RSCHTTPMeaning
2000200The retrieve, update or delete succeeded
2001201The resource was created
2002200The resource was deleted
2004200The resource was updated
4000400The request was malformed; check the body and the headers
4004404No such resource exists
4103403An access policy refused the request
4105409A resource with that name already exists
5000500Something went wrong inside the server; check the log
5001501The current backend does not support that type

When a request fails, the reason arrives as a single line in the m2m:dbg field of the response body. That line is the first thing to read when you are working out what went wrong.


Where to go next

If you work with Postman, collecting these requests into a collection is worth the few minutes it takes. Put the server address and the CSE name into environment variables and you will not have to retype them again.

The source and the latest changes live in the GitHub repository. If you want to read about the standard itself, the specifications are published at oneM2M.