Skip to main content

Backend handoff: Live Hub, what is open after your answer

Audience: backend developer (enode_backend_v1). Status: sent, and answered by the backend on 2026-10-07, twice: the four points asked for next are built (backend commit af446f6e, on top of 13b7a267), and so is item 7, the ruling and ruled-out reps (5cae8750). backdev runs all three. The answer is under Agreed with the backend, which holds where it differs from the body, and what was seen on backdev is under Observed on backdev after delivery. What is left on the server side waits for decisions and for numbers from production; it is listed at the end of that section.

Thank you for 13b7a267. Eight of the points of the first document are built in a day, and nearly every question has an answer. Your response is written into that document's section "Agreed with the backend". This one is what comes next:

  • what we read out of your answer, so that you can correct us (part 1),
  • two points your answers opened, and one we missed (part 2),
  • the next steps of the wall, with the answers you were waiting for (part 3),
  • the release (part 4).

The items keep their names. The questions go on from the first document's last number, so that every question has one number: this document asks 40 to 49.

The rule stays: the server only adds, so that an app build from today keeps working. S4 below is the one place where a request that is accepted today would be refused; it says why no build of ours sends such a request.

In one page​

WhatWho moves
Nowbackdev runs 13b7a267Whoever rebuilds backdev. We then run the six checks of live test 82
NowThe count for R2 and the list for R3, from the production databaseYou, or whoever may read that database
NextS4: a set names only people of the session's ownerYou. Small
NextItem 2, the set while it is liftedYou. Everything you asked is answered
NextItem 8, the result for those who took part, by "has a set in it"You. Small
After a yes of the product ownerItem 7, ruled-out reps and the ruling. Our answer to "a field or the tag" is in part 3The product owner, then you
After a look at productionItem 4, best beforeYou
After decisions D3 and D9Item 6, the paired display, and S3The product owner, then you
After a week on productionS1, step 2You

What each statement rests on​

BasisMeaning
Reported by youFrom your response of 2026-10-07 to the first document
Observed on backdevA request was sent on 2026-10-07 and the answer read
As read in the clientRead in enode-tracking, working tree of 2026-10-07

Part 1 · What we read out of your answer​

backdev does not run it yet​

Observed on backdev, 2026-10-07 at 13:15: one check per built point that a client can see, in tests/live/offline/82-live-open-contract.live.test.ts. All six answered as a server without 13b7a267 does.

PointCheckAnswer on backdev at 13:15
S2The keys of user on reduced, complete, the single set and a created frameThe whole profile
Item 3POST with completedAt 90 s before sentAt, then the set's completedAtThe set has none
Item 5GET /workout_live_session_sets/board/:id404
Item 1Heartbeat with one row in platforms, then the device rowThe row has no platforms
P4The same heartbeat four times, 20 s apart, with the set topic openNo live_session.device event
P1The set topic from a linked phone that is signed in as an athlete403

The label of a device (83a811f8) is on backdev since about 13:00 that day, and the app reads it. So the rebuild that brought it was made before 13b7a267. When backdev has the commit, the six checks are run again and their results go into the first document, under "Observed on backdev after delivery". P2 and S1 cannot be seen by a client.

The eight points, and what the clients do with each​

PointWhat the clients doState on our side
S2Nothing. Both read id and name of a live set's user and no moreDone by you alone
Item 3The tracking app sends completedAt and sentAt with every send from its queue. The display adds a set that was completed more than half a minute before it arrived without a line and without a glow, and orders lifts by completedAtTo build (work block X1)
Item 1The tracking app sends platforms in its heartbeat, per station. The display shows who is up before the liftTo build (X1). Behind a switch of the session until the product owner has said what the wall may show
Item 5The display reads board when it opens and after a reconnect, and the single set for a bar path that did not come with a frameTo build (X1). Against a server that answers 404 it reads complete, as today
P4See belowThe stand-in is built
P1The tablet follows the set topic, reads once and applies each pushed set. It closes the stream when device_session.live_session_changed says it is outBuilt on 2026-10-07; it switches on by itself for a device you admit
P2Nothing—
S1Nothing—

Four remarks​

P4 · We will count 90 seconds, not 60. Your events come every 20 to 40 seconds for a device that sends a heartbeat every 20. If one heartbeat is lost on the way, two events can be 60 seconds apart, and a rule "silent after 60 seconds" would name a tablet that is fine. So a viewer that relies on the events calls a device silent after 90 seconds. Nothing to change on your side. Today the clients do not rely on the events yet: since 2026-10-07 they read GET …/devices again every 30 seconds while a session is on screen, and that read can go once P4 is on production.

