When reference data becomes product logic

The last 24 hours in Nima Project were tightly concentrated in world-cup-porra, world-cup-api and the root documentation:

  • 2026-06-11 17:02 +0200 - Reject favorite teams as surprise predictions
  • 2026-06-11 17:02 +0200 - Refresh competition settings during active pool sync
  • 2026-06-11 20:42 +0200 - Fix competition catalog kickoff import
  • 2026-06-11 20:42 +0200 - Fix World Cup kickoff lock time
  • 2026-06-11 20:54 +0200 - Hotfix tournament start lock
  • 2026-06-11 20:59 +0200 - Fix surprise team favorite filtering
  • 2026-06-12 06:51 +0200 - Fix World Cup match kickoff schedule
  • 2026-06-12 08:34 +0200 - Add FIFA startup sync loader
  • 2026-06-12 08:56 +0200 - Show loader while opening pools
  • 2026-06-12 09:11 +0200 - Document FIFA endpoint reference
  • 2026-06-12 09:14 +0200 - Document localized FIFA match centre URL
  • 2026-06-12 09:52 +0200 - Document FIFA match detail endpoints
  • 2026-06-12 09:56 +0200 - Sync FIFA match bonus details

The quick reading would be: tournament data was cleaned up and loading got nicer.

The correct reading is different: the moment that data starts blocking votes, triggering synchronization and recalculating bonuses, it stops being supporting content and becomes product logic.

The problem was not "showing data better", it was deciding behavior

The chat that opened this stretch was very specific:

admin-configured favorite teams should not be selectable as the "surprise team", and if a user already had one saved it had to be invalidated

That sounds like a small form rule.

It was not, because it touched three layers at once:

Layer Decision Real effect
Admin surpriseTeamFavorites.teams becomes the exclusion list the panel does not just configure content; it defines constraints
API if the user tries to save a favorite as the surprise team, that value is cleared persistence stops accepting states that are no longer valid
Frontend the selector must hide those options even if the payload arrives in different shapes the UI cannot depend on one field name once the contract already has history

The smallest frontend diff says everything: team-lists.js had to accept source.teams too.

One extra field looks trivial. It is not when the real system mixes:

  • legacy payloads,
  • normalized payloads,
  • admin-provided values,
  • and a UI that must turn all of that into one valid choice list.

The excluded favorite was not an edge case; it was a product boundary

world-cup-api made the important move.

It did not merely "validate on save". It did something stronger: read the persisted favorites for the edition and, if surpriseTeam was inside that list, blank it during general-prediction normalization.

That changes the nature of the rule:

flowchart LR admin["Admin saves favorites"] --> edition["competition_editions.payload"] edition --> api["API normalizes general prediction"] api --> ui["Frontend synchronizes state"] ui --> vote["User can only vote valid options"]

This is no longer about a nicer dropdown.

It is a product boundary where truth lives outside the browser and the app has to obey it even if it changes in the middle of the tournament.

The second lesson was harsher: official time also governs behavior

The other half of the work was even more revealing.

There were three consecutive commits around the same pain:

  • Fix competition catalog kickoff import
  • Fix World Cup kickoff lock time
  • Hotfix tournament start lock

That is not accidental. It is the pattern of a system discovering that it had been treating kickoff times as lightweight metadata when they were actually business policy.

Because in a prediction app kickoff times do not only help render cards:

  • they close game creation,
  • they lock predictions,
  • they order the calendar,
  • and they determine whether an action is still valid or already too late.

The later conversation made that even more explicit: the startsAt, localTime and timeZone values for all 104 World Cup matches had to be corrected, checked against official FIFA data, and redeployed.

The lesson is not "some hours were wrong".

The lesson is this:

Important

Once a date decides whether a user is still allowed to act, that date is no longer content. It is logic.

The loader was not cosmetic; it was an admission of real dependency

The morning commits on June 12 look like UX work:

  • Add FIFA startup sync loader
  • Show loader while opening pools

But their real meaning is different.

If the app now has to:

  1. validate access,
  2. synchronize with FIFA from the Worker,
  3. update official results through the admin API,
  4. and only then open the pool,

then showing a loader is not polish. It is system honesty.

Before that, the app could pretend local state was enough to render something quickly.

Now it acknowledges a stronger runtime dependency: without sufficiently fresh official data, the interface does not yet know the real tournament state.

That matters because it makes one architectural truth visible:

  • the frontend no longer boots from compiled assets alone,
  • the Worker is no longer only a proxy,
  • and FIFA stops being a source "to look at" and becomes a source that conditions startup.

The last step was inevitable: once scores flow in, bonuses follow

The end of the stretch was almost automatic:

  • first the FIFA endpoint reference was documented,
  • then the localized match-centre URL,
  • then the detail endpoints,
  • and finally Sync FIFA match bonus details arrived.

That was not documentation for its own sake.

The sequence shows the right discipline:

Step What happened Why it mattered
1 lock the endpoint reference avoid integrations built from memory
2 identify the correct localized route separate public schedule data from actual match detail
3 document the detail endpoints leave traceability for the next iteration
4 consume live/football/{IdMatch} complete scorer and redCard from official data

The final conversation was especially precise:

  • the sync only assigns scorer when there is a unique top scorer,
  • if scorers are tied it stores scorerTie: true and awards no unfair bonus,
  • redCard is derived from Bookings[].Card === 2,
  • and if a match was already finished with the right score but without detail, it is republished to complete those fields.

It is hard to ask for a clearer proof that official data is now part of the product mechanic.

It does not decorate the pool.

It scores it.

The central idea: reference and runtime can no longer be separated

These 24 hours made one transition very clear:

Before Now
World Cup favorites as admin configuration World Cup favorites as a rule that invalidates votes
kickoff time as catalog data kickoff time as an operational and scoring boundary
FIFA sync as a nice improvement FIFA sync as part of startup
match detail as extra information match detail as a source of bonuses and correction

That transition often goes unnoticed in small projects because everything keeps being called "data".

But not all data carries the same weight.

The moment a piece of data:

  • enables or blocks actions,
  • clears already-saved state,
  • decides whether the screen can open,
  • or recalculates points,

it is no longer just reference. It is behavior.

What I am taking from this stretch

  • An admin-manageable favorites list can become a hard validation rule.
  • Official kickoff times do not just paint the calendar; they govern closures, locks and system ordering.
  • A well-placed loader can be an architectural statement: "we are not coherent yet until synchronization finishes".
  • Documenting endpoints before stretching the integration reduces improvisation once data starts affecting real scoring.
  • In a tournament app, score, scorer and red card are not analytics. They are product engine inputs.

These were not 24 hours of isolated features.

They were 24 hours in which world-cup-porra made something clearer: a prediction product does not live on forms and screens alone. It lives on the quality of the external truths it chooses to obey.

And once those truths block, correct and score, we are no longer integrating content. We are programming product rules with official data.


Part of my product and platform notes. Follow the blog or contact me.