Hi.
Welcome to Healthcore❗️
But wait: what the hell is Healthcore❓
Healthcore is an open software and hardware architecture for healthcare devices. With Healthcore, devices from any manufacturer can communicate with each other and any interface through a variety of protocols and APIs. In this way, Healthcore centrally captures a person's condition in many different ways, reacts automatically to changes in their environment, and optionally informs caregivers, nurses or family members.
Healthcore standardizes the data from various devices, processes scenarios, and triggers actions. A simple API allows interfaces such as apps to visualize the device data.
Healthcore can run on any hardware — a Raspberry Pi, a PC, or any other device with a Linux system or Windows. The choice is yours.
Just imagine something like Home Assistant or OpenHAB, but specialized for healthcare. That’s exactly what this is.
Tip
Healthcore can also be used to manage smart home devices such as motion detectors or window sensors. After all, that’s the only way to get the full picture.
Healthcore is used by bulp.io, which produces some really cool hardware around the Healthcore.
So let’s democratize and de-monopolize the healthcare sector. Make healthcare devices and infrastructure affordable for everyone!
🤘HEALTHCORE!!!🤘
👉 Read more about ...
- 🚀 Quick start
- 🏗️ Architecture
- 📁 Folder structure
- 🔧 Installation (hardware)
- 💻 Installation (software)
- 🔐 Security
- 🔌 API communication
- 📈 Healthcheck
- 🧩 Own converters
- 💓 External data via APIs
- 🤖 Using an LLM for reports
- 🔎 Testing
🚀 Quick start
This quick start walks you through a realistic first setup with:
- a ZigBee router (coordinator USB stick)
- a ZigBee motion sensor
- one-week data collection
- one generated weekly report
1. Prepare hardware
You need:
- Host: Raspberry Pi / Linux PC / Windows PC
- ZigBee router (coordinator), e.g. ConBee II or Sonoff Zigbee 3.0 USB dongle
- ZigBee motion sensor (any supported model, or your own converter)
Connect the ZigBee router to your host via USB.
2. Download repository
git clone https://github.com/janrakete/healthcore-bulp.io.git
cd healthcore-bulp.io
npm install3. Define ZigBee router in .env.local
Create .env.local in the project root and set at least:
CONF_zigBeeAdapterPort=/dev/ttyUSB0 CONF_zigBeeAdapterName=zstack
Typical values:
- Linux:
/dev/ttyUSB0or/dev/serial/by-id/... - Windows:
COM3,COM4, ...
Adapter type depends on your hardware/chipset (examples: zstack, ezsp, deconz).
4. Create converter for your ZigBee motion sensor
- Use an LLM (for example GitHub Copilot, Claude Code, or Codex) to generate the converter.
- Prompt example (adapt sensor model and bridge path):
Create a ZigBee converter for the Aqara RTCGQ11LM motion sensor. Use the examples in /bridge-zigbee as a guide. Extend ConverterStandard, set static productName exactly to the model name, and include occupancy, battery, and tamper properties where supported. - Save the generated file in
bridge-zigbee/converters/, for exampleConverter_MyMotionSensor.js. - Verify
static productNamematches the model name reported by ZigBee exactly.
If your sensor is already supported, you can skip this step.
5. Download an LLM and configure it
- Download a GGUF instruct model (filename should contain
Instruct). - Put the model file in
server/libs/ReportingEngine-models/. - Add model name to
.env.local:
CONF_reportingEngineModel=YourModelName-Instruct.gguf
6. Start Healthcore services
Run each service in its own terminal:
node broker/app.js node server/app.js node bridge-zigbee/app.js
7. Add the ZigBee motion sensor via routes
- Start ZigBee scan:
curl -X POST http://localhost:9998/devices/zigbee/scan \ -H "Content-Type: application/json" \ -d '{"duration":30}'
- Get scan result and copy the discovered sensor UUID:
curl "http://localhost:9998/devices/zigbee/scan/info?callID=CALL_ID"8. Collect data from the sensor for one week
- Keep broker, server, and ZigBee bridge running continuously for 7 days
- Keep the motion sensor paired and powered
- Trigger sensor events naturally (room usage) so occupancy data is written
Optional spot check:
curl "http://localhost:9998/data/mqtt_devices_values?deviceID=SENSOR_UUID&orderBy=dateTimeAsNumeric,DESC&limit=20"9. Generate report for last week
After 7 days, call:
curl -X POST http://localhost:9998/reports/generate \ -H "Content-Type: application/json" \ -d '{ "startDateTime":"2026-07-15T00:00:00Z", "endDateTime":"2026-07-22T00:00:00Z", "language":"en" }'
Then fetch stored reports:
curl http://localhost:9998/reports
🏗️ Architecture
Let’s take a look at the architecture:

In the middle — that’s the Healthcore. The Healthcore consists of several Node.js servers with different tasks. The Node.js servers communicate with each other via MQTT. The most important thing is that there is a separate bridge for each protocol, which standardizes the incoming and outgoing data of the devices. Healthcore supports the following protocols:
- Bluetooth
- ZigBee
- LoRa P2P
- HTTP
- External APIs (Google Health, Garmin Health, …)
- Thread (planned)
And now the best part: you can add your own devices to the Healthcore! Each bridge includes a list of classes for devices. So you can handle the data transformation with simple JavaScript in a class for your device (= very cool).
On the left, you can see how various interfaces communicate bi-directionally with the Healthcore via a standardized API and visualize the data, for example. Just bring your own interface.
Healthcore keeps three independent concepts separate:
devices.roomIDis the physical installation or usage room of a device.devices.individualIDis the person who owns or uses a personal device.individuals.roomIDis the person's primary room or residence.
📁 Folder structure
├── broker/ # MQTT broker
├── server/ # Server
│ ├── routes/ # Routes for communication Interface via SSE ↔ Server ↔ Interface via API
│ ├── middleware/ # Middleware features
│ └── libs/ # Additionally libraries
├── bridge-bluetooth/ # Bluetooth ↔ MQTT bridge
│ └── converters/ # Common and own converters
├── bridge-zigbee/ # ZigBee ↔ MQTT bridge
│ └── converters/ # Common and own converters
├── bridge-lora/ # LoRa ↔ MQTT bridge
│ └── converters/ # Common and own converters
├── bridge-http/ # HTTP ↔ MQTT bridge
│ └── converters/ # Common and own converters
├── bridge-integrations/ # External API providers ↔ MQTT bridge (Google Health, Garmin, …)
│ └── converters/ # Provider converters
├── tests/ # Jest tests, manual tests and example device firmware
├── healthcheck/ # Healthcheck (see below)
Note
All variables are defined in .env in the root directory. They can be overridden using the .env.local file.
🔧 Installation (hardware)
- Host platform
- Raspberry Pi 4 or (or better) or any Linux/Windows PC with network access
- Adapters
- Bluetooth: Built-in BLE or USB dongle
- ZigBee: USB coordinator (e.g. CC2531, ConBee II, Sonoff Zigbee 3.0 USB stick - full list here)
- LoRa: USB or serial LoRa adapter (e.g. Dragino LA66 LoRaWAN USB Adapter)
- Connections
- Plug adapters into host; note device paths (e.g.
/dev/ttyUSB0orCOMx) and set in.env.local(CONF_loRaAdapter*and/orCONF_zigBeeAdapter*)
- Plug adapters into host; note device paths (e.g.
💻 Installation (software)
Prerequisites
- Node.js (v24 or higher) and npm
Project setup
- Clone/download the repository and
cdinto its root. - Create
.env.localto override defaults; fill in:- Adapter paths:
CONF_zigBeeAdapterPort,CONF_zigBeeAdapterName,CONF_loRaAdapterPath,
- Adapter paths:
- Install dependencies:
Start services (each in its own terminal or managed via a process manager):
# MQTT broker node broker/app.js # Server node server/app.js # Bridges node "bridge-bluetooth/app.js" node "bridge-zigbee/app.js" node "bridge-lora/app.js" node "bridge-http/app.js" node "bridge-integrations/app.js"
If you want to use it for production (only macOS / Linux), just run
chmod +x production-start.sh
./production-start.sh # maybe with sudo or
chmod +x production-stop.sh ./production-stop.sh
production-start.sh uses the process manager, so that a service is restarted if it crashes. The relevant logs can be found in the logs folder.
Installation example for Raspberry Pi:
# Update and reboot the system sudo apt update sudo apt full-upgrade -y sudo apt install -y git curl bluetooth bluez sudo reboot # Install Node.js curl -fsSL https://deb.nodesource.com/setup_lts.x | sudo -E bash - sudo apt install -y nodejs # Activate Bluetooth sudo systemctl enable bluetooth sudo systemctl start bluetooth sudo rfkill unblock bluetooth # Clone repository git clone https://github.com/janrakete/healthcore-bulp.io.git cd healthcore-bulp.io npm install # Find out ZigBee adapter port ls -l /dev/serial/by-id # change this in .env.local, i.e. CONF_zigBeeAdapterPort=/dev/ttyUSB0 # Start Healthcore and put it to autostart chmod +x production-start.sh ./production-start.sh
🔐 Security
Warning
By default, Healthcore is initially unsecured to facilitate configuration and development.
If CONF_apiKey, CONF_corsURL, CONF_brokerUsername/CONF_brokerPassword and/or CONF_tlsPath remain empty in the .env.local file, security measures are inactive; however, they can be enabled as follows:
-
CORS: Cross-Origin Resource Sharing (CORS) is a mechanism that enables Healthcore to specify which origins (domain, scheme, or port) are authorized to access the API. To define these permitted origins, the respective values must be entered as a comma-separated list under
CONF_corsURLin the.env.localfile. Please ensure that URLs do not include a trailing slash (/), as this is generally not required and may lead to configuration errors. -
API: To implement API key authentication, the key must be defined in the
.env.localfile underCONF_apiKey. Subsequent requests to the API must include thex-api-keyheader containing the specified key. -
TLS (HTTPS): To further secure API communication, a certificate can be used. This can be created using https://github.com/FiloSottile/mkcert. The created files must be named
cert.pemandkey.pem. Please keep these files outside the repository, so there is no chance to commit them accidentally. You can change the path in.env.localviaCONF_tlsPath. Default is same level as the repository. If a certificate is set, then automatically MQTTS instead of MQTT is used. So you have to changeCONF_brokerAddresstomqtts://localhost:9999in.env.local. -
MQTT: To use an authentification for MQTT, set
CONF_brokerUsernameandCONF_brokerPasswordin.env.local.
🔌 API communication
Healthcore provides a comprehensive API that allows you to control all data and devices in a standardized way. Here is a complete example of connecting to a ZigBee device.
You can explore all APIs using Swagger:
http://localhost:9998/api-docs/
(9998 is the standard server port and localhost the standard base URL, configured in .env - overwrite it in .env.local if you want)
Available routes:
/info:Info routes provide runtime and health details about the server and connected services/data:Data routes offer generic CRUD access to allowed database tables with filtering and update controls/devices:Device routes handle discovery, registration, connection management, metadata updates, and reading/writing device values across bridges/devices-groups:Devices can be grouped together, e.g., “All motion detectors”/scenarios:Scenario routes let you create, manage, and execute automation logic made of triggers and actions/alerts:Alert routes expose alert lists, statistics, single-alert lookup, and status updates/reports:Report routes return stored reports and trigger manual report generation for a selected time range/update:Update routes check for newer versions and install the latest code from the repository
Example for using ZigBee device:
// Base URL const API = "http://localhost:9998"; // 1) Start scan (pairing mode) const scanResponse = await fetch(API + "/devices/zigbee/scan", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ duration: 30 }) }); const scanData = await scanResponse.json(); const callID = scanData.data.callID; // 2) Read discovered devices (wait a few seconds after starting scan) const infoResponse = await fetch(API + "/devices/zigbee/scan/info?callID=" + callID); const infoData = await infoResponse.json(); const foundDevice = infoData.data.devices[0]; // getting first device // 3) Get current values const valuesResponse = await fetch(API + "/devices/zigbee/" + foundDevice.uuid + "/values"); const valuesData = await valuesResponse.json(); console.log(valuesData); // 4) Set value(s) (only works for writable properties) await fetch(API + "/devices/zigbee/" + foundDevice.uuid + "/values", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ values: { state: "on" } }) });
If a value write does not change anything, check the converter of your device in bridge-zigbee/converters/ and verify that the property is writable.
If you need to find the IP address of the server on the local network: The Healthcore server uses a Bonjour service to make itself known on the network. The default identifier is “healthcore”, but it can be customized in the .env file with CONF_serverIDBonjour.
📈 Healthcheck
Healthcore has an integrated dashboard (= Healthcheck) to view the status of the system and to display several data from the APIs.
How to start Healthcheck:
Then open a browser und type:
9990 is the standard port Healthcheck and localhost the standard base URL, configured in .env - overwrite it in .env.local if you want. If you also want to call the Healthcheck from another client in the same network, please use the IP that is displayed in the console after start.
🧩 Own converters
The own converters subsystem lets you transform raw device data (e.g., binary BLE characteristic values) into structured JSON properties that your interface (i.e. your app) can use. Each bridge (Bluetooth, ZigBee, LoRa, HTTP) has its own converters/ folder with individual converter classes extending a shared ConverterStandard base.
Important
As an open-source project, Healthcore thrives on having as many converters as possible. So please add your converters via a pull request or as an issue. Use GitHub Copilot, Claude Code or Codex with the following prompt to create converters for well-known smart home components: “Create a ZigBee converter for the SONOFF S60ZBTPF. Use the examples in /bridge-zigbee as a guide.”
Below is a detailed Bluetooth device example:
-
Create a new JS file in the bridge’s
converters/folder (e.g.Converter_MyConverter.js). -
Extend
ConverterStandard:In
Converter_MyConverter.js, import the base and declare your class:const { ConverterStandard } = require("./ConverterStandard.js"); class Converter_MyConverter extends ConverterStandard { static productName = "bulp-AZ-123"; // Must match the product name that is reported by the device. constructor() { // Initializes base converter internals and standard property catalog. super(); // Metadata used when the device is created in Healthcore. this.powerType = "MAINS"; this.vendorName = "bulp.io"; // Custom BLE characteristics (UUIDs) this.properties["19b10000e8f2537e4f6cd104768a1217"] = { // BLE characteristic UUID used as key name: "rotarySwitch", // property name exposed by the API reportingInclude: false, // do not include this field in reporting output reportingRole: "actuator", // reporting role metadata used by Healthcore notify: true, // subscribe to notifications when value changes read: true, // allow reading this value from the device write: false, // no writing to this characteristic anyValue: 0, // fallback/example value type for validation valueType: "Numeric" // value kind shown to the app layer }; this.properties["19b10000e8f2537e4f6cd104768a1218"] = { name: "speaker", reportingInclude: false, reportingRole: "actuator", notify: false, read: true, write: true, anyValue: ["on", "off"], valueType: "Options" }; // Optional: include standard BLE properties from ConverterStandard this.properties["2a19"] = { standard: true, read: true }; // Replaces { standard: true } placeholders with full standard metadata. this.resolveStandardProperties(); } // Convert raw BLE values to Healthcore value objects. get(property, value) { if (property.read === false) { return undefined; } // Use shared conversion logic for standard UUIDs. if (property.standard === true) { return this.getStandard(property, value); } // Handle device-specific UUIDs. switch (property.name) { case "rotarySwitch": const buf = Buffer.from(value); return { "value": buf[0], "valueAsNumeric": buf[0] }; case "speaker": return value[0] === 1 ? { "value": "on", "valueAsNumeric": 1 } : { "value": "off", "valueAsNumeric": 0 }; default: return undefined; } } // Convert app values to raw bytes before writing to the BLE device. set(property, value) { if (property.write === false) { return undefined; } switch (property.name) { case "speaker": // Only allow values listed in property.anyValue. if (property.anyValue.includes(value)) { return Buffer.from([value === "on" ? 1 : 0]); } return undefined; default: return undefined; } } } module.exports = { Converter_MyConverter };
-
Auto-load:
Converters.jsdynamically requires all files inconverters/(excludingConverterStandard.js), detects the staticproductName, and registers your class.
💓 External data via APIs
The Healthcore can receive data directly from devices, but it can also retrieve and store data through APIs provided by services such as Google Health or Garmin.
These integrations are implemented through bridge-integrations. Each data provider must have its own implementation in the converters subfolder. A "Google Health" integration already exists and will be used as the example below.
1. Create a Google Cloud project
- Sign in to Google Cloud Console with your Google account
- Create a new project
- Enable the "Health API" for the project
- Configure the OAuth consent screen and customize the branding
- Create an OAuth2 client of type "Desktop Application"
- Download and save the generated client credentials file
- Under "Audience", add test users (only these users can authorize access)
- Under "Data Access", grant the following scopes:
googlehealth.activity_and_fitness.readonlygooglehealth.health_metrics_and_measurements.readonlygooglehealth.nutrition.readonlygooglehealth.sleep.readonlygooglehealth.irn.readonlygooglehealth.ecg.readonly
2. Obtain OAuth tokens
Open the following URL in your browser (but replace {{CLIENT_ID}} with client ID from the saved file first):
https://accounts.google.com/o/oauth2/v2/auth?client_id={{CLIENT_ID FROM FILE}}&redirect_uri=http://localhost&response_type=code&access_type=offline&scope=https://www.googleapis.com/auth/googlehealth.activity_and_fitness.readonly https://www.googleapis.com/auth/googlehealth.health_metrics_and_measurements.readonly https://www.googleapis.com/auth/googlehealth.nutrition.readonly https://www.googleapis.com/auth/googlehealth.sleep.readonly https://www.googleapis.com/auth/googlehealth.irn.readonly https://www.googleapis.com/auth/googlehealth.ecg.readonly
Sign in with one of the configured test users and complete the consent flow.
After approval, copy the value of the code parameter from the final URL displayed in your browser.
3. Exchange the code for tokens
Send a POST request to (replace the {{}} with the known values):
https://oauth2.googleapis.com/token?code={{CODE FROM ABOVE}}&client_id={{CLIENT_ID FROM FILE}}&client_secret={{CLIENT_SECRET FROM FILE}}&redirect_uri=http://localhost&grant_type=authorization_code
4. Store the tokens
The response contains:
access_tokenexpires_inrefresh_token
Insert these values into the integrations_accounts table together with an arbitrary "Account ID".
5. Create the device
Add a new device through the API.
The device's "Device ID" must match the "Account ID" stored in the integrations_accounts table.
Wait, one more step: encrypting tokens!
To encrypt the tokens stored in the database:
-
Define a secret key in
.env.localusing the variableCONF_credentialEngineSecret -
Encrypt the tokens using AES-256-GCM and the configured secret key.
-
Replace the plain-text values in the
integrations_accountstable with the encrypted versions.
🤖 Using an LLM for reports
To generate reports for each person and the rooms they live in using a local LLM, download a free GGUF model (for example from https://huggingface.co/mradermacher/models?search=instruct). Make sure the filename contains Instruct, as these models are optimized for instruction-following tasks.
Copy the downloaded model into the /libs/ReportingEngine-models directory.
Then update the .env.local file and set the CONF_reportingEngineModel variable to the name of the downloaded model file (i.e. "HARC-Qwen2.5-7B-Instruct.Q4_K_M.gguf").
You can now generate and access reports through the following API endpoints:
/reports/generate/reports
🔎 Testing
You can test Healthcore in two different ways: automated tests and manual tests.
Automated tests
The automated tests live in the tests/ folder and are powered by Jest. They cover converters for all bridges, data CRUD operations, device management, scenario logic, SQL validation, and authentication. Everything runs against an in-memory SQLite database, so no real hardware or running services are needed.
To run all automated tests:
That's it. If something breaks, you'll know immediately.
Manual tests
Some things simply can't be automated — real Bluetooth adapters, physical ZigBee devices, push notifications on actual phones, network resilience, and end-to-end flows through the entire system. That's where the manual test plan comes in.
The full manual test plan is documented in tests/MANUAL.md.
The rule is simple: first run npm test to make sure all automated tests pass, then work through the manual tests when you have the hardware connected.