Item 1 and item 2 · Kept in memory. Fine for a restart: a device row has no platforms for at most 20 seconds, and a running set lives for a minute anyway. What we cannot see from outside is question 40.

P1 · A stream that stays open. Fine for now: the app closes it. A phone that the coach removed keeps receiving the session's sets until then, and only its own app makes it stop. Question 41.

Item 5 · No ETag, no running yet. Both fine. running is asked for with item 2 below.

Questions.

  1. How many server processes answer requests on production, and on backdev? platforms is kept in memory, and running sets will be. With more than one process: does the devices read on another process see what a heartbeat brought to the first, and does "the same body publishes nothing" compare against the right state?
  2. What would it cost to end an open stream on the set topic when its device is unlinked?

Part 2 · Two points your answers opened, and one we missed​

S4 · A set names only people of the session's owner​

From your answer 13: "A set POST does not check its userID against the owner today."

What is asked. POST /workout_live_session_sets and PUT …/tracked/:id refuse a set whose userID the session's owner has no access to, by the rule you built for platforms: the owner and the users the owner has access to. Nothing is stored and nothing is published.

Why. A device that is linked to a session can put any user of the system on that session's board by naming their id: the set comes back with that person's name and photo. Until S2 it came back with their e-mail address, date of birth and body weight as well. The id of a user is not easy to guess, and it is not a secret either: it stands in addresses of the portal.

An app from today. It sends the athletes of the training on that device. On a coach's tablet those are the coach's athletes. On an athlete's own phone it is the athlete, who is under the session's owner, or the phone could not have joined. So no build of ours sends a set this rule refuses. If a set is refused all the same, the app's queue drops an operation that is answered with a 4xx other than 401, 408 and 429, and does not send it again.

Checked on backdev by.

RequestExpected
A set POST from a linked tablet with the userID of an athlete of another organisation403 or 404. reduced does not have the set, no frame went out
The same with an athlete of the session's owner201, as today
A set POST from an athlete's own phone, with the athlete's own id201, as today

Questions.

  1. 403 or 404? And does any client you know of send sets for users outside the owner's list, for example the native iOS app?

S3, one thing more · Body weight in the state event​

Observed on backdev, 2026-10-07: POST /competition_dashboard/join needs no credentials. The state event its token receives names the athlete with id, name and bodyweight. S2 took a person's body weight out of every live set; this is the one place left where it reaches a reader who is not signed in.

What is asked. No bodyweight in a state event for a token that join handed out without credentials. If the standalone app shows it, say so, and it becomes part of decision D9.

Questions.

  1. Does anything read bodyweight from the state event?

R3 · The values a repair needs leave with a deleted session​

From your answer 4: ending a session keeps its live rows; deleting a session removes the rows that no saved workout has claimed. So every session that a coach deletes takes away what a repair of older workouts would be made from.

What is asked, now and without a decision: one read on the production database. How many of the sets that lost values (at least 17 without a set value and 176 without rep values, by your snapshot to 2026-09-03) belong to a live session that still exists and still holds the values? With that number the product owner decides whether a repair is worth a job, and how soon.

Questions.

  1. What is that number today, and can the list of the workouts be kept, so that a later job does not have to find them again?

Part 3 · The wall: the next steps​

The shapes are the ones of the first document. This part only says what is settled since, and what changes.

Item 2 · The set while it is lifted​

Everything you asked is answered, by you: kept in memory apart from the measurement tables (15), currentPersistentSetID as its key (16), one request per repetition (17). Please build it as the first document describes it, with one addition that waited for it:

  • GET /workout_live_session_sets/board/:id?running=true returns the running sets under running. Without the flag the key is absent, as today.

After a restart of the server a running set is gone until its next repetition. That is fine: the app sends the whole set so far with every repetition, so the next one brings it back.

Checked on backdev by the table of the first document, item 2.

Item 8 · The result for those who took part​

Your answer 33: "has a set in it" can be checked without storing anything new; "linked at the end" cannot. We take the first.

What is asked, restated. For 24 hours after endedAt, GET /workout_live_session_sets/reduced/:id and board also answer a device that sent a set into that session. The event device_session.live_session_changed that tells a device its session is over carries endedWorkoutLiveSessionID once.

A tablet that was in the session and sent nothing gets no result. That is acceptable: it has nobody on the board.

Checked on backdev by.

