Self-hosted агрегатор данных о здоровье с AI-чатом поверх. Один развёрнутый инстанс обслуживает одну семью. Всё хранится на твоём железе, наружу уходят только запросы к AI-провайдеру, ключ к которому ты прописал сам.
Три вкладки:
- chat — спросить AI «как я сплю», «какой был пульс в 12:35», «нарисуй
график глюкозы за неделю». AI сам ходит в БД через тулзы
(
query_metrics,render_chart,list_metric_types) — не угадывает «по памяти», достаёт точные значения. Графики рендерятся прямо в чате. - database — таймлайн всех точек данных. Фильтр по типу, ручной ввод, infinite-scroll (доскролл вниз → загрузится ещё пачка по 500).
- integrations — подключённые источники, AI-модели, темы, system prompt и memory — долгосрочная markdown-память AI про юзера (что AI сам запомнил между диалогами + что попросил юзер).
Роли:
- admin — настраивает AI-модели (общие на семью), раздаёт инвайты, видит таймлайн и чат по данным всех. Своих устройств не подключает.
- user — подключает свои источники (Apple Health / Oura / Libre / погода и т.д.), смотрит свой таймлайн, общается с AI поверх своих данных. Использует AI-модели прописанные админом.
Все интеграции в integrations/<id>/ — папка с index.ts экспортирует
HealthProvider, остальное (UI, scheduling, маппинг, дедуп) подхватится
автоматически.
| провайдер | режим | что тянет |
|---|---|---|
| apple-health | push (webhook) | Apple Watch + iPhone через iOS-приложение Health Auto Export. HR / HRV / sleep_analysis / steps / workouts / SpO2 / temp / 30+ метрик |
| oura | pull (OAuth2, 15м) | sleep / readiness / activity / HRV / heartrate per-sample / workouts / spo2 / VO2max / tags. HR с backfill'ом 48ч (ring → phone → cloud имеет лаг) |
| libre | pull (LibreLinkUp, 15м) | глюкоза с FreeStyle Libre 2/3 через LLU sharing |
| weather | pull (Open-Meteo, 60м) | текущая погода по городу + прогноз на 48ч вперёд (типы weather_*_forecast, обновляются каждый sync) |
Apple Health push-режим: после подключения карточка показывает персональный webhook-URL с токеном — вставляешь его в HAE → Automations → REST API, и iPhone начинает раз в N минут POST'ить JSON на твой биомон.
Поддерживаются 4 провайдера: OpenAI, Anthropic, Google Gemini, OpenRouter (агрегатор сотен моделей по одному ключу). Все стримятся SSE, все понимают tool-calling в своих нативных форматах.
Tools (AI вызывает сам когда нужно):
query_metrics(type, since?, until?, hours_back?, aggregation?, limit?)— выборка точек.since/until— в локальной TZ юзера,aggregation=raw/hourly/daily(min/avg/max по бакетам).render_chart(type, ..., kind?: line | bar)— рисует SVG-график инлайн в чат, AI получает только summary (не дублирует точки в тексте).list_metric_types()— какие типы есть у юзера (если AI не уверен в имени).remember(note)— append bullet- (YYYY-MM-DD) <note>в долгосрочную память юзера. Вызывается обязательно когда юзер сказал «запомни X», + по инициативе AI для важных фактов (диагнозы, аллергии, цели, режим).replace_memory(content)— полная перезапись markdown'а.
Контекст, который AI получает в system prompt:
- system-prompt юзера (кастомный или сгенерированный из подключённых источников)
- блок MEMORY — то что AI запомнил между сессиями
- инструкции про tools (когда звать, когда нет)
- сжатые bucket'ы метрик за год — обзорный фон, для точных значений AI
должен звать
query_metrics
История сообщений в БД хранится с meta (tool-chip'ы и charts), поэтому
при перезагрузке чата UI восстанавливает всё что юзер видел в стриме.
Nuxt 3 + Nitro + PostgreSQL 16 + Drizzle ORM + nuxt-auth-utils
Tailwind CSS+Caddy 2(HTTPS reverse-proxy с self-signed CA). Сессионные cookie, AES-256-GCM на все third-party креды в БД, ключMASTER_KEYживёт в.envна сервере. SSE-стримы пробрасываются через Caddy сflush_interval -1.
Темы — в themes/<id>/index.ts, доступны через переключатель в
Integrations или cookie (для auth-страниц до логина).
git clone <this-repo> biomon && cd biomon
./install.shСкрипт сгенерит MASTER_KEY и session secret, поднимет Postgres,
приложение и Caddy в docker compose, прогонит миграции и распечатает
URL. Бэкап MASTER_KEY ОБЯЗАТЕЛЕН — потеря ключа = все
сохранённые API-ключи и креды устройств нечитаемы.
После первого запуска зайди на https://<host>/setup — заведи
админский аккаунт. Регистрация закроется; новые юзеры заходят только по
инвайту.
Положи папку в integrations/:
integrations/
fitbit/
index.ts // export default HealthProvider
Минимум для pull-провайдера:
const fitbit: HealthProvider = {
id: 'fitbit', version: 1, displayName: 'Fitbit',
description: '...', glyph: '◉', syncIntervalMinutes: 30,
getSetupSteps() { return [...] },
validateCredentials(c) { ... },
async sync(connection, since, creds) {
return { metrics: [...], credentialsPatch?, configPatch?,
deleteFutureTypes? }
},
}
export default fitbitДля push-провайдера (webhook вместо опроса) дополнительно реализуй
ingest(connection, payload, creds) и создай роут
/server/api/integrations/<id>/ingest.post.ts (см. apple-health как
референс). sync() можно оставить no-op возвращающим { metrics: [] }.
deleteFutureTypes: string[] — список типов, у которых перед инсертом
стираются все будущие записи (recorded_at > now) этой connection. Нужно
для forecast'ов чтобы старые предсказания не накапливались.
pnpm install
pnpm dev # http://localhost:3000
pnpm db:generate # после изменения server/db/schema.tsХук в nuxt.config.ts авто-сканит /integrations/ и /themes/ и
генерит registry.generated.ts на каждый билд/dev-start — реестры
обновляются сами.
- Все API-ключи моделей и креды устройств шифруются AES-256-GCM на
MASTER_KEYперед попаданием в БД. - Расшифровка только на бэкенде в момент использования.
- AI обычного юзера физически не может видеть данные другого:
buildHealthContext(userId)принимает ровно одинuser_id. Toolquery_metricsиспользуетctx.userIdиз сессии — нельзя подменить через arguments. Семейный путь — отдельный методbuildFamilyHealthContext(), доступный только из админских роутов (session.scope === 'all'требуетrequireAdmin). - Apple Health webhook авторизуется по per-connection токену (192 бита,
cleartext в
config.ingestToken— это shared-secret, юзер видит и копирует в HAE). - Session cookie
httpOnly. На проде Caddy подаётtls internal→ HTTPS обязателен,COOKIE_SECURE=true.
MIT — см. LICENSE.
