All docs
Project
Changelog
Source: CHANGELOG.md on GitHub · edit this page
What changed in each release of the server stack — the four images
ghcr.io/pulshealth/{ingest,api,mcp,web}, which share one version that
PULS_VERSION in .env selects. Upgrading is: bump it, bring the checkout
to the same release (git pull, or git checkout vX.Y.Z — the compose file
and the schema migrations come from it, not from the images), then
make pull up.
Two things are versioned separately and are not in this file:
- The iOS app, which ships on its own schedule through the App Store. Its
record is
docs/appstore/README.md§ Release record. - The Puls Sync Protocol, whose
schemaVersion(andX-Puls-Protocolheader) moves only for a change a v1 receiver would reject. A server release that adds an optional field or a new read endpoint keeps the protocol number where it is;docs/protocol/README.mdis the contract.
The format follows Keep a Changelog, and releases are semver over the stack: a major for a change that needs operator action (a breaking config or schema change), a minor for features, a patch for fixes. While the stack is on 0.x that promise is weaker by convention — a minor may carry a change that needs operator action, and when it does this file says so at the top of the entry.
Unreleased#
Nothing since 0.2.0.
[0.2.0] - 2026-09-18#
Upgrading: move the checkout to v0.2.0 (git pull, or
git checkout v0.2.0), set PULS_VERSION=0.2.0 (or track latest), then
make pull up; migrate applies 014_device_tokens.sql and re-runs
099_read_roles.sh. No .env changes are required — the shared
PULS_TOKEN keeps working exactly as before, and PULS_MULTI_USER defaults
to off. The checkout step is not optional: 0.2.0's ingest records
device_token_id on every batch, a column only 014 adds, so the new
images on a 0.1.0 checkout answer every upload with a 500 (the app keeps its
anchors and retries, so nothing is lost, but nothing syncs either).
Added#
- Per-device tokens (
make devices ARGS='issue --user <uuid> --name <label>',list,rename,revoke; SRV-8). Each is stored only as its SHA-256, bound to one user, revocable on its own and stamped with its last use. A request that presents one acts as that user:X-User-IDmay be absent or equal, anything else is 403 before the body is read. Everybatchesrow now records which device wrote it (device_token_id, NULL for the shared token) and the per-batch log line carriestoken_id. Migration014_device_tokens.sql. - Per-request user scoping on the product API (SRV-11, the API side;
the web viewer's switcher and the MCP server's
userargument below ride on it). Every/v1route takes an optionaluser=<uuid>query parameter; absent, the request is answered forPULS_USER_IDexactly as before.GET /v1/userslists the users the deployment answers for — name, e-mail,createdAt,lastSync,batches,uploadedSamplesfrom thebatcheslog — plusdefaultandmultiUser./openapi.jsondescribes the parameter on every scoped operation. PULS_MULTI_USER(.env, defaultfalse) decides whetheruser=may name anyone but the default. Off, another user is 403multi-user reads are disabled, never a quiet answer for the default user; a value that is not a UUID is 400; neither charges the auth-failure limiter. Turning it on means the one staticPULS_API_TOKEN— the tokendocs/ai.mdsays to hand to a ChatGPT Action — reads every user on the server, so it stays off until you want that.puls-export --user <uuid>(default$PULS_USER_ID, else none) picks whose data to export, and a 403 is explained the way a 401 is.- web: a user switcher when the database holds more than one user;
?user=<uuid>picks one (SRV-11). - MCP:
list_users, and auserargument on every tool;PULS_USER_IDpins an instance to one person (PULS_MCP_USER_IDfor the Compose service). Needs the product API'suserparameter and/v1/users(SRV-11). GET /v1/summary?range=7d|14d|30d|90don the product API: the last N calendar days as one short markdown page (activity, heart, sleep, workouts, body, coverage) for pasting into a chat that has no MCP connection;format=jsonfor the numbers. The MCP server exposes it asget_summary(AI-6).
Changed#
PULS_TOKENis optional.PULS_ALLOW_SHARED_TOKEN(defaulttrue) turns the shared token off once every phone has its own; emptyPULS_TOKENdoes the same. Ingest logs its auth mode at startup and warns when nothing at all could authenticate.scripts/bootstrap.shandmake pairingaccept that mode instead of dying on an empty token.- A device-token lookup that fails because the database is unreachable is
503
authentication unavailable, never 401, and is not charged to the auth-failure limiter; neither is a 403 user mismatch. - The
grafanarole loses SELECT ondevice_tokens(revoked by099_read_roles.shon every run). - The
api_readerrole gains SELECT onbatches(for/v1/users; the table holds no credential). No operator action:099_read_roles.shre-runs on the nextdocker compose up -d. - web: Next.js 16.3.5 (from 16.3.4).
[0.1.0] - 2026-09-14#
The first tagged release, and the one that first publishes
ghcr.io/pulshealth/{ingest,api,mcp,web} — before it, a compose install had
nothing to pull and had to build from the checkout. Everything below shipped
together; there is no earlier release to diff against.
Added#
- The Puls Sync Protocol, v1. One gzipped NDJSON
POSTplus optional read endpoints, specified indocs/protocol/with JSON Schemas, a fixture corpus with expected outcomes, an offline schema checker (tools/protocol-check) and a Python reference receiver. The receiver's smoke test doubles as a conformance runner against any implementation:smoke_test.py --url <url> --token <token>. - The reference backend. Go ingest and product API over PostgreSQL 17 / TimescaleDB, with Grafana dashboards for the data and for ingest health. Ingest is idempotent per sample UUID, upserts aggregates and activity rings, and connects as a scoped DML-only role rather than the superuser.
- Schema migrations that apply themselves. The
migrateservice runs before every app service ondocker compose up -d, records each file inschema_migrationswith a checksum, refuses an edited or missing applied file, and requires an explicitbaselinefor a database that predates it. - A one-command quickstart.
scripts/bootstrap.shgenerates the secrets, starts the stack and prints the pairing QR;makewraps the rest.--lantrades TLS for a phone on the same Wi-Fi, on request only. - Read-only MCP server (
server/mcp), 11 tools over the product API in stdio and streamable-HTTP modes, so Claude Desktop, Claude Code, Cursor and ChatGPT can answer questions from the data. It never touches Postgres. - Export.
GET /v1/exportstreams CSV or JSONL;tools/puls-exportis a dependency-free CLI over it. - Product API endpoints an analyst asks for first: daily sleep, bounded raw samples, workout series, state of mind, activity rings, daily metrics that resolve the iPhone/Watch double-count.
- One type vocabulary.
docs/protocol/catalog.jsonis rendered from the SwiftHealthTypeCatalog, and the web catalog is generated from the JSON, so the two published lists cannot drift. - Self-hosting safety rails. Auth-failure rate limiting on both ingest and
the product API (failed attempts only, never successful ones), a
/healthzon each that answers from a two-second cache rather than the pool, an optional password on the web viewer, loopback binds by default, and an opt-in backup service with a documented restore drill. - Open-source hygiene. Apache-2.0 with
NOTICEandTRADEMARK.md,SECURITY.md,CONTRIBUTING.mdwith DCO sign-off, a code of conduct, issue and PR templates,AGENTS.md,llms.txt, and a CI gate that fails on owner-specific content in tracked files.