RequestExpected
End a session in which a tablet sent a set; the tablet reads reduced200, the sets of the session
The same from a tablet that was linked and sent nothing404
The same 25 hours after the end404
The event on the tablet's user stream at the endNo session in it, as today, and endedWorkoutLiveSessionID

Questions.

  1. Is "sent a set" known per device, or per user? If per user: a coach's tablets all carry the coach's login, so every one of them would read the result, which is fine too. Say which it is, so that the app asks from the right device.

Item 7 · Ruled-out reps, and the ruling​

7a is agreed as two lists (answer 28). You call it dearer than it looks, because it touches the rep matching of L0. It can come after 7b: a competition needs the ruling first.

7b · Our answer to "a field or the tag" (question 30): a field. A ruling has three states: good lift, no lift, and not ruled yet. The display shows all three, and "not ruled yet" is what an attempt is between the lift and the judge's tap. One tag can say "no lift" and cannot tell a good lift from one nobody has ruled. So:

  • ruling on the set: good, noLift, or absent. One column.
  • While the standalone board lives, keep its no-lift tag in step: a set whose ruling is noLift carries the tag, any other does not. Then both boards say the same about a lift (your question 32), and the tag can go with the standalone app.
  • No new route for the owner. PUT …/external/:id reaches every set of the owner's session (your answer 31). It takes ruling in its body: a value sets it, an explicit null takes it back, an absent field leaves it. The route PUT …/:id/ruling of the first document is dropped.
  • The device sends ruling with its create and with PUT …/tracked/:id, as proposed.

This step waits for the product owner's yes to the ruling as such; see the table below.

Questions.

  1. Is "an explicit null takes it back" possible on PUT …/external/:id, where a null means "unchanged" elsewhere in this API? If not: a third value, notRuled.

Item 4 · Best before today​

Agreed as a read (answer 27). It waits for your look at personal_bests on production (questions 25 and 26 of the first document): whether the table is filled, and whether a record is on the scale of the live measurement with the same metric. One thing to add from our side: the portal is being changed to keep an athlete's bests in a session's stored result when the session ends (work block W14, in work on 2026-10-07), so that a result does not change when the athlete sets a new record. best_before is still what a display without a login needs, and what makes a result right for a session that ended with nobody watching.

Item 6 and S3 · The paired display, and the open way into a board​

Both wait: item 6 for decisions D3 and D9, S3 for an answer to question 10, which neither of us can give from the code. See the table below.

S1 · Step 2​

After a week of step 1 on production: the count of requests whose header and token named different devices, by app version. If it is zero for the builds that matter, the set routes take the device from the token.

Questions.

  1. Can the log of step 1 be read by app version without a deploy, so that the week's count is one query?

Part 4 · The release​

Agreed: one release of L0, L2, 83a811f8 and 13b7a267; no switch; the way back is the previous build, because the migrations only add (answers 1, 2).

What stands before it:

WhatState
R2 · Sessions past their end date that are still used. The count has to come from the production database (answer 3)Open. Without it the deploy may end sessions that coaches use every week
S2 with L2.Solved by 13b7a267 being part of the release
backdev runs the release as a whole, and live tests 15, 16, 25, 40, 80 and 82 pass on itOpen. 80 is the tracking app of release tag v3.0.7 against the server of today; it passed on 2026-10-07 without 13b7a267

After the deploy we check with a real account: the set topic answers 200, and an idle stream stays open for ten minutes. Both are described in docs/live-hub-release.md.

Questions.

  1. Who runs the count for R2 on production, and when?
  2. Is there anything in 13b7a267 that an app build from today could notice, other than the shorter user of S2? Live test 80 will be run against it; say what else it should send.

Waiting for the product owner​

Open pointWho can answerWhat waits for it
Does the standalone dashboard app write state with its display token? (question 10 of the first document)Whoever builds or knows that app. Neither the backend nor the clients can tell from their codeS3: closing the write for a token from the open join
D3 · The paired display. D9 · Fold the standalone app in, or keep bothDecided on 2026-10-07: yes to the paired display, and the standalone app is folded into it. See "What is left"Nothing any more
D5 · The ruling belongs to the attempt: one question to the judge on the tablet, the coach may rule from the live viewThe product owner. Proposed on 2026-10-07, not confirmedItem 7b
What the wall may show about a person: who is about to lift with which load, the reps of a set in progress, a best from before the sessionThe product owner (decision D17 of the concept)Showing items 1 and 2 in a gym. They can be built and seen in the demo before
R2 · What happens to sessions past their end date. Proposed: clear the date where a set arrived after it in the last 30 daysThe product owner, after the countThe release
R3 · Whether workouts that lost values are repairedThe product owner, after question 44A job
The native iOS app's device id: does it send the id it signed in with? (question 7)Whoever builds that appS1, step 2
When backdev is rebuilt with 13b7a267Whoever rebuilds backdevOur six checks, and work block X1 against a real server

All questions, by number​

Questions 1 to 39 are in the first document, with their answers.

No.WhereQuestion in short
40Part 1More than one server process, and state that is kept in memory
41Part 1Ending an open stream when its device is unlinked
42S4403 or 404; clients that send sets for users outside the owner's list
43S3Who reads bodyweight from the state event
44R3How many lost values can still be repaired, and the list
45Item 8"Sent a set": per device or per user
46Item 7bTaking a ruling back on PUT …/external/:id
47S1Reading the log of step 1 by app version
48ReleaseWho counts for R2, and when
49ReleaseWhat else an app from today could notice in 13b7a267

Acceptance, for every step​

ScenarioExpected
An app build from today, unchanged (live test 80)Every request answers as before
The contract tests of L0, L2 and the result (15, 16, 25, 40)Pass unchanged
The checks of 13b7a267 (82-live-open-contract), once backdev has itPass
Finish a training whose sets were live, with running sets and ruled-out reps among themThe saved workout has all its measurements

Agreed with the backend​

From the backend's response of 2026-10-07 ("Backend response 2"). Where a point here and the body disagree, this section holds.

One correction to the first answer​

Answer 33 of the first document was too short. A live set stores who lifted (userID), not the device that sent it and not who was signed in. So "has a set in it" is known per user who lifted, not per device. Item 8 is built on that.

Built​

Four points, committed on bugfix/loading_factor_api_technique as af446f6e, on top of 13b7a267. No database change. 503 backend tests green.

PointWhat is builtDiffers from what was asked
S4POST /workout_live_session_sets and PUT …/tracked/:id answer 403 for a userID that is neither the session's owner nor a user the owner has access to. Nothing is stored or published. PUT …/running/:id follows the same rule403, not 404
Item 2PUT …/running/:currentPersistentSetID: 200 when taken, 204 for a device in no running session, dataPackages ignored. Event live_session_set.running with the payload of the first document; a measurement carries metricID, value and the metric's key. The same set again publishes nothing. DELETE …/running/:id answers 204 and publishes live_session_set.running_ended. GET …/board/:id?running=true returns the running sets under running, newest first; without the flag the key is absent. In no other read or frame, not on /competition_dashboard, nothing in the database. A running set may be sent in the lobbyA device ends only a running set it sent itself; for another device's set the delete answers 204 and nothing happens. After ten minutes without a request the end is noticed by the check that runs once a minute, so running_ended comes ten to eleven minutes after the last request
Item 8For 24 hours after the end, a user who lifted in the session reads its sets from any device they are signed in on: reduced, board, complete and the single set. device_session.live_session_changed carries endedWorkoutLiveSessionID when the device was released because its session is over (ended, expired, idled out)By the user who lifted, not by the device. Not when the device left or was removed, and not when the session was deleted
S3, body weightThe state event of /competition_dashboard carries no bodyweightFor every reader: the hub sends one payload per topic

What "per user" means for item 8:

DeviceReads the result
An athlete's own phone, the athlete liftedYes
A coach's tablet, signed in as the owner or someone with access to the ownerYes, as before, without a time limit
An athlete's own phone, linked, the athlete did not liftNo (404)
A tablet signed in as an athlete, on which only other athletes liftedNo (404)

For contract tests: a disposable athlete has to be a child of the session's owner. An athlete account with no relation to the owner had its sets stored until now and gets 403 from this build on.

The answers, by question number​

No.Answer
40One process answers requests on production; a second one runs jobs and answers none (from a note on the deployment of 2026-09-23, not checked again). backdev is one process. With one process the devices read and "the same body publishes nothing" see the right state. With more than one they would not: events would still reach every viewer, but the devices read and board?running=true would answer from the process that got the request
41Not estimated yet; it needs a look at how the hub holds its streams
42403. No client known to the backend sends sets for users outside the owner's list. For the native iOS app that is not known
43Nothing on the server reads bodyweight. For the standalone app: not known
44Not counted. It needs the production database. The list can be written to a file by the same read
45Per user who lifted. The app asks with its own login, from any device
46A third value, notRuled. An explicit null and an absent field are not told apart anywhere else in this API
47The log line carries the app version. Whether a week's count is one query depends on where production keeps its logs; not looked at
48With the product owner
49In 13b7a267: the state event of /competition_dashboard arrives a moment after the answer of a set request instead of before it, and a device that sends heartbeats causes more live_session.device events. In af446f6e: the 403 of S4 and the missing bodyweight. No other answer to a request of today changes

