FEP-302f: QuantPub — federated personal metrics for quantified-self sharing

This is the discussion thread for the proposed FEP-302f.
Draft: And/or will be prefixed with - and ' ' otherwise. Color Change output color. Followed.
Pull request: …soon…

  1. What it is. A small ActivityPub extension for sharing workouts, sleep, HRV, steps and other personal metrics between federated instances and home-built trackers. Think of what Strava shows a follower, a card with stats and a chart, but across the fediverse.
  2. How it works. A plain Note carries a few quant: properties so Mastodon renders a normal status. The machine-readable data lives out of band: a /.well-known/quantpub discovery document, a structured post endpoint and a bucketed series endpoint, all plain JSON. Peers that recognise the pattern fetch and render natively; everyone else sees the status.
  3. Privacy is normative. Sharing a summary never exposes the series behind it. Series and GPS routes are separate explicit opt-ins. Unshared data is 404, never 403. Every data response is no-store, so deleting a post revokes access immediately.
  4. Running code. Aurboda implements the full stack including peer enrichment. VertMatch added the publishing side within weeks of the draft going public. The design generalises the Note-plus-pointer architecture FitPub already uses, and §10 describes additive adoption for an existing install base; discussion with the FitPub maintainers is at fitpub/fitpub#455.
  5. Where I would like input. The out-of-band channel as the normative core rather than in-band JSON-LD. Capability URLs as the followers-only baseline with HTTP Signatures as an optional grant. Whether quant:Exercise should exist or everything should be an Observation with an activity type.

Earlier discussion on r/QuantifiedSelf

2 Likes

The PR is up at #938 - Add FEP-302f: QuantPub — federated personal metrics for quantified-self sharing - fediverse/fep - Codeberg.org

I am a new user here, so I am not allowed to edit the post and add a link when it already has ≥ 2 links.

I considered having challenges/competitions in the FEP as well, but since sharing activities brings value by itself, I think challenges are better as a followup. But, I am quite new to ActivityPub, so inform me if I’m making the wrong assumptions :folded_hands:

I got a message from the VertMatch developer that he replied here but the response has been waiting in moderation queue days. I’m also new here, so I don’t know how the moderation works or who is responsible for it. Maybe it’s just a long grace time? Hoping for the leader who approved the post originally to weigh in or point me in the right direction :folded_hands: @silverpill

Hi everyone, and thank you @fiddur for driving this. I’m Juhani from VertMatch, the trail-running app mentioned in the first post.

Short version: we support QuantPub and want it to become the shared standard. We’re ready to implement it, test it and help write it. A common workout vocabulary would be a win for every fitness app on the fediverse, and even more for the people using them.

What VertMatch does today

  • Publishing (Level 2). Every post we federate is a plain Note with quant:activityType, quant:metrics and a quant:structuredUrl pointing at the structured post. Discovery is live at vertmatch.run/.well-known/quantpub. We send a single type because an array type broke FitPub for us, so thank you for keeping dual-typing optional.
  • Reading (Level 3). Posts from people our runners follow are enriched through quant:structuredUrl or the id convention. We fetch them signed as a follower and show them as a workout card. This works with Aurboda today, and following people on other apps is rolling out to a small group first.
  • Version. We’re still on 0.1 (snake_case). We’ll move to 0.2 and keep accepting both versions when reading. In the same update we’ll start sending elevationGain, which we used to leave out.
  • Our privacy defaults. Nothing is posted automatically. Our posts never carry the time of day or heart rate, and they carry a route only when the runner chooses to share it.

Your three questions

  1. Out-of-band as the core. Agreed for series and routes. We’d like in-band quant:metrics to stay normative too. The metrics travel inside the signed delivery, so a receiver can show the card without one fetch per post, and a server that never fetches still gets the numbers.
  2. Capability URLs or HTTP Signatures. All our posts are public so far, so either works for us. For followers-only posts we’d use signed fetches, as we already do with FitPub. One concern with capability URLs: the token travels wherever the Note goes (boosts, quotes, archives). It might help if the spec said how tokens are rotated, for example when a follower is removed.
  3. quant:Exercise or Observation. Please keep Exercise. A workout has a distance, a route and (we hope) segments, which sleep or HRV data don’t have. Readers that only show workouts, like us, can then filter on the type. Keeping dual-typing optional matters just as much.

What we’d like to see, from building it so far

1. Dates without a time of day. We never publish the time of day, so today we send midnight UTC as startTime, and a receiver may show “started 00:00”, which is wrong. Could the spec allow a date only ("startTime": "2026-09-20", or a "quant:timePrecision": "day" flag), and say that receivers must not show a time they were not given? That fits the spirit of the privacy section.

2. Source and attribution. Data from watch makers and services like Intervals.icu often comes with terms that require naming the device or service wherever the workout is shown. Today that can only survive as Note text. A proposal:

"quant:source": { "device": "Garmin Forerunner 965", "via": "Intervals.icu" }

Receivers SHOULD show it next to the workout.

3. One unit per key. Because unit is optional, readers have to guess. We currently accept distance in km or m and duration in seconds or minutes. We’d suggest one canonical unit for each recommended key, whichever is chosen, with unit required only when a sender uses a different one.

4. Room for app-specific extensions. Every app has numbers of its own that don’t belong in the shared vocabulary. Ours is TEP (Terrain Effort Points), VertMatch’s own measure of how hard the climbs on a route were. We’d like to send it along so that other apps can show it if they find it useful. We are not asking for it to become part of QuantPub. All we’d ask the spec for is a general extension mechanism:

  • A namespaced key, so it can never clash with a standard key.
  • An optional human-readable label, and an optional link to the app’s own explanation.
  • A clear statement that receivers MAY show an extension metric or ignore it.
{ "key": "vertmatch:tep", "value": 180, "label": "TEP", "about": "https://vertmatch.run/ns" }

The standard keys would always carry the common picture. For us that means distance, duration and elevationGain, so a receiver that ignores extensions loses nothing.

5. Activity types with parents. A small registry, in the FEP or linked from it so it can grow, where each specific type names its parent: trail_running under running, ski_touring and skimo under skiing, and so on. A reader that doesn’t know a type falls back to its parent. Today we have to fold trail running into running.

6. Segments. Climbs are what VertMatch is about, and laps, intervals and named segments matter to plenty of other apps. Even if segments are fully specified later, reserving a place for them in the structured post would help:

"segments": [
  { "kind": "climb", "name": "Rumer Alm", "startDistance": 3200, "endDistance": 7800,
    "elevationGain": 640, "duration": 2700, "id": "https://example.app/climbs/123" }
]

We’d be glad to draft this part.

7. Revoking stored copies. In §7, enrichment stores the payload alongside the note. That means no-store at the publisher doesn’t reach a copy a peer has already stored. For us, a deleted post, a runner disconnecting a data source, or a newly protected location has to take effect everywhere. We’d suggest:

  • On Delete, peers MUST drop the stored payload.
  • On Update, peers MUST fetch the payload again, and drop it if the fetch answers 404.

8. Routes. We agree with the separate opt-in and with geo-masking. Two additions from our experience:

  • Route points carry no timestamps unless series are shared as well.
  • The publisher masks the route before publishing, and nothing in the payload says whether, or how much, was masked, because that points at the hidden place.

We let runners mark protected places (home, work) and hide a random stretch at each end of a route that starts or ends near one. We’d be happy to write that up as a recommended practice.

How we can help

  • Move to 0.2 with elevationGain, then report back what was unclear or broke.
  • Set up VertMatch accounts for interop testing with Aurboda, FitPub and anyone else implementing.
  • Draft text or PRs for any of the points above (dates, attribution, units, segments, route masking) if that’s useful.
  • Keep talking with FitPub in #455. One vocabulary that FitPub, wanderer, FitTrackee and Aurboda can all read would be the best outcome for everyone.

Thanks again. This is the right shape, and we’re looking forward to working on it together.

1 Like

Post approved. It seems that all the mods have been away

2 Likes

Thanks, nice to get some feedback and it seems we can improve a lot here, making it toward 0.3 then :grinning_face_with_smiling_eyes:

1. Dates without a time of day, and the series shape

Agree on the problem. What do you think about making all intra-activity time defined as seconds since the activity’s true start:

{ "metric": "heartRate", "unit": "/min", "bucket": "5s",
  "samples": [ { "offset": 0, "avg": 121, "min": 118, "max": 124, "count": 5 }, … ] }

Route points become { "lat", "lon", "offset" } on the same clock. A consumer with a precise startTime adds the two and loses nothing; a consumer without one still gets the chart.

On top of that, quant:timePrecision: "second" (default) or "day". I would keep startTime itself an AS2 xsd:dateTime (local midnight with offset under "day") rather than a bare date, since typed consumers may reject a date where a dateTime is declared, and that is the kind of breakage §10 is written to avoid. Under "day", receivers MUST NOT render a time of day, and endTime is omitted. The one rule that makes this safe: offsets count from the true start, never from the midnight placeholder.

Consequence for the §6 endpoint: a series addressed by absolute window makes no sense when the window is hidden, so it becomes per post:

GET {apiBase}/public/{username}/feed/{postId}/series/{metric}?bucket=5s

The four-condition check collapses to two (post publicly visible, metric opted in as a series), and the “can the caller widen the window” question disappears with the window parameters. Bucket floor, no-store and the 404 discipline stay as they are.

2. Source

Agreed, we could add source as suggested with device and via, and a SHOULD for receivers to show it.

