diff --git a/.env.example b/.env.example index 0f7b1067b..30b6af11a 100644 --- a/.env.example +++ b/.env.example @@ -1,37 +1,106 @@ -# Database -DATABASE_URL="postgresql://healthlog:healthlog@localhost:5432/healthlog?schema=public" +# ============================================================================= +# HealthLog — Environment Configuration +# ============================================================================= +# Copy this file to .env and fill in the values. +# $ cp .env.example .env +# +# Required variables: DATABASE_URL or POSTGRES_PASSWORD, SESSION_SECRET, +# ENCRYPTION_KEY, API_TOKEN_HMAC_KEY +# +# Generate the three secrets with: +# $ openssl rand -hex 32 # any UNIX shell +# $ node -e 'console.log(require("crypto").randomBytes(32).toString("hex"))' +# +# All secrets must be 64-character hex strings (= 32 bytes). +# ============================================================================= -# App + +# ----------------------------------------------------------------------------- +# Database +# ----------------------------------------------------------------------------- +# When using docker-compose.yml: the app container reaches Postgres via the +# service name "db", so the hostname is "db" — NOT "localhost". +# When developing without Docker (`pnpm dev` against a host-installed Postgres): +# change the hostname to "localhost" and adjust the credentials. +DATABASE_URL="postgresql://healthlog:CHANGE-ME@db:5432/healthlog?schema=public" + +# Postgres password — used both by the db service AND by DATABASE_URL above. +# Pick a strong random password, e.g. `openssl rand -base64 24`. +POSTGRES_PASSWORD="CHANGE-ME" + + +# ----------------------------------------------------------------------------- +# App URL +# ----------------------------------------------------------------------------- +# The public URL where users reach the app. Behind a reverse proxy with TLS, +# this looks like https://healthlog.example.com. For local dev, leave default. NEXT_PUBLIC_APP_URL="http://localhost:3000" APP_URL="http://localhost:3000" + +# Node environment: "development" for `pnpm dev`, "production" for Docker. NODE_ENV="development" -# Session -SESSION_SECRET="change-me-to-a-random-64-char-hex-string" -# Encryption (for tokens, sensitive notes) -ENCRYPTION_KEY="change-me-to-a-random-64-char-hex-string" +# ----------------------------------------------------------------------------- +# Required Secrets — generate with `openssl rand -hex 32` +# ----------------------------------------------------------------------------- +# Signs session cookies. Rotating it logs out everyone. +SESSION_SECRET="REPLACE-WITH-OUTPUT-OF-openssl-rand-hex-32" -# HMAC key for API token hashing -API_TOKEN_HMAC_KEY="change-me-to-a-random-64-char-hex-string" +# AES-256-GCM key for encrypting Withings tokens, GitHub PATs, AI provider +# keys, and VAPID private keys at rest. Rotate carefully — old ciphertexts +# need re-encryption. +ENCRYPTION_KEY="REPLACE-WITH-OUTPUT-OF-openssl-rand-hex-32" -# Withings OAuth (M5) +# HMAC-SHA256 key for hashing external-API Bearer tokens before storing them +# in the database. Rotating it invalidates all issued tokens. +API_TOKEN_HMAC_KEY="REPLACE-WITH-OUTPUT-OF-openssl-rand-hex-32" + + +# ----------------------------------------------------------------------------- +# Withings OAuth (optional — for automatic scale/BP/activity sync) +# ----------------------------------------------------------------------------- +# Register an app at https://developer.withings.com/ and paste the credentials +# here. Per-user OAuth credentials can also be configured in Settings. # WITHINGS_CLIENT_ID="" # WITHINGS_CLIENT_SECRET="" -# WITHINGS_REDIRECT_URI="" -# WITHINGS_WEBHOOK_SECRET="" # random secret for webhook callback URL hardening +# WITHINGS_REDIRECT_URI="https://healthlog.example.com/api/withings/callback" +# WITHINGS_WEBHOOK_SECRET="" # any random string — used to harden the webhook URL -# Telegram webhook hardening (optional but recommended) + +# ----------------------------------------------------------------------------- +# Telegram (optional — webhook hardening) +# ----------------------------------------------------------------------------- +# Set to verify incoming Telegram updates. The bot token itself is configured +# per-user in Settings (encrypted at rest). # TELEGRAM_WEBHOOK_SECRET="" -# OpenAI (optional, user-provided keys stored encrypted) -# No server-side key needed - users bring their own -# Structured Logging (Wide Events) -# LOG_LEVEL="info" # debug | info | warn | error -# LOG_SAMPLE_RATE="1.0" # 0.0–1.0, fraction of successful requests to log -# LOG_SLOW_THRESHOLD_MS="3000" # Requests slower than this are always logged -# LOG_INCLUDE_STACK="true" # Include stack traces in error events -# LOKI_ENDPOINT="" # Optional: Loki Push API (e.g. http://loki:3100) -# LOKI_USERNAME="" # Optional: Loki basic auth username -# LOKI_PASSWORD="" # Optional: Loki basic auth password +# ----------------------------------------------------------------------------- +# AI Provider (optional) +# ----------------------------------------------------------------------------- +# Users bring their own API keys via Settings. No server-side key needed by +# default. The admin can configure a fallback key in the Admin Panel. + + +# ----------------------------------------------------------------------------- +# Structured Logging — Wide Events (optional, sensible defaults) +# ----------------------------------------------------------------------------- +# Levels: debug | info | warn | error +# LOG_LEVEL="info" + +# Sample rate for successful requests (0.0 = sample nothing, 1.0 = log all). +# Errors and slow requests are ALWAYS logged regardless of this setting. +# LOG_SAMPLE_RATE="1.0" + +# Slow-request threshold in milliseconds — anything slower is always kept. +# LOG_SLOW_THRESHOLD_MS="3000" + +# Include stack traces on error events. Disable in environments where logs +# leave the host unencrypted. +# LOG_INCLUDE_STACK="true" + +# Optional: ship Wide Events to Loki. Leave blank for stdout-only logging. +# LOKI_ENDPOINT="http://loki:3100" +# LOKI_USERNAME="" +# LOKI_PASSWORD="" diff --git a/README.md b/README.md index 554daeedf..250c930a9 100644 --- a/README.md +++ b/README.md @@ -67,21 +67,24 @@ Most health apps lock your data behind proprietary clouds, push subscriptions, a ## Quick Start +Plan ~5 minutes for a working install. The bundled `docker-compose.yml` builds the app from source — pre-built images on GHCR are coming in a separate release. + ```bash git clone https://github.com/MBombeck/HealthLog.git cd HealthLog cp .env.example .env ``` -Generate the required secrets: +Generate the four required secrets and paste them into `.env`: ```bash -openssl rand -hex 32 # SESSION_SECRET -openssl rand -hex 32 # ENCRYPTION_KEY -openssl rand -hex 32 # API_TOKEN_HMAC_KEY +echo "POSTGRES_PASSWORD=$(openssl rand -base64 24)" >> .env +echo "SESSION_SECRET=$(openssl rand -hex 32)" >> .env +echo "ENCRYPTION_KEY=$(openssl rand -hex 32)" >> .env +echo "API_TOKEN_HMAC_KEY=$(openssl rand -hex 32)" >> .env ``` -Add them to `.env`, then: +Then bring the stack up: ```bash docker compose up -d @@ -89,25 +92,27 @@ docker compose up -d Open **http://localhost:3000**. The first registered user becomes admin. +> Behind a reverse proxy (Caddy / Traefik / Nginx) for TLS, set `NEXT_PUBLIC_APP_URL` and `APP_URL` to your public URL in `.env` before starting. See [Self-Hosting → Reverse Proxy](https://docs.healthlog.dev/self-hosting/reverse-proxy/) for examples. + --- ## Tech Stack -| Layer | Technology | -|-------|-----------| -| Framework | Next.js 16 (App Router, React Server Components) | -| Language | TypeScript (strict mode) | -| Database | PostgreSQL 16 + Prisma 7 (22 models) | -| Job Queue | pg-boss 12 (reminders, insights, backups) | -| UI | shadcn/ui, Tailwind CSS 4, Radix UI, Lucide Icons | -| Charts | Recharts 3 | -| Data Fetching | TanStack Query 5 | -| Forms | React Hook Form 7 + Zod 4 | -| Auth | SimpleWebAuthn 13, Argon2id | -| Notifications | Telegram Bot API, ntfy, Web Push (VAPID) | -| PDF | jsPDF (client-side generation) | -| Testing | Vitest 4 | -| Deployment | Docker (multi-stage Alpine) | +| Layer | Technology | +| ------------- | ------------------------------------------------- | +| Framework | Next.js 16 (App Router, React Server Components) | +| Language | TypeScript (strict mode) | +| Database | PostgreSQL 16 + Prisma 7 (23 models) | +| Job Queue | pg-boss 12 (reminders, insights, backups) | +| UI | shadcn/ui, Tailwind CSS 4, Radix UI, Lucide Icons | +| Charts | Recharts 3 | +| Data Fetching | TanStack Query 5 | +| Forms | React Hook Form 7 + Zod 4 | +| Auth | SimpleWebAuthn 13, Argon2id | +| Notifications | Telegram Bot API, ntfy, Web Push (VAPID) | +| PDF | jsPDF (client-side generation) | +| Testing | Vitest 4 | +| Deployment | Docker (multi-stage Alpine) | --- @@ -130,23 +135,24 @@ HealthLog is designed for people who take data ownership seriously. ### Required -| Variable | Description | -|----------|-------------| -| `DATABASE_URL` | PostgreSQL connection string | -| `SESSION_SECRET` | 64-char hex string for session signing | -| `ENCRYPTION_KEY` | 64-char hex string for AES-256-GCM | -| `API_TOKEN_HMAC_KEY` | 64-char hex string for API token hashing | +| Variable | Description | +| -------------------- | ------------------------------------------------------------- | +| `POSTGRES_PASSWORD` | Password for the bundled Postgres service (Docker Compose) | +| `DATABASE_URL` | PostgreSQL connection string (uses `POSTGRES_PASSWORD` above) | +| `SESSION_SECRET` | 64-char hex string for session signing | +| `ENCRYPTION_KEY` | 64-char hex string for AES-256-GCM | +| `API_TOKEN_HMAC_KEY` | 64-char hex string for API token hashing | ### Optional -| Variable | Description | -|----------|-------------| -| `NEXT_PUBLIC_APP_URL` | Public-facing URL (default: `http://localhost:3000`) | -| `WITHINGS_CLIENT_ID` | Withings OAuth2 client ID | -| `WITHINGS_CLIENT_SECRET` | Withings OAuth2 client secret | -| `WITHINGS_REDIRECT_URI` | OAuth callback URL | -| `WITHINGS_WEBHOOK_SECRET` | Webhook URL hardening secret | -| `TELEGRAM_WEBHOOK_SECRET` | Telegram bot webhook secret | +| Variable | Description | +| ------------------------- | ---------------------------------------------------- | +| `NEXT_PUBLIC_APP_URL` | Public-facing URL (default: `http://localhost:3000`) | +| `WITHINGS_CLIENT_ID` | Withings OAuth2 client ID | +| `WITHINGS_CLIENT_SECRET` | Withings OAuth2 client secret | +| `WITHINGS_REDIRECT_URI` | OAuth callback URL | +| `WITHINGS_WEBHOOK_SECRET` | Webhook URL hardening secret | +| `TELEGRAM_WEBHOOK_SECRET` | Telegram bot webhook secret | Telegram bot token, ntfy settings, Web Push VAPID keys, Umami, and GlitchTip URLs are configured in the **Admin Panel** and stored encrypted in the database. @@ -205,60 +211,60 @@ All mutations require authentication via session cookie. External ingest uses Be
Health Data -| Method | Endpoint | Description | -|--------|----------|-------------| -| `GET` | `/api/measurements` | List measurements (paginated, filterable) | -| `POST` | `/api/measurements` | Create measurement | -| `DELETE` | `/api/measurements/:id` | Delete measurement | -| `GET` | `/api/analytics` | Trend summaries (7d/30d) | -| `GET` | `/api/export` | Export as CSV or JSON | -| `POST` | `/api/import` | Import from JSON | -| `POST` | `/api/doctor-report` | Aggregated data for PDF | +| Method | Endpoint | Description | +| -------- | ----------------------- | ----------------------------------------- | +| `GET` | `/api/measurements` | List measurements (paginated, filterable) | +| `POST` | `/api/measurements` | Create measurement | +| `DELETE` | `/api/measurements/:id` | Delete measurement | +| `GET` | `/api/analytics` | Trend summaries (7d/30d) | +| `GET` | `/api/export` | Export as CSV or JSON | +| `POST` | `/api/import` | Import from JSON | +| `POST` | `/api/doctor-report` | Aggregated data for PDF |
Mood -| Method | Endpoint | Description | -|--------|----------|-------------| -| `GET` | `/api/mood-entries` | List mood entries | -| `POST` | `/api/mood-entries` | Create mood entry | -| `DELETE` | `/api/mood-entries/:id` | Delete mood entry | -| `GET` | `/api/mood/analytics` | Mood trend analytics | -| `POST` | `/api/integrations/moodlog/webhook` | moodLog.app webhook | +| Method | Endpoint | Description | +| -------- | ----------------------------------- | -------------------- | +| `GET` | `/api/mood-entries` | List mood entries | +| `POST` | `/api/mood-entries` | Create mood entry | +| `DELETE` | `/api/mood-entries/:id` | Delete mood entry | +| `GET` | `/api/mood/analytics` | Mood trend analytics | +| `POST` | `/api/integrations/moodlog/webhook` | moodLog.app webhook |
Medications -| Method | Endpoint | Description | -|--------|----------|-------------| -| `GET` | `/api/medications` | List all medications | -| `POST` | `/api/medications` | Create medication | -| `PUT` | `/api/medications/:id` | Update medication | -| `DELETE` | `/api/medications/:id` | Delete medication | -| `POST` | `/api/medications/:id/intake` | Log intake event | -| `GET` | `/api/medications/:id/compliance` | Compliance stats | -| `POST` | `/api/ingest/medication` | External intake (Bearer) | +| Method | Endpoint | Description | +| -------- | --------------------------------- | ------------------------ | +| `GET` | `/api/medications` | List all medications | +| `POST` | `/api/medications` | Create medication | +| `PUT` | `/api/medications/:id` | Update medication | +| `DELETE` | `/api/medications/:id` | Delete medication | +| `POST` | `/api/medications/:id/intake` | Log intake event | +| `GET` | `/api/medications/:id/compliance` | Compliance stats | +| `POST` | `/api/ingest/medication` | External intake (Bearer) |
Auth and Integrations -| Method | Endpoint | Description | -|--------|----------|-------------| -| `POST` | `/api/auth/register` | Create account | -| `POST` | `/api/auth/login` | Password login | -| `POST` | `/api/auth/logout` | Destroy session | -| `GET` | `/api/auth/me` | Current user profile | -| `POST` | `/api/auth/passkey/*` | WebAuthn flows | -| `GET` | `/api/withings/connect` | Initiate Withings OAuth | -| `POST` | `/api/insights/generate` | Regenerate AI insights | -| `GET` | `/api/gamification/achievements` | Achievement progress | -| `GET` | `/api/health` | Docker health check | +| Method | Endpoint | Description | +| ------ | -------------------------------- | ----------------------- | +| `POST` | `/api/auth/register` | Create account | +| `POST` | `/api/auth/login` | Password login | +| `POST` | `/api/auth/logout` | Destroy session | +| `GET` | `/api/auth/me` | Current user profile | +| `POST` | `/api/auth/passkey/*` | WebAuthn flows | +| `GET` | `/api/withings/connect` | Initiate Withings OAuth | +| `POST` | `/api/insights/generate` | Regenerate AI insights | +| `GET` | `/api/gamification/achievements` | Achievement progress | +| `GET` | `/api/health` | Docker health check |
@@ -266,16 +272,16 @@ All mutations require authentication via session cookie. External ingest uses Be ## Integrations -| Integration | Setup | Purpose | -|-------------|-------|---------| -| **Withings** | Env vars | Auto-sync weight, BP, and activity | -| **Telegram** | Admin Panel | Medication reminders with inline buttons | -| **ntfy** | User Settings | Self-hosted push notifications | -| **Web Push** | Admin Panel | Browser-native VAPID notifications | -| **OpenAI** | User Settings | AI health insights (BYOK) | -| **moodLog.app** | User Settings | Mood tracking sync | -| **Umami** | Admin Panel | Privacy-friendly analytics | -| **GlitchTip** | Admin Panel | Sentry-compatible error tracking | +| Integration | Setup | Purpose | +| --------------- | ------------- | ---------------------------------------- | +| **Withings** | Env vars | Auto-sync weight, BP, and activity | +| **Telegram** | Admin Panel | Medication reminders with inline buttons | +| **ntfy** | User Settings | Self-hosted push notifications | +| **Web Push** | Admin Panel | Browser-native VAPID notifications | +| **OpenAI** | User Settings | AI health insights (BYOK) | +| **moodLog.app** | User Settings | Mood tracking sync | +| **Umami** | Admin Panel | Privacy-friendly analytics | +| **GlitchTip** | Admin Panel | Sentry-compatible error tracking | --- diff --git a/docker-compose.yml b/docker-compose.yml index b7f4355d3..c990c15bc 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -4,10 +4,10 @@ services: context: . dockerfile: Dockerfile container_name: healthlog-app - expose: - - "3000" + ports: + - "3000:3000" environment: - DATABASE_URL: "postgresql://healthlog:healthlog@db:5432/healthlog?schema=public" + DATABASE_URL: "postgresql://healthlog:${POSTGRES_PASSWORD:?POSTGRES_PASSWORD is required}@db:5432/healthlog?schema=public" NODE_ENV: production SESSION_SECRET: "${SESSION_SECRET:?SESSION_SECRET is required}" ENCRYPTION_KEY: "${ENCRYPTION_KEY:?ENCRYPTION_KEY is required}" @@ -24,7 +24,7 @@ services: container_name: healthlog-db environment: POSTGRES_USER: healthlog - POSTGRES_PASSWORD: healthlog + POSTGRES_PASSWORD: "${POSTGRES_PASSWORD:?POSTGRES_PASSWORD is required}" POSTGRES_DB: healthlog volumes: - pgdata:/var/lib/postgresql/data