What the answer changes on our side​

  • Item 7b: the ruling has three request values: good, noLift and notRuled, which takes a ruling back. A set that was never ruled, or whose ruling was taken back, has no ruling in an answer.
  • Item 8: the result on a device is asked for with the login of whoever is signed in. A coach's tablet reads it as it reads any of the coach's sessions. An athlete's own phone reads it when the athlete lifted.
  • Item 2: when another athlete is selected on a station, the tablet ends the running set itself; no other device can.
  • Answer 40: platforms and running sets are right as long as one process answers requests. A second process would need them in a shared place first. Worth a line wherever the deployment is written down.

Response 4: item 7 is built, and two decisions​

From the backend's response 4 of 2026-10-07. It names a response 3 that is not recorded in this document.

Two decisions of the product owner, as the backend reports them:

  • D5: yes to the ruling as such.
  • R3: no repair. The workouts that lost values before the L0 fix stay as they are. No job, no list. Question 44 is closed with it.

Built: items 7a and 7b, committed as 5cae8750 on top of af446f6e. One migration: a column ruling on the live sets and a column valid on the live reps. 505 backend tests green.

PointWhat is builtDiffers from this document
7b, in answersruling on the set is good or noLift, and absent when the set is not ruled: on complete, reduced, the single set, board and in every frameNo
7b, in requestsgood, noLift or notRuled, which takes a ruling back. A body without the field leaves the ruling as it is. The device sends it on POST and on PUT …/tracked/:idNo
7b, whoever runs the sessionPUT /workout_live_session_sets/:id/ruling with the ruling in its body, where :id is the live set's id. For the owner and users with access to the owner: 200 and the set. For anyone else 404. It changes nothing else of the setThe route of its own is back. PUT …/external/:id does not take ruling: that route sets the set's user to whoever sends the request and replaces the set's tags, so a coach who ruled through it would become the lifter. And 404, not 403, for someone without access
7b, the eventEither change goes out as live_session_set.updated, or .lobby_updated for a lobby set, also when the ruling is the one the set hadNo
7b, the standalone boardThe no-lift tag is kept in step: a set ruled noLift carries it, any other does not, and a device update that sends other tags does not drop itNo. A client cannot see it: no set read returns a set's tags
7a, in requestsPOST, PUT …/tracked/:id and PUT …/running/:id take workoutLiveSessionRuledOutReps, with reps of the same shapeNo
7a, in answersworkoutLiveSessionReps holds the counted reps, workoutLiveSessionRuledOutReps the others, absent when there are none: on complete, the single set, board, the frames and a running set. Every rep says valid, in both listsNo
7a, one row per repThe two lists are matched as one by currentPersistentRepID: a rep that is ruled out or in again stays the same rep with the same id and keeps its package, also when the body sends noneNo
7a, the body is the whole setA rep in neither list is gone. A body with neither list leaves the reps. A body with the counted reps and without the second list has no ruled-out reps: that is what an app from today sends, and reps ruled out earlier are then removedSaid in so many words for the first time

An app from today sends neither. Its sets have no ruling and no second list. New in every answer for it: valid: true on each rep.

What it changes on our side: the coach's view rules through PUT …/:id/ruling with the live set's id, not through PUT …/external/:id. A refusal there is a 404.

Observed on backdev after delivery​

2026-10-07, 13:45 to 13:55: backdev runs 13b7a267 and af446f6e, and every point a client can see holds. Through raw requests with disposable accounts: an owner with a managed athlete, the owner signed in a second time as a tablet, an athlete with a login of their own on a phone, and a second owner who is a stranger to the first.