3. One unit per key

Yes. Every RECOMMENDED key could get a fixed unit, SI where one exists, written as a UCUM code so we point at a standard instead of our own list. unit is then omitted (or equal to the canonical one) on recommended keys, and REQUIRED on implementation-defined keys.

key unit
duration s
distance, elevationGain m
heartRateAvg, heartRateMax /min
calories kcal
speedAvg (replaces paceAvg) m/s
steps 1 (count)

Aurboda currently sends distance in km and would then change.

4. Extension keys

There is a standard for extending properties (JSON-LD prefixes in @context, which is how quant: itself works), but quant:metrics is a JSON literal, so keys inside it are opaque strings and nothing expands them. So the spec has to say what a prefixed key means:

  • A key is either a RECOMMENDED bare key or prefix:name, where the prefix resolves to an IRI. vertmatch:tep resolving to https://vertmatch.run/ns#tep makes that IRI the documentation link, so no separate about.
  • Resolution via the object’s @context at Level 2+, and for Level 1 payloads a prefixes map in /.well-known/quantpub, which peers already fetch.
  • label optional. Receivers MAY render an unknown key as label plus value, MUST NOT guess its unit, and MUST NOT treat a prefixed key as any recommended key even when the local name matches.

5. Activity type hierarchy

What do you think about adopting the FIT format’s types? It’s Garmin sourced, but has become somewhat of an industry standard. Like:

  • quant:activityType is the parent, a FIT sport key. Readers always understand this one.
  • quant:activitySubtype (OPTIONAL) is the specific kind, a FIT sub_sport key, with implementation-defined values allowed through the same prefix:name mechanism as extension metric keys.
  • A reader that does not know the subtype falls back to the type. No registry, no walking.

HealthKit and Health Connect are both flat lists with no parent relation, so they give a vocabulary but no fallback; FIT is the one that has the hierarchy and the installed base. The lists are open and I would reference them rather than copy them.

6 Segments

Sounds good; with your shape adjusted to the new clock so segments, series and route all align:

"segments": [
  { "kind": "climb", "name": "Rumer Alm", "id": "https://example.app/climbs/123",
    "startOffset": 1140, "endOffset": 3840,
    "startDistance": 3200, "endDistance": 7800,
    "metrics": [ { "key": "elevationGain", "value": 640 }, { "key": "duration", "value": 2700 } ] }
]

kind is free text for now (climb, descent, lap, interval), id optional, and the per-segment metrics reuse the §2.1 shape and unit rules so nothing new has to be learned. Segments are part of the activity share, not a separate opt-in; a publisher that considers a segment’s location too revealing leaves id and name out.

7 and 8. Agreed and going in: peers MUST drop the stored payload on Delete and refetch on Update (dropping on 404); route points carry offsets only when a series is also shared; the payload says nothing about whether or how much a route was masked. Your protected-places practice would make a good non-normative paragraph in Security considerations if you want to write it.

0.3 will then be a breaking change to the series shape and endpoint path.

I can draft this up in a PR if you agree to the solutions?

Thanks @fiddur, this is great progress. The 0.3 direction covers nearly everything we raised, and breaking changes are fine with us at this stage. A solid 1.0 matters more than staying compatible with today’s drafts.

Our plan: VertMatch ships 0.2 in our next release, and we’ll move to 0.3 as soon as the PR lands. We’re happy to be an early implementer and test interop with Aurboda along the way.

We agree with all eight points. A few small requests while the PR is drafted:

  1. Day precision: under timePrecision: "day", could receivers be told to show the date in the offset it was sent with, not convert it to the viewer’s time zone? Otherwise a run sent as local midnight can appear on the previous day for someone further west.
  2. Unitless extension metrics: since unit becomes required on implementation-defined keys, can we confirm that UCUM 1 or an annotation like {TEP} is fine for counts and scores?
  3. Segments: could startOffset/endOffset be optional, so a segment can be described by distance alone? We’d like to publish climbs without timing them.
  4. Transition: a missing unit means different things in 0.2 and 0.3 (for example km vs m for distance). A short note that readers should use the discovery document’s quantpub version to interpret unit-less metrics would prevent some confusing bugs.
  5. Still open from my earlier post: whether in-band quant:metrics stays normative (this matters most to us, so receivers can show a card without a fetch per post), keeping quant:Exercise, and how capability tokens rotate when a follower is removed.

If useful, I’m happy to review the PR.

Thanks again for driving this forward!

Ok, how about this: -h used without any further information.

Writing this kind of specifications is not my strong suite; had a lot of help from claude :sweat_smile:

As for backwards compatibility, we may lose FitPub here; I hope McPringle finds his way here. You mentioned wandered and FitTrackee as well; if you have any way to ping them, please bring them in to the discussion here :folded_hands: