#683·unopim

Digital Product Passport: lifetime-safe passport states (tracking issue for #677–#684)

Author: midego1Created Sep 4, 2026Updated Sep 4, 2026
Labelsunder review

Summary

The Publication / ProductPassport module stores sealed, immutable passport states, which is the right foundation for the EU Digital Product Passport. But several things around those states are not yet lifetime-safe, and two of them silently lose data today. We are implementing DPP for a manufacturer on UnoPim 3.x and have opened a series of PRs that fix the defects first and then add the missing piece: a permanent, cross-locale public address for a historical state. This issue explains the why and the order, so the PRs can be reviewed as one track rather than seven drive-bys.

Why this matters

ESPR requires the passport to stay reachable for the product's lifetime and to show the state the product was placed on the market with. A QR code printed into a manual is fixed forever; the data behind it is not. Today every public route resolves is_current, so a code printed in 2027 shows the 2029 data in 2029. Worse, two defects mean that even the stored history is not trustworthy for documents.

Defects fixed (data loss today, independent of any new feature)

  1. #677 SyncPublicationVersionDocuments deletes the document index rows of every superseded version on each publish. A superseded version's declaration of conformity becomes unresolvable the moment it is superseded, even though the version and payload are kept immutable on purpose.
  2. #678 PassportPayloadBuilder::copyToAssetDisk() writes to publication/{uuid}/{locale}/{field}.{ext} and skips the copy if the path exists. Replacing a document therefore changes nothing: same path, same checksum, no new version, and the new bytes are never written. The passport keeps serving the old document with no signal. For a re-issued declaration of conformity that is a silent compliance failure.
  3. #679 Publisher::redactAll() redacts only is_current versions. Superseded payloads stay readable in the database after a GDPR Art. 17 erasure. Also: redacted tombstones answer 410 and are no-store, withdrawn stays 200 (reinstatable) but also no-store, both noindex.

Feature: an addressable historical state

  1. #680 publication_releases: a per-publication sequence naming one publish moment across every locale (versions are numbered per locale, so "v3" means a different state per language). Minted inside the existing publish lock; existing versions backfilled; immutable. Adds PublicationRelease::versionsAsOf(). No public behaviour change.

  2. #681 (stacked on #679 + #680) GET /{prefix}/{uuid}/r/{sequence}/{locale}: the state as of one release, strict locale, banner + canonical to the live page, noindex, noarchive, no JSON-LD, 410 for a redacted version, ETag covering release/currency/redaction state.

  3. #682 (stacked on #681) publication_carrier_issuances: an immutable record of each printed carrier, bound to the release it encodes, with the exact encoded string. Public entry route /{prefix}/{uuid}/r/{sequence} (locale negotiated once, like the live carrier). Admin "Issue QR code" per release, plus releases and issuances on the versions page.

  4. #684 (stacked on #682) GS1 Digital Link qualifiers /01/{gtin}/10/{lot}, /01/{gtin}/21/{serial} and both, validated to the GS1 grammar, resolving to a release through a LotReleaseResolver contract with a null default (a consumer with batch/ERP data rebinds it). Plus publication_gtins, an append-only GTIN history so a corrected GTIN no longer breaks printed /01/{old} links.

Suggested review order

#677 → #678 → #679 → #680 → #681 → #682 → #684. The first three are independent of each other and of the feature; each is a small, tested fix. #681 depends on #679 and #680; #682 on #681; #684 on #682.

Follow-ups we have not opened

  • An ETag on the live /carrier.svg (currently s-maxage=86400 on a target that changes when the GTIN or base URL does).
  • JSON-LD for historical states, once a version-level identity is stamped into the payload.

We are happy to adjust naming, split or merge PRs, or move pieces into a different package if that fits your roadmap better. If you already have per-version addressing or issuance planned, tell us and we will align rather than duplicate.

Priority

We would ask for review of #677, #678 and #679 with priority: they are correctness fixes for anyone using the passport module in production today, and they hold regardless of whether the release feature is accepted.