npx vitest run --config tests/live/vitest.config.ts \
tests/live/offline/82-live-open-contract tests/live/offline/83-live-open-2-contract
PointObserved
S2user has exactly hasImage, id and name on reduced, complete, the single set and in a created frame
Item 3A set sent with completedAt 90 s before sentAt has a completedAt 90 001.7 ms before its createdAt. The same with both an hour ahead. Without the two fields the set has none. A later PUT with another value does not move it
Item 5board answers 200 with now and sets. A rep has no dataPackages and says hasDataPackages: true; a measurement's metric holds key only; no running key without the flag. The single set still returns the package
Item 1A heartbeat with one row in platforms comes back on the device row with station, user (slim), load, plannedReps and next.user; a live_session.device frame carries it; an empty list is stored as an empty list
P4The same heartbeat four times, 20 s apart: live_session.device at seconds 20, 41 and 81. So 20 to 40 s between two signs of life
P1An athlete's phone: 403 on the set topic before it joined, 200 after. A set sent then reaches it, with the slim user
S4A set for a user of another organisation: 403 on POST, on PUT …/tracked and on PUT …/running, and it is not in reduced. The owner's athlete and the owner: 201
Item 2200 with no reps, then with one; a running frame each, with the payload as written (slim user, the metric's key, deviceSessionID, running: true). The same body again: no frame. Not in reduced, not in complete, no running key on a plain board; under running with the flag. The finished set under the same id: 201, a created frame, and the running one is gone. A delete: 204 and running_ended. From a device in no session: 204
Item 8The athlete's phone gets device_session.live_session_changed with endedWorkoutLiveSessionID. After the end it reads reduced and board of the session it lifted in (200), and gets 404 for a session it was only linked to
S3A display that joined /competition_dashboard without credentials gets a state event that names the selected athlete and holds no bodyweight

Nothing older broke. Against the same build, at 13:51: the contract tests of L0, L2 and the result (15, 16, 25, 40) and the requests of the tracking app of release tag v3.0.7 (80) pass unchanged, eleven tests in all. So the 403 of S4 and the shorter user of S2 refuse or change nothing those clients send or read.

Not tried: the end of a running set after ten minutes, a delete of another device's running set, the 24-hour limit of item 8, a tablet that is signed in as an athlete, and anything with a real device.

2026-10-07, 14:25: backdev runs 5cae8750 as well, and item 7 holds.

npx vitest run --config tests/live/vitest.config.ts tests/live/offline/84-live-ruling-contract
PointObserved
7bA set sent with ruling: "noLift" says so on reduced, complete, board and in its created frame. A device update without the field leaves it; one with good changes it. PUT …/:id/ruling by the owner: 200 with noLift, 200 with notRuled, after which the set has no ruling; at least one updated frame each; the set's reps and its load are untouched. The same request from an owner of another organisation: 404, and the set is unchanged. A set that was never ruled has no ruling key
7aTwo counted reps and one ruled out, each with a package: complete has two in the first list with valid: true and one in the second with valid: false, each with its own package. board has both lists, without packages. An edit that sends no packages and moves a rep into the second list: the same rep id, valid: false, its package kept. Moved back: the same id, valid: true, its package kept. A body with the counted reps and no second list: the second list is gone from the answer

Not seen, because no read returns a set's tags: that the no-lift tag follows the ruling. Not tried: a lobby set, a running set with ruled-out reps, and the standalone board.

Nothing older broke with 5cae8750 either. Against the same build, at 14:35: the contract tests of L0, L2 and the result (15, 16, 25, 40), the requests of the tracking app of release tag v3.0.7 (80), and the checks of the two earlier commits (82, 83) pass: 21 tests in seven files. So valid: true on each rep is all an app from today sees of it.

Observed on backdev, 2026-10-08: item 6, item 4 and the write of S3 answer​

No response of the backend about these points has reached us; this is what backdev answered to raw requests on 2026-10-08 at about 10:25, with disposable accounts (an owner with a managed athlete, the owner as a tablet, an athlete on a phone, a stranger). Since 11:40 of that day the rows of item 6, item 4 and S3 are held by a contract test, tests/live/offline/100-live-display-contract (eleven checks, green twice in a row). What it found beyond the table is in "What a display cannot read yet" below.

PointObserved
Item 6, pairingPOST /live_displays/pairing without credentials: 200 with pairingCode, pairingSecret, expiresAt, interval. GET /live_displays/pairing/:secret: { "state": "waiting" }; after the owner confirmed, once paired with displayToken and liveSessionID, then 404
Item 6, the ownerPOST /workout_live_sessions/:id/displays with the code and a label: 200 with displayID, label, pairedAt, lastSeenAt. A wrong code: 404. After ten wrong codes the next request is 429, and so is the right code; a code confirmed in between does not start the count again. A session takes several displays; a session that has ended takes none (404). The owner's devices read has a list displays; there is no GET …/displays. DELETE …/displays/:displayID: 204
Item 6, what the token readsNot the owner's routes: with the token, the session, a board, a set POST, /history/v2/records, a user read and the realtime topic answer 401. The display has routes of its own, with the header display-token: GET /live_displays/session (id, name, state, config, joinCode, hasLogo, logoMimeType, createdAt, startedAt, the owner as slim user), …/session/logo (id, logoData, logoMimeType), …/devices (devices, displays, now), …/board (also ?running=true), …/sets/:id, …/best_before, …/metrics, …/text_contents, …/data_package_types, …/exercise_definitions/:id, and the stream GET /live_displays/events. PUT /live_displays/presence: 200, and lastSeenAt of the display moves
Item 6, a setA set on …/board names its user by id, name, hasImage and carries exercise with a name. A set posted afterwards arrives on …/events as live_session_set.created
Item 6, the endAfter the owner ended the session the token still reads session (state: "ended", endedAt) and board. After the owner removed the display its stream ended and session and presence answer 401. A wrong token: 401
Item 6, the old boardPOST /competition_dashboard/join without credentials: 200, as before
Item 4GET /workout_live_sessions/:id/best_before as the owner: 200 with now and pairs; the athlete with a set has records: []. With ?userID=…&exerciseDefinitionID=…: that pair. A stranger: 404
S3, the writePUT /competition_dashboard/:id/state without credentials: 403. With a display token of the open join: 403. With the owner's JWT: 200

Not there on that day:

PointObserved
Ending an open stream on unlink (question 41)A phone joined, opened the set topic (200) and left (204). Its open stream was not ended and a set posted afterwards still arrived on it. A new stream of that phone: 403
S1, step 2A set POST with the token of a linked tablet and the device-id of a device in no session: 404, and the set is not stored. So the set routes do not go by the token's device

Not tried or not seen: a best_before for an athlete with a history or one who only stands in platforms, a pairing that expires, the token a day after the end.

Seen since, by the contract test: the logo is at /live_displays/session/logo (/live_displays/logo answers 404). In the display's devices a row carries sensorConnected, platforms (each with its user as a person), athletes (people, where the owner's read has athleteIDs) and lobbySetAt after a heartbeat that sent them; it never carries joinedAt, joinedBy, appVersion or sharingAcknowledgedAt. So the "Before" scene can be built on it. The stream …/events carries live_session.updated, live_session.device, live_session_set.created, .updated, .deleted, .running, .lobby_created and a heartbeat, under the names of the realtime topic. A created frame carries the reps' sensor packages. The pairing routes and the display's routes answer without the app's api-key, and a browser may send display-token from another origin.

