No description
  • Go 51.5%
  • Swift 47.8%
  • Dockerfile 0.4%
  • Shell 0.3%
Find a file
devilreef cd8a7ccef2
All checks were successful
server-ci / build-image (push) Successful in 52s
Disable clinical (Health Records) sync by default
Apple Health Records is only available in a limited set of countries
(US + a handful), and querying HKClinicalType in unsupported regions
can cause HKSampleQuery to hang or requestAuthorization to stall
mid-prompt — which manifests as "Sync now" never completing.

Re-enable by flipping the flag back to true once both
(a) the device is in a supported region AND
(b) the App ID has the Apple-restricted health-records entitlement.
2026-05-12 15:45:52 +06:00
.forgejo/workflows Authenticate registry push with REGISTRY_TOKEN secret 2026-05-11 21:49:30 +06:00
docs/superpowers Document Swift Testing convention in the plan pre-flight 2026-05-12 14:12:57 +06:00
ios Disable clinical (Health Records) sync by default 2026-05-12 15:45:52 +06:00
server Add 'Health Sync — Body' Grafana dashboard backed by Postgres 2026-05-11 22:55:42 +06:00
.gitignore Tidy repo: drop bootstrap docs, add top-level README 2026-05-11 23:09:36 +06:00
CLAUDE.md Add CLAUDE.md with monorepo orientation for future Claude Code sessions 2026-05-11 23:18:53 +06:00
LICENSE Release into the public domain under The Unlicense 2026-05-11 23:16:12 +06:00
README.md Release into the public domain under The Unlicense 2026-05-11 23:16:12 +06:00

health-sync

Self-hosted mirror of all your Apple Health data. An iOS app reads from HealthKit, ships deltas over HTTPS, and a Go server lands them in a Timescale-extended Postgres so you can query and graph your own body history without handing it to anyone else.

Architecture

┌────────────────────┐    HTTPS / gzip JSON     ┌──────────────────────┐
│  HealthSync.app    │  ───────────────────▶   │  health-sync-server  │
│  (iOS 26, SwiftUI) │  ◀───────────────────   │  (Go 1.25)           │
└─────────┬──────────┘    bearer-token auth    └──────────┬───────────┘
          │                                                │
          │ HKAnchoredObjectQuery                          │ pgx
          │ per HK type, persisted anchor                  │
          ▼                                                ▼
   ┌──────────────┐                                ┌──────────────┐
   │  HealthKit   │                                │  Timescale   │
   │   (system)   │                                │  on Postgres │
   └──────────────┘                                └──────────────┘
  • iOS walks every HK sample type, batches deltas, posts to /v1/samples:batch, /v1/workouts:batch, /v1/ecg:batch, /v1/clinical:batch, and /v1/characteristics. Anchors live in a SQLite file in the app sandbox so each sync only sends new rows.
  • Server keys idempotency on (source_uuid, type_id, time) — re-sending a batch ingests zero new rows. A BGAppRefreshTask schedules a sync roughly every hour; a "Sync now" button drives it manually.
  • The HealthKit type catalog (server/internal/types/types.yaml) is the single source of truth for both ends; iOS codegens its catalog from the same YAML at build time.

Repo layout

server/                       Go module — API server + CLI
  cmd/
    health-sync-server/         main HTTP daemon
    health-sync-cli/            mint-code, revoke, stats
    codegen/                    types.yaml → Swift catalog
  internal/
    api/  store/  auth/  metrics/  migrate/  types/
  deploy/
    Dockerfile
    docker-compose.yml          host-loopback proxy variant
    docker-compose.caddy.yml    standalone with auto-TLS
    Caddyfile                   reverse-proxy config
    prometheus.yml              example scrape config
    grafana-dashboard.json      server-ops dashboard (Prometheus)
    grafana-dashboard-body.json body-metrics dashboard (Postgres)
    README.md                   deploy + observability notes

ios/                          xcodegen project — HealthSync.app
  project.yml                   single source of truth for the Xcode project
  HealthSync/Sources/
    App/  Auth/  Network/  HealthKit/  Persistence/  Sync/  Features/
  HealthSync/HealthSync.entitlements
  HealthSyncTests/Sources/
  scripts/codegen.sh            shells out to server/cmd/codegen
  MANUAL_TESTS.md               device-only checklist

.forgejo/workflows/           CI — server image build + push

Server quickstart

Requires Go 1.25 and Docker (the tests spin up real Postgres via testcontainers-go).

cd server
go test ./...                # full test sweep
go build ./cmd/...           # binaries in ./bin or $GOPATH/bin

For local end-to-end against the iOS app:

cd server/deploy
echo -n '<long random password>' > secrets/db_password
printf 'postgres://healthsync:%s@db:5432/healthsync?sslmode=disable' \
  "$(cat secrets/db_password)" > secrets/db_url
docker compose up -d
docker compose run --rm server health-sync-cli mint-code --ttl 15m

Type the resulting 8-char code into the iOS app during onboarding.

For a production VPS deployment (Caddy-fronted, auto-HTTPS) see server/deploy/README.md.

iOS quickstart

Requires Xcode 17+ and xcodegen (brew install xcodegen). The catalog generation step calls go run so Go must be on PATH inside Xcode's build environment too.

cd ios
bash scripts/codegen.sh        # one-time: generate the HK catalog
xcodegen generate              # produce HealthSync.xcodeproj
open HealthSync.xcodeproj

In Xcode, plug in your iPhone, pick it as the destination, hit ⌘R. The bundle is me.devilreef.healthsync; signing is set to team Y29665FRMG. HealthKit must be enabled on that App ID in Apple's developer portal (Xcode "Automatic Signing" will register it on first build).

CLI test sweep against the simulator:

cd ios
xcodegen generate
xcodebuild -scheme HealthSync \
  -destination 'platform=iOS Simulator,name=iPhone 17' test

Observability

Metrics scrape at :9090/metrics on the server. Prometheus config and two importable Grafana dashboards (one for server ops, one for body metrics out of Postgres) live in server/deploy/. See server/deploy/README.md for the read-only Postgres role recipe Grafana should use.

License

Released into the public domain under The Unlicense. See LICENSE for the full text. Do whatever you want with it.