Ideas
Every idea for coraine in one place - what is planned and what is only possible. None of it is a commitment; the roadmap says which of these are next. The day-to-day backlog is the second half of it.
Each idea: one line, then what there is to say about it, and where more lives.
Storage
corDB persistence - cheaper writes
corDB persists with --dbDir (a log and snapshots - corDB's design);
what it costs is in Performance, "corDB on disk": nothing on reads, 6-24 % on
writes that change the store, more while the store grows fast. Two ways to bring it down: a PATCH or
merge that logs only the attributes it touched (ATTRS_PUT, already in the record format) instead
of the whole entity, and a cheaper encode of a record (the per-request cost is the encode).
corDB history: retention and a selector
--troe corDB keeps the temporal history in the corDB store itself - captured where corDB writes,
on disk with the store, the whole temporal API on it, no second plugin and no PostgreSQL. In-process
history costs nothing measured on eight shared cores; the same history in PostgreSQL costs corDB 91 % of
its write rate. Two things it does not have yet: retention - history only grows; kept for a time or
a size, whole old chunks dropped - and a selector: what history records, configurable and
subscription-shaped (entities, attributes, include or exclude), where today every write to every
attribute is recorded. Not built either, and not needed for those two: the persistence log as the
history itself (history is segments of its own today).
The Entity as it was at time T
A current-state Entity at an instant in the past - not the temporal representation (every value with
its timestamps), the Entity as a retrieve would have answered at T. Discussed at ETSI, not in the
specification yet; implemented when it is. With corDB's log it is nearly free by system time (the
newest snapshot at or before T plus the log up to T); by observedAt it needs the temporal index per
attribute - and a late sample can change the answer later. The roadmap, § 15.
corsh - the corDB shell
A maintenance tool (a general broker shell is for later), part of corDB's own repository once corDB
leaves this one: stats, backup (consistent while the broker runs - a snapshot,
then hard links), compact, purge by type or q, drop a tenant, index add/drop/list, and on a
stopped broker's files verify, dump, export/import. Live data goes through the broker (cor://
to the admin plugin); files only when no broker runs. Design: corDB's design § 10.
corDB indexes
corDB indexes the entity id (a hash) and keeps entities in creation order - the default query order,
as with mongoc. A query by type, a q and a geo-query walk the tenant. A type index in creation
order (with persistence), attribute-value indexes for q, an R-tree for georel - each a
declaration kept with the tenant's data, rebuilt at load, measured before it stays.
corDB standalone - and as haaux
corDB as a process of its own, beside the brokers, for high availability: already connected to every broker of the group, it is what haaux needs (below). One codebase, built as the in-broker plugin (the default) or the server, with a remote-corDB plugin as the broker's client of it; backups of current state and history. (corDB already is a repository of its own, SEAMWARE/corDB.)
A smaller CorNode
A prefix layout for the tree node: smaller nodes, less corDB memory, fewer cache misses walking a
tree. To be measured with perf stat cache-misses on the GET scenarios.
Protocols and transports
cor:// - what the format does not do yet
Packed numeric arrays and timestamps as integers (cor-protocol-details.md § 4.9, § 4.10); the wire
size of broker-to-broker traffic, with its expanded names, measured; Unix-domain sockets for brokers
on one host.
Transports as plugins
HTTP and cor:// as protocol plugins, loaded like the database, TRoE and bridge plugins - and with that an NGSIv2 API plugin, and NGSI-LD itself perhaps a plugin. No hurry.
Coroutines - open questions
The cap (1024 per loop) and the stack size (256 KiB) as options; libmicrohttpd's own
MHD_suspend/resume_connection for coroutines, or the built-in server as the default; a yielding
mongoc stream (mongoc_client_set_stream_initiator) so mongoc requests can be coroutines too.
Coroutines § 6.
Bridges and devices
More bridges
Kafka, WebSockets, OPC UA on the Bridge/Channel seam that carries DDS, MQTT and Modbus. The protocol names the endpoint and the endpoint decides the transport; HTTP stays inline rather than a plugin, because it is also the NGSI-LD API.
- Kafka - one standard binding across the brokers that support it.
- WebSockets - notifications to consumers that cannot be HTTP servers: behind NAT, a firewall, a browser.
- OPC UA - variables as attributes, monitored items as subscriptions, methods as Service Execution.
Bridges and Channels over the API
Today they come from the configuration file and are read over GET; create, update and delete over
the API, persisted - the design is in bridge-channels-details.md (not published while the concept
is in front of ETSI).
Aligning Bridges and Channels with ETSI
Expected in 2027, on whatever TC DATA standardises after the Athens face-to-face of October 2026. Adaptation work rather than new capability: the objects, the endpoint-scheme convention and the codec seam are likely to move, the transports carried over them are not. coraine's Bridge and Channel are not standard NGSI-LD.
Service Execution
Actuation as a first-class citizen of the API, beyond the suggested workflows of TS 104 175 Annex G. DDS services and actions reach the broker today through Channels and a provisional convention (a write to an attribute, goals as its instances); the broker still needs a way to say "do this" that is not a write to an attribute.
The IoT Agents as cor-agent plugins
Parity with the FIWARE IoT Agents - UltraLight, JSON, LWM2M, LoRaWAN, Sigfox, OPC UA, ISOXML - as plugins on the bridge contract instead of separate processes. The same source compiles to a reduced cor-agent configuration, so agent-then-broker becomes a deployment choice: a small edge build beside a central broker, or one binary doing both (FIWARE@Home on a Raspberry Pi). More of them as deployments ask, not to complete a matrix. Speaking to devices directly, FIWARE IoT Agents.
Philips Hue - the first FIWARE@Home plugin
FIWARE@Home - coraine on a Raspberry Pi as the home's context broker - starts with the devices people already have. Parity with the FIWARE IoT Agents is the floor; the home needs more plugins than those, and Philips Hue comes first.
A plugin on the bridge contract, speaking the Hue Bridge's local API (version 2, HTTPS on the home network, no cloud):
- Pairing - the bridge is found on the network (mDNS), and the plugin gets its application key when someone presses the bridge's link button.
- Entities - lights, rooms and zones, scenes, and the sensors (motion, temperature, light level,
buttons). A light's
on, brightness and colour are Properties; the room it is in is a Relationship - so in c³ Connect the home is a graph: rooms, their lights, their sensors. - Live - the bridge's event stream pushes every change; the plugin writes it to the broker as it happens. No polling.
- Switching - a write to a light's entity (on, brightness, colour, a scene) becomes the bridge call that does it.
A CKAN bridge and a CSV bridge
CKAN is the data portal behind many city and government open-data sites. FIWARE reads nothing from
it: its CKAN components (the Cygnus and Draco CKAN sinks, ckanext-harvest-ngsild) all go the other
way and store a broker's data in CKAN. Reading from CKAN is new.
Both are bridges on the Bridge/Channel seam, like DDS, MQTT and Modbus - the bridge knows nothing
about entities; it speaks (endpoint, json, time):
- CKAN - the endpoint is a dataset's resource; each row read through CKAN's API
(
datastore_search, rows as JSON) is one message, timed by a column of the row or by when it was read. - CSV - the same for a CSV file: a local one, one at a URL, or a CKAN dataset whose data is an uploaded CSV file rather than a DataStore table.
CKAN pushes nothing - no event stream, no change feed for rows - so how a Channel follows its resource is a mode, set per Channel:
- once - read the resource at start-up, then stop. Most open data, and the c³ demo.
- poll - watch the dataset's activity stream (
package_activity_list, "resource updated"); on a change, read only the new rows (_idabove the last one seen - DataStore rows get increasing integer ids) when the table only grows, or the whole resource again when it was replaced. A row changed in place keeps its_id; it is seen only through a timestamp column. The broker's upsert writes only what changed. - push - where the portal has a webhooks extension installed (
ckanext-webhooks): a dataset change is pushed, then read as in poll.
Most open data is not an append stream: a dataset is replaced whole, often nightly - a fresh CSV or a rewritten table. Poll treats that as a re-read, not a tail.
The Channel does the NGSI-LD side, as for every bridge: which column is the entity id, a column
holding a place becomes the location GeoProperty, a column naming another row becomes a
Relationship - made in c³ Configure, beside DDS and OPC UA. The other direction comes with the seam:
a Channel the other way stores a broker's entities in CKAN - what the Cygnus and Draco sinks do today,
without a separate process.
As a demo in c³: a dropdown of datasets from a portal, a preview of the first rows, the mapping, then the ingest - and the entities are on the map and in the graph in Connect. Real data to play with.
A smaller DDS stack for the DDS bridge
The DDS bridge runs on a full DDS stack, the largest part of a coraine installation (about five times the broker) - so the arm64 image and the Debian packages ship without the bridge, and a device agent on a small board could not carry it. What the bridge needs is a subset: discovery (SPDP/SEDP), reliable and best-effort readers and writers, fragmentation and CDR serialisation - no content filters, no DDS Security. Two ways, the cheaper first: a DDS implementation written in C, built behind the same plugin; or that subset of the wire protocol (RTPS) on its own. Interoperability with DDS and ROS 2 nodes is a property of the wire protocol and stays as it is; the work is in testing it against each of them.
Modbus - beyond v1
Contiguous registers read together, RTU, read on demand through a registration, a way back for a write's outcome. The Modbus bridge.
A registration that names a bridge
A value the broker does not hold, fetched from a device when read (a battery level that polling would drain): a registration whose endpoint is a bridge scheme, served by the bridge.
The API and the specification
Subordinate subscriptions on registration change
§ 10.5.2.4 handles creation and deletion of a registration, not PATCH.
Advertising the user @context and the core version
The ;v1.9 parameter on the core context's Link header, and § 13.4's ?core=.
Problem Details and error reporting - proposals to ETSI
Machine-readable errors and per-item outcomes that NGSI-LD does not define yet - taken to the ETSI TC DATA face-to-face in Athens; not implemented. The drafts are not public.
Our own string collation, replacing ICU
§ 7.6.2.1 makes ICU "root" collation the default order for orderBy on strings, and libicu costs
three shared libraries and 39.2 MiB - libicudata alone is 31.6 MiB, nine times the broker, for a
table. An MVP of root collation in corNgsild: UTF-8 to code points, a primary/secondary/tertiary
weight table for the Latin ranges real deployments use, the category order (punctuation < digits <
letters) the current ASCII approximation gets wrong; locale tailorings added on demand, one at a
time. ICU stays behind COR_FEATURE_ICU_COLLATION=ON. orderby_collation_locale.test is the
discriminator the MVP has to turn green without ICU.
Array reduction in corJsonld
One JSON-LD normalisation applied once at the input boundary rather than at each call site.
One MIME-type handling
The media types parsed, negotiated and rendered in one place instead of several.
Security
Authorisation inside the broker
NGSI-LD defines no authentication or authorisation. An optional layer inside coraine decides on the
request it has already parsed: per entity in a batch, for entity types after @context expansion,
by tenant - "may only read type X" becomes a condition on the query, not a filter on the response.
ODRL policies, as in the FIWARE Data Space Connector; the identity is the verifier's token for a
verifiable credential. Authorization in the broker.
An NGSI-LD plugin for APISIX
A policy enforcement point that understands NGSI-LD, as a plugin for APISIX
- the gateway that already is the enforcement point of every FIWARE Data Space Connector deployment.
It decides on the NGSI-LD request itself: the operation, the entity type (from the request, the entity
id, or the query), the attributes, the tenant - with the client's @context, as the broker would read
it. It sits in front of any NGSI-LD broker - Scorpio, Stellio, Orion-LD, coraine - and belongs in
APISIX's plugin hub; no such plugin exists today. The same decision rules as the authorisation inside
the broker, for a deployment that keeps them at the gateway.
Operations and availability
Migrating from Orion-LD
A deployment of Orion-LD moves to coraine with its data: entities, subscriptions, registrations.
First a converter - an executable that reads an Orion-LD database and writes coraine's (MongoDB for
mongoc, the files of a persistent corDB). Then, the general way: ETSI TC DATA has agreed that a
broker should be able to save its data in a neutral format (possibly a set of CSV files; nothing
about it is decided yet). With that format, any broker's export is coraine's import, and the
converter becomes "export, import". To be taken further at the Athens face-to-face, October 2026.
Scaling out: sharding by tenant and by entity type
A standalone corDB, reached over the network by every broker, costs most of what makes corDB fast. Scaling out instead keeps corDB linked into each broker and splits the data between brokers:
- By tenant. Every request names its tenant (
NGSILD-Tenant) and tenants share nothing - no query, subscription or join crosses them. APISIX routes on the header to the broker that owns the tenant; corDB already keeps a store per tenant. No router of our own. - By entity type. The type is often not in the request (by id alone, queries without
type, batches mixing types, subscriptions over several types), so routing needs NGSI-LD itself: routing coraines - a reduced build without a store, behind APISIX, as many as the load needs - with an exclusive registration per type pointing at the shard that owns it. Forwarding, merging, paging across sources (EntityMaps), splitting batches and distributed subscriptions are the distributed operations coraine already has; router to shard over cor://. What is new: an id -> shard directory for requests by id alone (or the type enforced in the entity id), and moving a type between shards. - One type too big for one broker - split it by id range or hash, several registrations for the
same type with an
idPatterneach.
Sharding is scale; availability is separate - each shard has its replica (haaux). The routing build's feature set is decided together with the rest of conditional compilation. First measurement: the router-to-shard hop over cor://.
haaux
High-availability cache synchronisation without a shared database: brokers register with each other at startup, keep the connection, and sync subscriptions, registrations and contexts interrupt-driven in single-digit milliseconds. No polling. High availability.
HA peer push over a shared database
Where the brokers share a database, the one that changes a subscription, registration or @context pushes the change to the others itself - over cor://, in parallel with nothing in between - and answers the client once they have acknowledged it. The change stream (~50 ms: majority commit, then a re-read) stays as the safety net. The cluster's members live in a collection of the shared database with a lease each, so a restarted broker finds and reconnects to the others. It narrows the window in which an entity update reaching another broker misses a notification - closes it for an update that comes causally after the subscription, and shrinks it from ~50 ms to a LAN round trip between independent clients, whose order is undefined anyway. Roadmap, § 5.1.
A memory budget and admission control
A broker that knows its memory budget (a container's limit) and refuses writes before the OOM killer
ends it - --memoryLimit is the start of it.
Build, distribution and footprint
c³ - the coraine Control Center
One application for everything around a broker, in three parts:
- Compile - build a broker for a deployment: which features are compiled in, the profile
training (PGO) and
make tuneon a workload, the target hardware; the result as a binary, a container image or a Debian package. It drivescoraine-buildfrom thecoraine-devpackage, so it needs no toolchain of its own. - Configure - the mapping tools: DDS topics, services and actions to NGSI-LD entities and attributes (the mapping file the DDS bridge reads), and the same for OPC UA variables, monitored items and methods.
- Connect - a live connection to a running broker: its entities as boxes and their Relationships as arrows between them - zoom, move the view, drag entities into place - the entities with a GeoProperty on a map, and their values as they change.
A web application, not a native one: the build often runs on a server without a screen, reached
from a laptop, a Mac or a tablet; the best graph libraries are web libraries; and the broker already
speaks HTTP and WebSocket. A small backend in C on the build machine serves the pages and runs what
Compile and Configure need (coraine-build, the mapping files). Connect can also come with the
broker itself, as an API plugin serving the page: connecting to a broker is then opening its URL.
Starting it - one command, c3: it starts the backend on a local port and opens the
application. With Chrome or Chromium installed it opens as an application window of its own
(--app=<url>, no tabs or address bar), full-screen (--start-fullscreen); otherwise a tab in the
default browser (xdg-open, open on macOS). A page cannot make itself full-screen without a click
(the browser's rule), so full-screen is the launcher's job. On a machine without a screen, c3
prints the URL to open from a laptop (through ssh -L when the port is not reachable). The first
thing shown is a splash screen while the backend gets ready - each step as it really completes
(the coraine-dev version, coraine-build --list-features, the brokers and bridges found), not a
timer - then Compile.
Connect over the WebSocket transport (WebSocket): the initial state by queries,
then a subscription on the same socket and the notifications it pushes - no polling. A browser cannot
set HTTP headers on a WebSocket, which the transport already allows for: every message carries its
headers (NGSILD-Tenant, Link) in its own metadata. Two things to settle with the broker's
authorisation, not after it: how a browser presents a token on the socket (in a message's metadata,
or a cookie), and an Origin check for a page not served by the broker itself (as --corsOrigin
does for HTTP).
Packages
A Debian repository and apt-get install coraine, with a coraine-dev that pulls the whole
dependency stack in one command. Today building from source is the only way to a machine that does
not run the container image.
Finish conditional compilation
Per-feature #ifdefs so a deployment compiles only the NGSI-LD it uses. REGISTRATIONS and
SUBSCRIPTIONS compile out and the HTTP server is a build choice, but most declared feature flags do
not reach the code they name - Building from source, in detail says which.
Release libraries, all of them
make release builds corRest, corJsonld and corNgsild as release; the libraries under them come in
as the last build, usually debug. Each needs an archive per flavour first (doc/performance.md).
Embedded deployment
The broker adds 4.3 MiB to a machine, holds 17 MiB resident and answers 12 ms after exec; constrained
hardware is a build-configuration question, not a redesign - conditional compilation is what makes
that true.
An ARM image, and a more diverse nightly
An ARM64 image; nightly builds with unsigned char, the sanitizers (ASan, UBSan), clang and musl -
each finds bugs the others hide.
Quality
Continuous ETSI conformance
The official test suite kept at 100 % as the specification evolves, test-side corrections fed upstream.
Broader performance regression coverage
More scenarios measured nightly and recorded, so a regression is noticed by CI, not by a user.
--connectionPoolSize on one core
Why corHttp is slower than libmicrohttpd on one core, and whether the pool size is the cause -
Performance, "An open question".
Live public transport: GTFS-realtime
A demo with a real graph and real movement, on standard data any city publishes.
- Static GTFS - a zip of CSV files:
routes.txt,stops.txt,trips.txt,stop_times.txt,shapes.txt,calendar.txt, linked by id columns. Routes, stops and trips become entities with their relationships; a route's path is the rows ofshapes.txtcollected in order into oneLineString. Loaded once, when the broker starts. - GTFS-realtime - Protocol Buffers over HTTP, polled every few seconds; three feeds:
- VehiclePosition - the vehicle (id, label), its trip (trip, route, direction, start time), its
position (latitude, longitude, bearing, speed), its progress (the stop,
INCOMING_AT/STOPPED_AT/IN_TRANSIT_TO), its timestamp, often its occupancy; - TripUpdate - predicted arrivals and departures, delays;
- Alert - cause, effect, the routes and stops affected, the text for passengers.
- VehiclePosition - the vehicle (id, label), its trip (trip, route, direction, start time), its
position (latitude, longitude, bearing, speed), its progress (the stop,
- The bridge - each VehiclePosition updates one
Busentity:location,bearing,speed,status, Relationships to itsTrip,Routeand nextStop; the measurement time isobservedAt. Needs a protobuf decoder (protobuf-c). The MBTA's own JSON API (V3) serves the same data as JSON, with filters and a stream of changes - the simpler start for Boston; GTFS-realtime is what every city has. - c³ - subscribes to
Bus(watchedAttributes=location) over its WebSocket; each notification moves a marker, turned to the bearing and animated between positions, over the route'sLineString. The history holds every position: a trip can be replayed. - Invented data - what no feed has (drivers, shifts, depots) generated, with
Bus → drivenBy → Driver. - Models - Smart Data Models' GTFS-based models (UrbanMobility) for the entity types.