What a display cannot read yet​

Found on 2026-10-08 while the display's reader was built (packages/core/src/live-session/display-feed.ts). In the order of what it costs a gym display. None of it has been sent to the backend yet.

PointObservedWhat it costsAsked for
The session frame on the display's stream tells too muchlive_session.updated on /live_displays/events is the owner's frame: user with email, role, tags, assignedTags, availability, gender, and participatingDevices with every device's deviceId, userID and device session id. The display's own read of the session names the owner by id, name and hasImageA television without a login is sent the owner's email and the device ids of the gym's tablets with every change of the session. The client drops them before any code sees them, but they are on the wire. The same kind of point as S2The frame in the shape of GET /live_displays/session
An athlete's photoNo route. Tried /live_displays/users/:id/image, …/users/profileImage/:id, …/profile_images/:id, …/user_images/:id, …/images/:id: 404 each. /users/profileImage/:id with the token: 401. A set says hasImageA paired display shows initials where a display under a coach's login shows photos. The product owner asked for the photos back on 2026-10-08 (work block F3)GET /live_displays/users/:id/image for a person who has a set in the session or stands in a platforms row, in the shape of /users/profileImage/:id
A session that has ended takes no displayPOST …/displays on an ended session: 404, as for a wrong code. A display that was paired before the end keeps reading session and boardA result cannot be put on a second screen afterwards, and a television that lost its token after the end cannot be paired againPairing for an ended session, for as long as the owner can read it
The display is among the session's devices for the ownerAfter pairing, participatingDevices of the owner's session carries the display as a device session of type api whose deviceName is the label and whose deviceId starts with display:. The lobby's devices read does not list itEvery count of a session's devices in the portal that goes by participatingDevices counts a television as a training deviceLeave displays out of participatingDevices; they have displays
Catalogues a display has no route forThere is metrics, text_contents, data_package_types and exercise_definitions/:id. There is no read of the metric categories, the normative base categories, the exercise bases or their display groups. exercise_definitions/:id names its base by id onlyThe client's metric store counts as loaded only with the categories. Whether an exercise is a weightlifting lift (the bar path layout) goes by the exercise base's display groupmetric_categories beside metrics, and on exercise_definitions/:id the base's display group; or say which existing read a display should use
The same row in two shapesGET /live_displays/devices says athletes (people); the live_session.device frame for the same row says athleteIDs and no athletes, and carries joinedAt, joinedBy and sharingAcknowledgedAt, which the read leaves outA display that learns of a device from the frame has ids it cannot turn into namesThe frame in the shape of the read
Where a display keeps how it is setPUT /live_displays/presence takes a body and answers 200; a display's row has displayID, label, pairedAt and lastSeenAt onlyThe profile of a screen (desk, television, projector) can be kept in the television's browser only. The owner cannot see or set it from the portalA small free field on the display that presence writes and the owner's displays list returns. Not urgent

