Mirobody v2
The C++ port of the Python mirobody server, built to run inside the Android and iOS host app as well as standalone on desktop or a server. Because the core runs on-device, all data can stay on the phone - it never has to leave the device unless you choose to sync it.
Live demo: test.mirobody.ai
A lightweight C++ server that links personal health data to LLMs - it pulls wearable, lab, and clinical records from health-data platforms and feeds them to multiple LLM providers behind one uniform interface. Five pieces make up the core:
-
LLM clients (src/llm/) - one streaming
llm::Clientper provider (OpenAI, Google Gemini, and MiroThinker today), with new providers slotting in behind the same contract. The native apps (Android · iOS · Qt · Electron) additionally offer a fully on-device option — Gemma 4 via LiteRT-LM (llama.cpp on Electron) — so chat can run with no server and no network at all. -
MCP tools (src/mcp/ + res/mcp_tools/) - server-side tools exposed over a Model Context Protocol endpoint for clients to discover and call.
-
Agents (src/chat/ + res/agents/) - pick a client by provider name, build the system prompt, and stream a turn back.
-
Health platforms (src/health/vendor/) - one
vendor::Vendorper source, brokering wearable, lab, and clinical-EHR data behind a common authorize / fetch / webhook contract, resolved by id through the registry. Each implements transport against the platform's public API (fetcheverywhere, plus consent/webhook where documented; undocumented operations stay explicit stubs). Clients sort into three buckets:platform/- 15 B2B aggregators (Terra, Validic, Human API, …)phone/- smartphone-vendor stores with a cloud API (Huawei)device/- consumer device brands (Fitbit, Withings, Garmin)ehr/- direct EHR systems via SMART on FHIR — one generic client for every ONC-certified EHR (Epic, Oracle Health/Cerner, athenahealth, …), with a directory loader that discovers each tenant's FHIR base URL from public Service Base URL lists (ONC Lantern, vendor bundles)- on-device-only stores (Apple Health, Samsung Health, Google Health Connect, Xiaomi) have no server API and so no client — the host apps read them on-device (Health Connect / HMS Health Kit on Android, HealthKit on iOS) and POST FHIR Observations instead
See src/health/vendor/README.md for the platform comparison the metadata is drawn from, plus the per-vendor implementation-status table.
-
FHIR R4 (src/fhir/) - an embedded RESTful FHIR R4 endpoint plus the terminology machinery behind it:
- uploaded documents are parsed into indicators and values
- units are normalized to UCUM; indicators mapped to SNOMED CT / LOINC / RxNorm
- on-device health data the Android / iOS apps read (Health Connect / HMS Health
Kit / Apple HealthKit) is POSTed to the same write endpoint as
Observations - the results are served as FHIR resources
See src/fhir/README.md.
Agents and tools both self-register at compile time: drop a .cpp in the
matching res/ directory and rebuild.
On desktop/embedded it runs as a standalone binary serving HTTP + WebSocket over the network. On Android and iOS the same core ships inside the host app - as a shared library loaded over JNI on Android, and as a static library linked through the C API in src/mirobody.h on iOS.
That C API is a plain extern "C" surface, so any language with a C FFI can
embed the core without going through the HTTP/WebSocket front door - Java
(JNI/JNA/Panama), Go (cgo), C# (P/Invoke), Rust (extern "C"), Swift,
Python (ctypes/cffi), and so on.
Health data flows the same way with or without a backend. With a server, device
uploads land in a transient health_ingest_staging inbox that a background worker
drains into Postgres (hot: health_indicators + health_facts) and Parquet (cold,
raw), while fhir_resources keeps the FHIR-truth summary Observations. Fully
on-device, the same mirobody_core pipeline runs inline into a local SQLite file
with the identical schema — no inbox, worker, or cold tier.
Why open device access
If a wearable's moat were purely its algorithms, opening the raw Bluetooth (BLE GATT) layer wouldn't threaten it — yet most consumer vendors keep it closed. The commonly cited reasons are market dynamics, not any one company's practice:
- The moat is usually broader than the algorithm — a historical-data flywheel plus subscriptions that sell processed outputs (scores, readiness), which open raw access can dilute.
- Little upside for incumbents — at scale a developer ecosystem barely moves hardware sales, while an open protocol costs documentation, compatibility, and support. The incentive is asymmetric: low for large players, high for open ones.
- Path dependency — products designed closed years ago are expensive to reopen (pairing, security model, every existing app).
The public counter-example is Polar: its H10/H9 chest straps implement the standard GATT Heart Rate service, so any app can read them directly — and by many accounts that openness helped make them a default heart-rate source for developers.
mirobody's thesis: when your value isn't a locked algorithm subscription, open ingestion is a feature, not a leak. It reads whatever speaks an open standard — standard BLE GATT sensors (and legacy classic-Bluetooth HDP devices), the phone health stores (Apple Health / Health Connect / HMS), and EHRs over SMART on FHIR — normalizes everything to FHIR R4, and keeps it on your device unless you choose to share it. See src/health/README.md.
Quick start
The default desktop database backend is POSTGRESQL, so these install
the Postgres client (libpq) alongside the core deps. For other backends,
Fedora/RHEL packages, and full options, see Building - desktop.
Signing in — demo login is off by default. A fresh checkout configures no email delivery or social provider, so to sign in without setting one up first, enable the built-in demo accounts: uncomment the
EMAIL_PREDEFINE_CODESblock inconfig.example.yml(it maps[email protected]→777777, and so on). It ships commented out on purpose — while active, anyone who can reach the server could sign in with those well-known credentials, so keep it disabled on any reachable deployment and rely on real email-code or social sign-in there. Every mention of demo credentials below assumes you have enabled this block.
Linux
# Debian / Ubuntu / WSL
sudo apt install build-essential cmake ninja-build pkg-config \
libwebsockets-dev libcurl4-openssl-dev libssl-dev \
rapidjson-dev libyaml-cpp-dev libhiredis-dev libpq-dev \
libjpeg-dev libpng-dev libtiff-dev libwebp-dev
./build.sh # -> build/mirobody
./mirobody # reads ./config.yml
macOS
brew install cmake ninja pkg-config libwebsockets curl openssl@3 \
rapidjson yaml-cpp hiredis libpq \
jpeg-turbo libpng libtiff webp
./build.sh # -> build/mirobody
./mirobody # reads ./config.yml
Note — response compression. The distro/Homebrew
libwebsocketspackages are built withLWS_WITH_HTTP_STREAM_COMPRESSIONoff, so a server linked against them serves every response uncompressed (e.g.assets/index.jsat ~520 KB). That's fine for local development. The production Docker image instead builds libwebsockets from source with-DLWS_WITH_HTTP_STREAM_COMPRESSION=ON -DLWS_WITH_ZLIB=ONso it gzip/deflates responses — do the same for any bandwidth-sensitive deployment.
Windows
Install Visual Studio with the Desktop development with C++ workload and the
C++ CMake tools component (provides Ninja and a bundled vcpkg). vcpkg pulls
the native deps from vcpkg.json automatically - no manual installs.
build.cmd :: configure + build -> build\mirobody.exe
build\mirobody.exe :: reads .\config.yml
Webpage
The web client is a static single-page app in htdoc/, served by the
standalone desktop/server binary (the mobile builds have no static surface).
Build it into res/htdoc, where HTTP_ROOT points by default; mirobody then
serves it at http://localhost:8080:
cd htdoc
npm install
npm run build # -> res/htdoc, served by mirobody at :8080
For live-reload development, run the webpack dev server (port 8090) - it proxies
API calls to a mirobody backend on :8080:
npm start
The login screen offers email one-time-code sign-in plus whichever social
providers are configured - Google / Apple / X (via Firebase), WeChat, GitHub, and
Tanka QR-code login (on by default). Tanka is the one provider with a runtime
node dependency: the server self-heals Tanka's request-signer by running
res/tanka/discover.cjs at boot and weekly, falling back to a pinned build when
node or Tanka is unavailable. See src/user/README.md for
the flow and the TANKA_* keys in config.example.yml.
Android
Native deps come from prebuilt sysroots under android/prebuilt/<ABI>/
(arm64-v8a only today). Then build the host app, which bundles
libmirobody.so:
cd android
./gradlew assembleRelease
See Building - Android for the prebuilt-sysroot setup.
iOS
Native deps come from prebuilt sysroots under ios/prebuilt/<sdk>-<arch>/.
Build the arm64 device slice and link it into the SwiftUI host via the C API:
cmake -S . -B build-ios-arm64 -G Xcode \
-DCMAKE_TOOLCHAIN_FILE=path/to/ios.toolchain.cmake \
-DPLATFORM=OS64 -DDEPLOYMENT_TARGET=15.0 \
-DCMAKE_PREFIX_PATH="$(pwd)/ios/prebuilt/iphoneos-arm64"
cmake --build build-ios-arm64 --config Release
See Building - iOS for the simulator slice, the xcframework packaging, and Swift usage.
Desktop (Electron)
electron/ wraps the server in an Electron desktop app: the main
process embeds libmirobody in-process via koffi FFI (the same C API as the
other FFI hosts) and a BrowserWindow loads the htdoc web client from the
embedded server's loopback port - no separate process, and no electron-rebuild
across Electron's Node versions. Build the shared library and the web client
first, then run from the repo root:
./build-shared.sh # -> build-shared/libmirobody.* (build-shared.cmd on Windows)
( cd htdoc && npm run build ) # -> res/htdoc (already built in a fresh checkout)
( cd electron && npm install && npm start )
It uses the self-contained SQLite backend, so no external database is needed;
the embedded server runs off the committed config.example.yml,
so enable demo login there (uncomment EMAIL_PREDEFINE_CODES — see the note under
Quick start), sign in with [email protected] / 777777, and add
an LLM key to enable chat. npm run dist packages it with
electron-builder.
See electron/README.md for the extraResources packaging
layout, the writable-vs-read-only path handling, and the fixed-port note.
Desktop (Qt)
qt/ is a native Qt Quick (QML) desktop client - the C++ counterpart
of the web client in htdoc/. Unlike Electron, it does not embed
libmirobody; it is a pure HTTP/SSE API client built on Qt's networking stack,
so it talks to any running mirobody backend over the network. It mirrors the
web client's main flow (email-code login, streamed agent/proxy chat, provider
picker, history, settings, ten languages). It is off by default and needs
Qt 6.5+. On Windows build with MSVC (the msvc2022_64 kit; matches
build.cmd and LiteRT-LM's Windows toolchain); on Linux/macOS use GCC/Clang.
:: Windows (MSVC) — standalone wrapper script in qt/, no mirobody_core deps needed
qt\build-qt.cmd deploy :: -> build-qt\app\mirobody_qt.exe (windeployqt'd)
# Linux / macOS
qt/build-qt.sh # -> build-qt/app/mirobody_qt
# ...or from the top-level build on any OS:
cmake -B build-qt -S . -DMIROBODY_BUILD_QT=ON \
-DCMAKE_PREFIX_PATH=/path/to/Qt/6.x/<compiler>
cmake --build build-qt --target mirobody_qt
Launch it, open ⚙ → Backend to point at a server (default
http://127.0.0.1:8080), and sign in with a demo code once demo login is enabled
on that backend (uncomment EMAIL_PREDEFINE_CODES; e.g. [email protected] /
777777). Because the target pulls in no mirobody_core deps, it can also be
built on its own.
See qt/README.md for the architecture, the API mapping, and the two intentional omissions (social sign-in, KaTeX math).
Wechat Miniapp
A native WeChat Mini Program client lives in miniapp/ (WXML/WXSS/JS,
no build step). It mirrors the web client: the same {code, msg, data} envelope,
bearer-JWT auth, and streamed /api/chat agent protocol. Open the folder in
WeChat DevTools as a Mini Program project - it runs directly,
no npm install:
miniapp/
project.config.json # set your Mini Program AppID here
config.js # backend baseUrl (dev: http://localhost:8080) + language
pages/login/ # wx.login() -> POST /wechat/verify -> token
pages/chat/ # provider picker + streamed agent chat
Login goes through POST /wechat/verify: the page hands the wx.login()
code to the server, which exchanges it via WeChat's jscode2session
(WECHAT_APPID / WECHAT_SECRET in config.yml) for the user's
openid and mints the same tokens as /email/verify.
See miniapp/README.md for the full setup, the backend
contract, and the streaming-over-wx.request details.
Feature parity
Where each client stands today. ✅ done · 🚧 partial · — not yet. Electron
embeds the htdoc web UI, so it inherits every web feature and adds an
on-device LLM.
| Feature | Web | Android | iOS | Electron | Qt | Miniapp |
|---|---|---|---|---|---|---|
| Chat & content | ||||||
| Streaming replies | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
| Thinking / reasoning trace | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
| Tool-call cards (MCP) | ✅ | ✅ | ✅ | ✅ | — | — |
| Markdown | ✅ | ✅ | ✅ | ✅ | ✅ | — |
| Math (LaTeX / KaTeX) | ✅ | ✅ | ✅ | ✅ | — | — |
| Charts (ECharts) | ✅ | ✅ | ✅ | ✅ | — | — |
| Inline images | ✅ | ✅ | ✅ | ✅ | — | — |
| Attachment upload | ✅ | ✅ | ✅ | ✅ | — | ✅ |
| Model | ||||||
| Provider / model picker | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
| On-device LLM | — | ✅ | ✅ | ✅ | ✅ | — |
| Accounts & privacy | ||||||
| Sign in | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
| Multi-account switch | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
| Account avatar + nav drawer | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
| Incognito mode | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
| Chat history + resume | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
| Health & sharing | ||||||
| Phone health read | — | ✅ | ✅ | — | — | 🚧 |
| EHR (SMART on FHIR) | ✅ | ✅ | ✅ | ✅ | ✅ | — |
| Device / vendor connect | 🚧 | ✅ | ✅ | 🚧 | ✅ | — |
| Care circles / sharing | ✅ | ✅ | 🚧 | ✅ | 🚧 | ✅ |
| Settings | ||||||
| Language switch (i18n) | ✅ | ✅ | ✅ | ✅ | ✅ | 🚧 |
| Font size | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
| Backend URL | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
| Dark theme | — | ✅ | ✅ | — | — | — |
Notes
- On-device LLM — LiteRT-LM (Gemma 4) on Android / iOS / Qt, llama.cpp (Gemma GGUF) on Electron. A plain browser has no on-device model; the web app exposes it only when running inside Electron.
- Phone health read — Health Connect + HMS Health Kit (Android), HealthKit (iOS); the miniapp reads WeChat WeRun step data only. Qt and iOS additionally ingest BLE sensors directly.
- Device / vendor connect — connect / list / disconnect for cloud vendors (Fitbit, Withings, Garmin, Terra, …) is fully wired on Android / iOS / Qt; on Web / Electron the list and disconnect work but the connect OAuth flow is still a placeholder.
- Care circles / sharing — create / manage circles and share threads on Android, Web / Electron and Miniapp; iOS and Qt can open a conversation shared to you (read-only) but can't create shares yet.
- Miniapp language — the picker sets the model's reply language; the UI copy itself is Chinese only.
- Dark theme — Android and iOS follow the system light / dark scheme; the other clients ship a single light palette.
- iOS math — MarkdownUI has no KaTeX equivalent, so math-bearing replies render
through an offline KaTeX WebView (
MathMarkdownText); plain replies stay on native MarkdownUI. Math resolves when the turn settles — mid-stream it shows as source.
Database
mirobody_core keeps its in-process SQL persistence behind a single Database
class, linking exactly one concrete backend at build time:
- Backends —
SQLITE/DUCKDB/POSTGRESQL/POSTGRESQL_LEGACY/MYSQL/CLICKHOUSE, selected via theMIROBODY_DATABASE_BACKENDCMake option. - Defaults —
SQLITEon mobile,POSTGRESQLon desktop / server. - One per build — the backend is chosen at compile time, so call sites stay backend-agnostic.
See src/database/README.md for the full backend matrix, build-target defaults, the preprocessor macros each value defines, and the SQL-portability notes across the five dialects.
Cache
mirobody::cache::Cache is a Redis-flavored key/value store behind one type,
with a pluggable backend:
- API —
set/get/incr/decr/exists/del/expiretime/prune/flushdb/dbsize. - Backends — in-process
MemoryKv(zero-config default) or a hiredis-backed Redis connection. - Uniform — both expose the same
Cache open() constshape, so call sites read the same regardless of backend.
See src/cache/README.md for the usage example, the
expiration / eviction semantics, and the MemoryKv direct-access notes.
Storage
mirobody::storage::Storage is an object-store interface with runtime-selected
backends:
- API —
put_object/get_object/delete_object/presigned_url/public_url. - Backends — AWS S3 / S3-compatible, Alibaba Cloud OSS, Azure Blob, and local filesystem. Unlike the SQL
Database(one backend linked per build), all compile into every build and the caller picks one at runtime from config. - User uploads —
put_user_objectstores bytes under a per-user, content-addressed key (HMAC-hashed user segment and digest, content-type-derived suffix) with a<key>.metametadata sidecar;list_user_objectsscans a user's prefix back.
See src/storage/README.md for the backend table, the
configured() / open() usage example, the user-object key scheme,
LocalStorage setup, the named-OSS getter, and where request signing lives and
is tested.
Memory
mirobody::memory::Memory is a long-term memory interface — remember /
recall / forget — that stores durable facts about a user and recalls the
most relevant ones for a turn, surfaced to agents as the remember /
recall_memory MCP tools. Every backend compiles in; the caller picks one at
runtime via MEMORY_PROVIDER:
local(default) — facts plus 1024-dim embeddings in the appDatabase, ranked by in-process cosine over the caller's own rows; no extra services, works on every SQL backend.everos— an EverOS-compatible remote memory service.mem0— Mem0.zep— Zep.
These remote adapters talk to a SOTA memory service over HTTP. It is the mirobody analog of EverOS's memory subsystem.
See src/memory/README.md for the backend table, the
make_memory() usage example, the local cosine / pgvector-upgrade notes, the
remote-adapter caveats (opaque ids, server-side extraction), the config keys,
and how to add a backend.
Vendor
mirobody::vendor::Vendor is a health-data source interface — authorize_url /
list_providers / fetch / handle_webhook / revoke — over brokers of
wearable, lab-diagnostic, and clinical-EHR data. Every vendor compiles in; the
caller picks one at runtime by id via open_vendor(). The clients live under
src/health/vendor/ in three buckets:
platform/— 15 B2B aggregators (Terra, Validic, Human API, Junction, Metriport, …).phone/— smartphone-vendor stores with a cloud API (Huawei).device/— consumer device brands (Fitbit, Withings, Garmin).- on-device-only stores (Apple, Samsung, Google Health Connect, Xiaomi) have no client and feed the FHIR endpoint instead.
Alongside real, queryable VendorInfo metadata, each vendor implements transport
against the platform's public API: fetch everywhere (the vendor's native JSON or
FHIR, per platform), plus consent and webhook operations where the contract is
documented. Per-deployment hosts (self-hosted or contract-gated) require an
explicit base_url rather than a guessed one, and operations with no public
contract stay VendorError stubs — never fabricated.
See src/health/vendor/README.md for the competitive matrix the metadata is drawn from - positioning, data-source coverage, compliance posture, and integration style across all 15 platforms - and the implementation-status table tracking which operations are live per vendor.
Config
Config is resolved from layered sources, merged per key with higher layers overriding lower:
- Precedence (high → low) — environment variables →
config.yml→ remote config → committedconfig.example.ymltemplate. - Local — copy
config.example.ymltoconfig.yml(git-ignored) and edit that. - Remote — pulled only when
CONFIG_SERVER/CONFIG_TOKEN/ENVare all set (ENVselects which remote environment to fetch; it doesn't affect local files). - Encrypted values — values beginning with
gAAAAare Fernet-decrypted on load whenCONFIG_ENCRYPTION_KEYis set, one key shared across every source. - Sub-path mounting —
HTTP_URI_PREFIXcan mount the whole app under a sub-path.
See src/config/README.md for the full precedence list,
an example config.yml, the URI-prefix (sub-path mounting) details, remote-
config pull, and the encrypted-values derivation.
Building
The native dependency list (with licenses), toolchain minimums, and full build instructions for every target — desktop (Windows MSVC + vcpkg, and Linux / WSL / macOS), Android, iOS, the Python wheel, and the C-ABI shared library for embedding from Java / Go / C# / Node / Rust — live in docs/BUILDING.md.
The Quick start above has the condensed one-liner for the common platforms; reach for the full guide when you need the dependency/toolchain reference, cross-compilation, a non-default database backend, prebuilt-sysroot setup, or the embedding bindings.
HTTP API
| Method | Path | Purpose |
|---|---|---|
| GET | /api/health |
Liveness probe - returns ok. |
| POST | /api/chat |
Streams a chat response over SSE - runs the named agent, or (with no agent field) proxies the body to OpenAI /v1/chat/completions. Rate-limited per user (CHAT_RATE_MAX / CHAT_RATE_WINDOW_SEC; the shipped config.example.yml caps it at 5 turns / 60s, 0 disables). |
Paths are shown at the root; when HTTP_URI_PREFIX
is set they are served under it (e.g. /mirobody/api/health).
Errors come back as JSON:
{ "code": -1, "msg": "..." }
WebSocket routes
| Method | Path | Purpose |
|---|---|---|
| GET | /api/chat |
Live realtime bridge. JWT-guarded upgrade (bearer header or ?token=); each inbound JSON frame ({provider, system?, messages[]|question}) runs the matching realtime client (OpenAI Realtime / Gemini Live) and streams its events back as JSON frames, ending with {"type":"end"}. Shares the path with the POST /api/chat SSE route above. |
As with the HTTP routes, this is served under
HTTP_URI_PREFIX when it is set.
MCP
A Model Context Protocol JSON-RPC 2.0
endpoint lets MCP clients discover and invoke server-side tools. The service
(src/mcp/service.hpp) owns the route and dispatches the
standard methods (initialize, tools/list, tools/call, ping) to a
compile-time tool registry.
| Method | Path | Purpose |
|---|---|---|
| POST | /mcp |
JSON-RPC endpoint; authenticate with a bearer JWT. |
| POST | /mcp/{secret} |
Same endpoint, authenticated by a personal-MCP secret in the URL - for clients that can't send a bearer token. |
| POST | /personal/mcp |
Mint (or reuse) the caller's personal-MCP secret URL. |
Discovery (initialize / tools/list) is unauthenticated; tools flagged
auth resolve the caller's identity from the bearer JWT or the personal-MCP
secret before running. An auth-flagged call with no valid token returns
401 with a WWW-Authenticate: Bearer resource_metadata="…" header, which
points the client at the OAuth 2.0 authorization server below so it can obtain a
token. As with the other routes, paths are served under
HTTP_URI_PREFIX when set.
Adding a tool. Discovery is compile-time, not runtime: every file under
res/mcp_tools/ is globbed into the build and self-registers
via a MIROBODY_REGISTER_TOOL(...) line. A tool declares its parameters in a
small table (C++11 has no signature reflection); the registry expands that into
the MCP inputSchema and the OpenAI / Gemini function-descriptor variants. Drop
a new .cpp in that directory, rebuild, and the tool is live - see
res/mcp_tools/echo.cpp for the smallest example and
src/mcp/tool.hpp for the registry API.
OAuth 2.0
An embedded OAuth 2.0 authorization server lets MCP clients (and any standard
OAuth client) obtain access tokens through the browser authorization-code +
PKCE flow, rather than a human pasting a bearer token. It issues the same
mb_oauth JWTs the MCP endpoint already verifies; end-user login and consent
reuse the existing web client.
| Method | Path | Purpose |
|---|---|---|
| GET | /.well-known/oauth-protected-resource |
RFC 9728 resource metadata (root). |
| GET | /.well-known/oauth-authorization-server |
RFC 8414 server metadata (root). |
| GET | /.well-known/openid-configuration |
OIDC-discovery compatibility alias (same 8414 body). |
| GET | /.well-known/jwks.json |
Public JWKS for third-party token validators (RS256 only). |
| POST | /oauth/register |
RFC 7591 dynamic client registration. |
| GET | /oauth/authorize |
Authorization endpoint → web consent. |
| POST | /oauth/token |
authorization_code + refresh_token. |
| POST | /oauth/revoke |
RFC 7009 revocation (best-effort). |
The discovery documents are served at the host root (unaffected by
HTTP_URI_PREFIX); the endpoints they advertise carry the prefix. State
(registered clients, single-use codes) lives in the cache; refresh tokens are
stateless JWTs. Tokens are signed HS256 by default (JWT_KEY); set
JWT_PRIVATE_KEY to sign RS256 and publish a JWKS so third-party resource
servers can verify them without the secret. All keys are optional with working
defaults — see the OAUTH_* / JWT_* keys in config.yml and
src/oauth/README.md for the flow, security model, and
configuration.
FHIR
An embedded RESTful FHIR R4 endpoint (src/fhir/rest.hpp)
serves health records as generic, validated FHIR JSON. The wider goal: an
uploaded document (lab report, discharge summary, …) is parsed into indicators
and values, the units are normalized to canonical UCUM, the indicators are
mapped to SNOMED CT / LOINC / RxNorm codes, and the results are
materialized as FHIR resources here. It is the C++ port of the runtime half of
the Python mirobody.indicator package (the offline artifact builders stay in
Python; the core only consumes their output).
| Method | Path | Purpose |
|---|---|---|
| GET | /fhir/metadata |
CapabilityStatement (public discovery). |
| POST | /fhir |
batch / transaction Bundle. |
| GET | /fhir/{type} |
search → searchset Bundle (_id / _count / _offset). |
| POST | /fhir/{type} |
create (server-assigned id) → 201. |
| GET | /fhir/{type}/{id} |
read → 200 / 404 / 410 (deleted). |
| PUT | /fhir/{type}/{id} |
update or create-with-id → 200 / 201. |
| DELETE | /fhir/{type}/{id} |
delete (idempotent) → 204. |
Bodies are application/fhir+json; errors come back as an OperationOutcome.
Resource routes require a bearer JWT and are scoped to the authenticated user;
GET /fhir/metadata is public. Resources are persisted as generic JSON in a
fhir_resources table via database::Database, with the server injecting id
and meta (versionId / lastUpdated). As with the other routes, paths are
served under HTTP_URI_PREFIX
when set.
Status. Unit normalization and the REST server are built; the terminology resolver (indicator → code) and the document → FHIR pipeline are the next milestones. See src/fhir/README.md for the phase table, the generic-resource model, validation rules, and current limitations.
Reads and writes may target another user's records via ?subject=<member> —
an opaque care-circle member handle, never a raw user id — when a
care circle authorizes it; without it they stay scoped to the
authenticated user.
Care circles
A care circle layers opt-in sharing on top of the otherwise single-user core — it's whoever you trust with your health (family, a partner, a caregiver). Two things can be shared, each checked at the read/write path so no other table is rescoped:
- Conversations — grant a fellow member view/edit access to one chat thread; it then appears in their history.
- Health data — a per-member, per-circle switch (off / view / edit) that lets accepted members read — or read+write — your FHIR records, so the AI can answer "how is my family doing?".
Privacy — what the assistant can and can't reach
The view/edit switch governs two separate planes, and they are deliberately asymmetric:
- FHIR REST (
?subject=<member>) honors the full switch:viewreads,editreads and writes the sharer's records. This is the programmatic data plane (apps, integrations). - The AI assistant is read-only, always. When you pick a member in the chat
composer's "currently for" selector and ask on their behalf, exactly one
tool —
family_health— reads their data, and only withview-or-higher access. Even if they granted youedit, the assistant never writes to their records, memory, files, or chat history.editmatters only on the FHIR REST plane above.
Every other tool stays scoped to you, the signed-in caller, and never reads or writes the subject's data:
| Tool | Acts on the "currently for" subject? |
|---|---|
family_health |
Yes — read-only, gated by view+ health access |
whoami |
No — only flags that a subject is in focus (no id/data) |
list_files, read_file |
No — your uploads only |
recall_memory, remember |
No — your long-term memory only |
summarize_conversation |
No — your own current conversation only |
render_chart, echo |
No — touch no user data |
In other words, asking the assistant about a family member can only ever read
their health observations, and nothing the assistant does on their behalf can
modify their data or expose their files, memories, or conversations to you. See
src/chat/README.md for the threading details
(UserInfo::subject_user_id).
A circle is a group: any two accepted members are mutually in it. Roles are Member / Maintainer / Owner (maintainers and owners are admins who invite & remove; owners also rename, delete, and change roles). Invites go out by email and require acceptance.
| Method | Path | Purpose |
|---|---|---|
| POST | /api/circle/create | /rename | /delete |
manage circles you own |
| POST | /api/circle/invite | /accept | /decline | /remove |
membership (invite by email, acceptance required) |
| GET|POST | /api/circle/members |
every circle you belong to, |
No comments yet
Be the first to share your take.