Migrating from 3.x to 4.0.0
This update rewrites every stored document to align with the new UNTP v0.7 link variant model. The migration is idempotent and reversible only via your backup. Once you have completed step 7 (start the v4 service) and the service has begun accepting new registrations, rolling back to v3 requires restoring from the backup.
Before proceeding, take a full snapshot of your object storage bucket. Use whatever mechanism your storage provider supports (a local filesystem copy for self-hosted MinIO, an S3 versioning snapshot or aws s3 sync to a separate bucket, a GCS object versioning toggle, an Azure Blob soft delete, etc.). Keep the snapshot until you have confirmed the upgrade is stable.
This guide covers what changed between 3.x and 4.0.0, the data migration step you must run between the two versions, and the new behaviour you can expect after the upgrade.
What changed
v4.0.0 aligns the IDR with UNTP Identity Resolver v0.7. The biggest change is to the link variant data model.
Composite key reduced from 5-tuple to 4-tuple
Before v4, each link variant was uniquely identified by (targetUrl, linkType, mimeType, ianaLanguage, context). The scalar ianaLanguage field has been retired in favour of a hreflang: string[] array on each variant, so the composite key is now (targetUrl, linkType, mimeType, context).
A single variant can now advertise multiple BCP 47 language tags via its hreflang[] array, matching the UNTP LinksetSchema target shape.
Resolver matches against hreflang[] membership
The resolver's language-aware cascade was rebuilt against hreflang[] membership. The client's Accept-Language tags are matched (case-insensitively per RFC 4647 §2.1) against each variant's hreflang[] array. BCP 47 lookup fallback (e.g. matching en-GB against a variant tagged en) is not yet implemented and is tracked in pyx-industries/pyx-identity-resolver#117.
The retired fields are:
ianaLanguageon each link variant.defaultIanaLanguageboolean flag on each link variant.- The
?ianaLanguagequery parameter onGET /resolver/links(re-added as?hreflang).
defaultContext is now a pure fallback
defaultContext: true no longer participates in the language-aware front tiers of the cascade. It is consulted only when the requested language fails to match any variant. The publisher's "canonical default variant for the link type" semantics are preserved.
defaultMimeType scope is unchanged
defaultMimeType is still scoped per (linkType + context) at registration time. Resolution does not consult context at request time (no request-side context signal), so the cascade returns the first hreflang-matching variant whose flag is set. See the How It Works precedence table for the full cascade.
Additive variant fields and MIME loosening
These are additive and do not require any upgrade action; publishers can opt in to them once on v4.
public: booleanon each variant. Indicates that the URL itself is safe to publish in a public directory. Distinct fromaccessRoleandencryptionMethod, which govern who may retrieve or decrypt the resource. A variant may bepublic: truewhile still requiring an authorised role to fetch the content. An explicitpublic: falseround-trips and is preserved separately from "not set".rel: string[]on each variant. Carries additional link relation types qualifying the variant beyond its primarylinkType. The reserved valuepredecessor-versionis silently stripped from publisher input; the server emits it itself on predecessor entries derived from version history.- MIME type loosening. v3 rejected
mimeTypevalues outside a curated list; v4 accepts any RFC 6838 well-formed media type, including custom and vendor-prefixed types such asapplication/vnd.acme.sbom+json. Registrations that previously failed with a400on uncommon MIME types now succeed.
The full per-variant field reference lives in the Developer Guide.
Before you upgrade
1. Stop the v3 service
Stop traffic against the resolver before taking the backup. The migration assumes the bucket is not being mutated while it runs.
2. Back up your object storage bucket
Use whatever backup mechanism your storage provider offers. The migration is destructive in the sense that it overwrites each migrated document in place; without a backup, there is no rollback path.
Keep the backup until you have confirmed the v4 service is healthy.
3. Pull the v4 container image
docker pull pyx-identity-resolver:4.0.0
4. Update RESOLVER_DOMAIN and remove API_BASE_URL
In v4, RESOLVER_DOMAIN is the externally-reachable base URL only (scheme + host, no path, no trailing slash). The service appends the API path prefix (/api/v4) internally based on apiVersion in version.json. Drop the /api/3.0.0 segment from your existing value:
# Before (v3)
RESOLVER_DOMAIN=https://resolver.example.com/api/3.0.0
# After (v4)
RESOLVER_DOMAIN=https://resolver.example.com
If you previously set API_BASE_URL (for Swagger external-server resolution), remove it. RESOLVER_DOMAIN now serves both roles.
LINK_TYPE_VOC_DOMAIN is not an env var in v4. The link-type vocabulary URL is derived from RESOLVER_DOMAIN + the API path prefix + /voc, and can be overridden per identifier by setting namespaceURI on the identifier record.
The migration uses these values to rebuild every document's pre-computed linkset with the new URL shape.
Run the migration
5. Dry-run first
Run the migration in dry-run mode to see what would change without writing anything:
docker run --rm --env-file .env pyx-identity-resolver:4.0.0 \
yarn migrate:v4 --dry-run --verbose
The command prints one line per scanned document, e.g.:
[would-migrate] gs1/01/09359502000041: 3 variants → 1 (merged 2); 2 version entries rewritten
[noop] gs1/01/09359502000042: already in v4 shape
Followed by a summary:
Scanned: 124
Migrated: 117 (dry-run)
No-ops: 7
Failed: 0
Review the output. Pay attention to any [would-migrate] line where the input/output variant counts differ — that indicates a merge. The merge rule is first-registered wins (by createdAt): the oldest variant's linkId, title, and other metadata become the merged record's; the discarded variants' ianaLanguage values are unioned into the merged record's hreflang[].
If you have variants that share the new 4-tuple but disagree on other metadata (e.g. different title strings, divergent accessRole arrays), the merge will silently keep the first-registered variant's values and discard the rest. Either reconcile the divergence manually before running the real migration, or accept the first-registered-wins outcome.
6. Run the real migration
When you are satisfied with the dry-run output, run without --dry-run:
docker run --rm --env-file .env pyx-identity-resolver:4.0.0 \
yarn migrate:v4
The migration aborts on the first per-document failure by default so the failure is not buried in a long log. Pass --continue-on-error to migrate as many documents as possible and review the failures at the end.
To re-run on a single document by id (useful when iterating on a failure):
docker run --rm --env-file .env pyx-identity-resolver:4.0.0 \
yarn migrate:v4 --only gs1/01/09359502000041
The migration is idempotent: re-running on a document that is already in v4 shape is a no-op. If the migration fails partway through (process killed, object storage timeout, etc.), re-run from the start; already-migrated documents are skipped.
7. Start the v4 service
Once the migration reports Failed: 0, start the v4 service against the migrated bucket.
8. Recovery if anything goes wrong
If the migration produces unexpected results or the v4 service does not behave as expected, restore your object storage bucket from the backup taken in step 2 and re-deploy the v3 service. Then investigate the migration output and file an issue.
After the upgrade
listLinks query: ianaLanguage is now hreflang
The GET /resolver/links?ianaLanguage=<tag> query parameter has been replaced with ?hreflang=<tag>. Both accept a single BCP 47 tag. The filter returns responses whose hreflang[] array contains the supplied tag.
# v3
curl "https://resolver.example.com/api/3.0.0/resolver/links?namespace=gs1&identificationKeyType=gtin&identificationKey=12345678901234&ianaLanguage=en"
# v4
curl "https://resolver.example.com/api/v4/resolver/links?namespace=gs1&identificationKeyType=gtin&identificationKey=12345678901234&hreflang=en"
Registration payloads
ianaLanguage and defaultIanaLanguage are no longer accepted on the registration payload. Replace them with hreflang: string[] on each variant.
// v3
{
"linkType": "gs1:certificationInfo",
"ianaLanguage": "en",
"defaultIanaLanguage": true,
"context": "au",
"mimeType": "text/html"
}
// v4
{
"linkType": "gs1:certificationInfo",
"hreflang": ["en-AU", "en"],
"context": "au",
"mimeType": "text/html"
}
A publisher who wants region-specific routing should include the regional subtag in hreflang[] (e.g. ["en-AU"]). The resolver does not perform BCP 47 lookup fallback yet (tracked in pyx-industries/pyx-identity-resolver#117), so a request for en-AU will not match a variant tagged only ["en"].
Checklist
Required before upgrade
- Stop the v3 service.
- Back up your object storage bucket.
- Update
RESOLVER_DOMAINto the base URL only (drop the/api/3.0.0suffix) and removeAPI_BASE_URLif previously set. - Pull the
4.0.0container image. - Run the migration in
--dry-runmode and review the output. - Reconcile any documents where divergent metadata would be lost by the first-registered-wins merge rule.
Required during upgrade
- Run the migration (
yarn migrate:v4) and confirmFailed: 0. - Start the v4 service.
Recommended after upgrade
- Update registration scripts to send
hreflang: string[]instead ofianaLanguage. - Update API clients to use the new
?hreflang=<tag>query parameter onlistLinks. - Update consumers that rely on
Accept-Languagematching to send proper BCP 47 tags (e.g.en-AU) that exactly match a variant'shreflang[]entry.