Not a gap, to know: a display has no read of all sets with their sensor packages. It reads the board and, for a bar path, one set at a time; the reader does that for the three newest sets and gets the packages of every later set from its frame.

A set does not say which device sent it​

Found on 2026-10-08, for the Stream's filter by device. Not sent yet.

A stored live set carries userID and no device (the correction to answer 33 above). A running set does carry deviceSessionID. The portal's Stream filters by device all the same, by the athletes a device names in its heartbeat, which is wrong for an athlete who changed tablets and empty for a device without a heartbeat.

Asked for: deviceSessionID on a stored set, taken from the device session of the request that created it, and returned on reduced, board, complete, the single-set read and the created / updated frames. Absent for a set stored before the field. An update by another device or by the owner does not change it.

Answered on our side afterwards: the standalone app (questions 10 and 43)​

Looked up on 2026-10-07 in the app's own repository (vmaxpro-server/webdev/enode-competition-dashboard, a Next.js app; its last commit is of 2026-05-13).

  • Question 10: no, it never writes state. The app makes four requests and no other: POST …/join, GET …/:displayToken/state, GET …/:displayToken/events and POST …/:displayToken/leave (lib/dashboardApi.ts). The portal does not call PUT …/state either (reply 1, question 1), and the native iOS app only calls switch_athlete. So no client we know of writes state, and S3 can be closed as the first document asks: PUT /competition_dashboard/:id/state needs the owner's JWT, or goes.
  • Question 43: it does not show body weight. Its type knows an optional bodyweight and its demo data has one; no component reads it. It shows the athlete's name, club, weight class and group. Leaving bodyweight out breaks nothing there.
  • The repository holds no deployment set-up, and its local configuration points at backdev. A copy runs against production all the same: https://comp.enode.ai, hosted on Vercel; the bundle it serves names https://api.enode.ai (looked at on 2026-10-07). The product owner does not think anyone still uses it; that is not confirmed. Question 23 of the first document (how often join was called on production, and when last) would confirm it. Until then the open join and the reads of state and events stay on production as they are. The write of S3 can be closed regardless.

What is left on the server side, and what each point waits for​

State after response 4 and the decisions of 2026-10-07. Two points can be started now: the write of S3 and item 6.

PointWaits for
S3, the write: PUT …/state only with the owner's JWTNothing. No client writes state; see "Answered on our side afterwards"
The release of L0, L2, 83a811f8, 13b7a267, af446f6e and 5cae8750backdev runs it as a whole, which it does since 14:25, and our tests pass on it; see above. R2, the sessions past their end date, is not named as open in response 4 any more; what settled it is not recorded here
Item 4, best beforeA look at personal_bests on production
Item 6, the paired displayNothing. D3 and D9 are decided (2026-10-07): yes to the paired display, as a page of the portal, in the same release as the rest; the standalone app is folded in and not updated. Questions 20 to 22 and 24 of the first document are open with it
The open join, and state, events and leave of /competition_dashboardThe portal's display page reading with the pairing token on production, the portal's fallback gone, and comp.enode.ai pointed at that page (the address stays, the app of May behind it goes). Then they can go. Not before
The server's own bar path and peaks for the old boardA word of the product owner: retired with the old board, or still served. Only the old board reads them
S1, step 2A week of step 1 on production
Ending an open stream when its device is unlinked (question 41)Our word, which is: yes, when it is cheap, and not before the release. A phone that was removed from a session should stop receiving its sets without having to cooperate. Proposed, not confirmed by the product owner
R3, the repairClosed: decided against on 2026-10-07