A lightweight NGSI-LD Context Broker written in C, fully implementing ETSI GS CIM 009 v1.9.1 and passing the official ETSI NGSI-LD conformance test suite with a 100% success rate.*
coraine is small, fast, and — most importantly — plugin-driven. Loaded at startup as shared libraries:
- storage backends — where the current state lives
- temporal history — the temporal evolution of entities (TRoE)
- extra API surfaces — ops/admin endpoints beyond NGSI-LD
- outbound transports — REST/HTTP always, other protocols next (the bridge seam)
The core broker speaks NGSI-LD; the plugins decide where data lives, what extra endpoints exist and how the broker talks to the world.
- Product version: 0.4 — see the release notes and the changelog
- Spec: ETSI GS CIM 009 v1.9.1 (NGSI-LD) — fully implemented
- Language / build: C, CMake (wrapped by a convenience
makefile) - License: Apache License 2.0 — Copyright 2026 Seamware
This project is part of FIWARE. For more information check the FIWARE Catalogue entry for Core Context Management.
Questions, bugs and feature requests all belong in
GitHub issues — that is where the
maintainers are. General FIWARE questions also reach people under the
fiware tag on Stack
Overflow.
* On that 100%: the conformance runs use a corrected fork of the ETSI test suite. The changes are test-side fixes — the suite has bugs of its own and parts of it simply don't run as published — never relaxations of what the broker must do. The fixes are filed upstream with ETSI, as merge requests for the suite's maintainers to review. Until they are merged, the 100% is against the corrected suite, not the published one: 1106 of 1106 test cases, on MongoDB + TimescaleDB and on
corDB(2026-10-07).
- Quick start ← start here
- Footprint and speed
- Plugin architecture
- Running
- API walkthrough
- Documentation
- Roadmap
- Quality assurance
- Training
- Contributing
- License
A published image, an in-memory store, no external services. One command, and the broker answers NGSI-LD on port 1026:
docker run --rm -p 1026:1026 \
quay.io/seamware/coraine:0.5.0 --database corDBThen, from another terminal — create an entity and read it back:
curl -X POST localhost:1026/ngsi-ld/v1/entities \
-H 'Content-Type: application/json' \
-d '{
"id": "urn:ngsi-ld:Sensor:1",
"type": "Sensor",
"temperature": { "type": "Property", "value": 21.5 }
}'
# 201 Created
curl localhost:1026/ngsi-ld/v1/entities/urn:ngsi-ld:Sensor:1
# {"id":"urn:ngsi-ld:Sensor:1","type":"Sensor",
# "temperature":{"type":"Property","value":21.5}}That is the whole broker — corDB keeps entities in RAM, so nothing else has to
be installed or configured. Swap in --database mongoc --dbHost <host> when the
data should outlive the process; see Running.
Building it yourself instead — the dependency stack, the system packages, the make targets — is Building from source.
Images are at quay.io/seamware/coraine,
tagged <version>-<date>-<commit> — one immutable tag per merge to main, never
expiring. There is deliberately no latest: a tag that moves under a running
deployment is a version nobody can name afterwards. Pick the newest from the tag
list, or pin the one you tested.
The images are multi-arch: linux/amd64 and linux/arm64 (from
0.4.0-2026-10-08-e8bd0edc on) — the same tag pulls the image for the machine it
runs on. <tag>-amd64 and <tag>-arm64 pin one architecture. The arm64 image is
built without the DDS bridge.
A broker is an executable, but running one takes more than the executable. What
counts is everything that has to be on the machine before it can answer: the
binary, the plugins it loads, the libraries that were not there before it
arrived, and any other server it needs running. That is what is counted here. A
small main on top of large libraries is not a small broker, and quoting the
main would be the wrong number.
Four builds, the two axes that change what a coraine process is made of — the
HTTP server (corHttp, built in, or the external libmicrohttpd) and the
current-state DB (corDB, entities in this process's RAM, or mongoc, entities
in a MongoDB server). Every rate is per core: the broker is pinned to one
physical core and the load generator kept off it.
| Build | Disk added | RAM idle | RAM · 100 k entities | Start-up | req/s per core | entities/s per core |
|---|---|---|---|---|---|---|
corHttp + corDB |
4.3 MiB | 17 MiB | 354 MiB | 12 ms | 9 508 | 190 160 |
corHttp + mongoc |
11.1 MiB | 24 MiB | 45 MiB + mongod | 31 ms | 4 634 | 92 680 |
libmicrohttpd + corDB |
11.6 MiB | 13 MiB | 358 MiB | 9 ms | 12 577 | 251 540 |
libmicrohttpd + mongoc |
18.4 MiB | 20 MiB | 68 MiB + mongod | 26 ms | 7 342 | 146 840 |
AMD Ryzen 9 8940HX laptop, 16 physical cores, Ubuntu 26.04, release build,
COR_FEATURE_ICU_COLLATION=OFF. GET /entities?type=Vehicle&limit=20, 20
entities per response. Disk added counts only what a bare ubuntu:26.04
does not already carry. mongod adds 1.02–1.15 GiB resident and cores of its
own; corDB adds nothing. Rates: libmicrohttpd + corDB 2026-10-10 (0.5.0, PGO release, corDB
on disk with --dbDir), libmicrohttpd + mongoc 2026-10-05 (PGO release, MongoDB 8.2 on the host
network), corHttp rows and the other columns 2026-09-16. Measured natively - in a container see below.
Six things worth taking from that table:
- A complete NGSI-LD broker, in-memory store included, is 4.3 MiB of files a
machine did not already have — and three libraries, two of which are GEOS.
1.00 MiB of it is coraine, and the cor libraries are whole-archived into
that binary, so it is not a
maincalling out to something else:corNgsild,corRest,corJsonld,corTree,corAllocand the rest are in the megabyte. --database corDB --dbDir <dir> --troe corDBneeds no other service at all — the store and its temporal history on disk, in the same process. On eight cores the history costs a write 4-43 % (PATCH: 120 379/s against 145 664 without it); in TimescaleDB it costs coraine + MongoDB 52-97 % (PATCH: 8 478/s against 17 486).- The page size is the claim. 12 577 requests/s per core at
limit=20is 251 540 entities/s per core; atlimit=1it is 82 672 of each; atlimit=100it is 275 000 entities/s. A requests/s figure without the response size beside it means nothing. - Batching is worth two to four times per entity. A
PATCHone at a time is 104 400 entities/s per core; twenty per request is 200 440. Creates: 54 707 against 117 460. Deletes: 126 714 against 476 280 (corDBon disk,--dbDir, 2026-10-10). - In a container it depends on the network setup. With
--net=hostthere is no measurable difference. With a port mapping (docker run -p, the usual way) expect 5-15% less, and up to 30% less for a single client waiting on each answer - the mapped port is one more hop per request. - The database is the bill. One broker core doing batch updates through
MongoDB needs four mongod cores behind it before MongoDB stops being the
limit — five cores of machine to do what
corDBdoes on one. On eight cores shared by everything a configuration needs,corDBon disk serves 145 664 PATCH/s andmongoc17 486 (2026-10-05).
📊 Performance and footprint has the rest: what is
inside the megabyte, the per-client latency curve, what MongoDB costs in cores
and RAM, why ICU is off, an open question about --connectionPoolSize, and how
every number was measured.
In principle the broker shrinks to exactly the NGSI-LD you deploy: no subscription
engine on a read-only edge node, no registrations, no datasetId, no geo, no
tenants, no Mongo. The switches are COR_FEATURE_* at build time, which means
building it yourself — a published image is compiled with everything on.
⚠ Be warned before planning around it: the flags are declared, the work behind
them is partly done. -DCOR_FEATURE_MONGOC=OFF and
-DCOR_FEATURE_ICU_COLLATION=OFF (the reference build above) genuinely work, and
the subscription and registration engines compile out; several of the
remaining flags are still declarations with no #ifdef behind them, and turning
one of those off changes nothing or fails the link.
Building from source has the
full list and the honest state of each.
This is the heart of coraine. The broker binary holds the NGSI-LD protocol logic, the REST layer, the JSON-LD engine and the subscription matcher — and no storage code, no temporal code. Those, plus any non-NGSI-LD admin/ops endpoints, are shared libraries loaded at startup. You pick them on the command line; you can write your own without touching the core.
| Category | Selected with | Active at a time | Bundled |
|---|---|---|---|
| Current-state DB | --database / -db |
one | mongoc (default), corDB (in RAM, or on disk with --dbDir), ramDB (corDB in RAM only - the fastest; a restart starts empty) |
| History DB (TRoE) | --troe |
one | none (default), corDB, timescale |
| API services | --apiPlugins / -api |
any number | admin |
| Bridge (a foreign transport) | --bridges |
any number | dds, mqtt, modbus (their own repos); HTTP/HTTPS and cor:// are built in, not bridges |
| Transport (the API over another protocol) | --transports |
any number | ws - WebSocket: the API and its notifications over one connection (doc) |
| Auth (who sent a request, and may it) | --auth |
one, or none (every request let in) | fdsc - the FIWARE Data Space Connector's access tokens and ODRL policies, decided inside the broker (doc) |
Why that matters, beyond tidiness. The broker never talks to a database. It talks
to a driver interface — DbDriver.h for current state, TroeDriver.h for history —
and those two headers are the entire contract, function by function, documented
semantics included. So bringing coraine to a store it has never seen is writing one
shared library against a documented header. It is not forking a broker, not patching
a query builder, and not learning NGSI-LD: the protocol, the JSON-LD engine and the
subscription matcher stay in the core, and the driver is only ever asked storage
questions.
Two properties make that practical rather than aspirational. A NULL function
pointer means "unsupported", answered as 501 Not Implemented, so a new backend can
ship the day it does entity CRUD and grow snapshots, registrations or context
persistence later — corDB gained its persistence (--dbDir) entirely inside the
plugin, with no change to the broker. And the choice is made at startup, not at build time: the same binary runs
on MongoDB in production, in RAM for a test, and on your own store in the field,
because --database takes a path as readily as a name.
The full story — where plugins are resolved from, how the loader works, the driver
interfaces, plugin-contributed CLI args, and how to write your own — is in
doc/plugin-architecture.md.
Default listen port is 1026. Plugins default to mongoc (DB) + none (TRoE),
no API plugins.
# In-memory, pretty JSON, admin API on — zero external services:
coraine --database corDB --troe none --apiPlugins admin -pp 2
# Default (Mongo) on a custom port:
coraine --port 1027 --database mongoc
# Full plugin help (includes the selected plugins' own args):
coraine --apiPlugins admin --database mongoc --usageSelected common options (--usage for the full list):
| Option | Default | Meaning |
|---|---|---|
--port / -p |
1026 | TCP listen port |
--database / -db |
mongoc |
DB plugin (short name or path) |
--troe / -troe |
none |
TRoE plugin (none disables history) |
--apiPlugins / -api |
— | comma-separated API plugins |
--foreground / -fg |
— | accepted, no effect — the broker always runs in the foreground |
--pretty-print / -pp |
0 | JSON indent (0 = compact) |
--localOnly / -local |
off | disable distributed operations |
--defaultUserContext / -duc |
— | default @context URL |
--corsOrigin |
— | enable CORS (__ALL for any origin) |
--maxRequestSize / -mrs |
2 | max body MiB (§ 6.3.2; 0 = no cap) |
coraine speaks NGSI-LD under /ngsi-ld/v1. Start a broker that needs nothing else,
create an entity, and read it back:
coraine --database corDB --troe none --apiPlugins admin -pp 2 &
curl -X POST http://localhost:1026/ngsi-ld/v1/entities \
-H 'Content-Type: application/json' \
-d '{ "id": "urn:ngsi-ld:Vehicle:A100", "type": "Vehicle",
"brand": { "type": "Property", "value": "Mercedes" },
"speed": { "type": "Property", "value": 80 } }'
curl 'http://localhost:1026/ngsi-ld/v1/entities?type=Vehicle&q=speed>50'Queries, the three representations (normalized, concise, keyValues), updates,
subscriptions and notifications are walked through in
doc/api-walkthrough.md.
The API is not ours to define. coraine implements ETSI GS CIM 009 v1.9.1, and the normative definition is the ETSI deliverable itself — restating it here would only create a second copy to keep in sync, and the one that drifted would be ours.
The machine-readable form is published by ETSI ISG CIM, and it currently lags the specification, which is worth knowing before generating a client from it:
- OpenAPI 3.0.3, bundled and self-contained —
full_api.jsonon ETSI Forge, publicly readable with no account. It declares its version aslatestbut was last updated in April 2022, so it describes roughly v1.7.1–1.8.1 rather than the v1.9.1 implemented here. An ETSI Specialist Task Force is producing a current one. - The same definition split by resource —
spec/updated, withngsi-ld-spec-open-api.jsonas the root document. - The companion deliverable — ETSI GS CIM 047, "Context Information Management (CIM); OpenAPI Specification for NGSI-LD API".
So for anything added since 1.8.1 — and that includes much of what this broker implements — the specification document is the authority, not the OpenAPI file.
Where coraine deliberately differs from the specification, or where the
specification is ambiguous and we had to choose, it is written down rather than
left to be discovered: see doc/spec-coverage-gaps.md.
Rendered and searchable at coraine.readthedocs.io,
rebuilt from main on every merge. The same pages live under doc/ in this
repository, which is where to read them offline or alongside a checkout:
| Document | What it covers |
|---|---|
| Getting started | |
| Installation & Administration | dependencies, build, install, every option, the admin API, tenants |
| API walkthrough | the API by example, from create to subscribe |
| Building from source | the dependency stack, system packages, make targets; the source layout and compiling features out in its detail page |
| Transports - how a client talks to the broker, besides HTTP | |
| ↳ WebSocket | subscriptions and their notifications over one connection - for a web page (--transports ws) |
| ↳ The cor format and cor:// | the binary API - between brokers, and for clients that want it |
| Bridges - how the broker reaches devices and other systems | |
| ↳ Bridges and Channels | the model: a Channel ties a foreign endpoint to one attribute |
| ↳ DDS and ROS 2 | what coraine does with DDS, and where it differs from Orion-LD |
| ↳ MQTT | MQTT topics as Channels, and notifications to MQTT |
| ↳ Modbus | Modbus TCP registers as Channels |
| ↳ Speaking to devices directly | reaching devices without an IoT Agent tier, and what that needs |
| ↳ FIWARE IoT Agents | what they do, how they integrate, and where the boundary sits |
| Running it | |
| Plugin architecture | the plugin categories, the loader, the driver interfaces, writing your own |
| Authorization | security inside the broker - the auth plugin contract, and fdsc: the Data Space Connector's tokens and policies, as the connector decides them (--auth fdsc) |
| High Availability | several instances behind a load balancer: requirements, MongoDB privileges, troubleshooting |
| Migrating a database to coraine | another broker's data - entities, subscriptions, registrations, history - with its ids and timestamps; Orion-LD today |
| Performance and footprint | what it costs on disk and in RAM, per-core throughput, and how each number was measured |
| Coroutines | how a request that waits runs without blocking the others |
| Quality | |
| Testing | running the suite, and measuring coverage |
| Test coverage | what the suite covers, per DB, and what is left |
| Functest coverage of the spec | every spec statement, and whether a test asserts it |
| Where it's going | |
| Roadmap | where coraine is going - short, medium and long term |
| Ideas | every idea, planned or only possible, with what there is to say about it |
The full API is the specification itself: ETSI GS CIM 009 / TS 104 175, which
coraine implements in full. Every command-line option is listed by
coraine --usage, including the options contributed by the plugins you selected.
Next, in roughly this order - each in more detail in the roadmap, and every idea, planned or not, in Ideas:
- corDB persistence, cheaper writes - a PATCH logs the attributes it touched, not the entity.
- Automatic TRoE in corDB - temporal history from the same log, by a boolean.
- More bridges - Kafka, WebSockets, OPC UA on the Bridge/Channel seam.
- Service Execution - "do this" in the API, not a write to an attribute.
- Packages -
apt-get install coraine. - Finish conditional compilation - every feature flag reaches the code it names.
- Subordinate subscriptions on registration change - § 10.5.2.4 for
PATCHtoo.
- Conformance: 100% of the official ETSI NGSI-LD conformance test suite (see the note at the top of this file).
- Functional tests: 736 tests against MongoDB, 681 against
corDB(2026-10-02), run throughcorTest. A change in behaviour is not finished until a test pins it. - Coverage: measured per DB, run as described in
doc/testing.mdand published indoc/coverage.md, along with an estimate of how much of what is left can only be reached by making the environment fail. - Memory safety: the harness can run the whole suite with the broker under
valgrind (
corTest -vt), failing on definite or indirect leaks and on valgrind errors.
The FIWARE GE ratings badges (documentation completeness, responsiveness, FIWARE testing) are published from the Catalogue once the entry exists; they will be added here at that point.
| Documentation | FIWARE Academy | NGSI-LD Tutorials | FIWARE Catalogue |
|---|
The NGSI-LD tutorials apply to coraine unchanged — it implements the same API. The step-by-step guide in this repository is the shortest path from a running broker to a working subscription.
Contributions are welcome. CONTRIBUTING.md describes the terms —
including the Individual Contributor License Agreement that every pull request must
carry — how to build and test, and what a good bug report contains. Participation is
governed by the Code of Conduct.
C style for the whole stack is one document:
STYLE_GUIDE.md in
the corLibs umbrella.
The roadmap and the backlog are one page, doc/roadmap.md: what is next, what is not
built yet, and what is deferred by design.
coraine is licensed under the Apache License 2.0 — Copyright 2026 Seamware.
Every source file carries an SPDX-License-Identifier. The people and projects it is
built on are named in CREDITS.md.