AstroBaaS

Catalogue & product data

GPSR Product Safety Metadata

Free — GPL coresize Mshipped

This ships in the core. It is implemented in src/core/models.ts. What follows is the plan it was built from, kept as written — the shipped code may go further than it, or differ in the details.

Five free-text fields on every product, so that a listing can carry what Regulation (EU) 2023/988 (GPSR) asks an online offer to show. Storage, the API and the admin form only. Checking the data, templates for manufacturers, and the optional extra fields belong to the paid @astrobaas/compliance package (plan: paid/gpsr-compliance-templates-validation).

Corrected 2026-10-12. An earlier version of this page described importer fields, structured address columns, a ce_marking flag, compliance_docs, a batch-code format rule, and separate Manufacturer and Importer tabs. None of that shipped. What shipped is the five fields below. The earlier text is kept beside this file as PLAN.md.bak-2026-10-12.

The problem

Since 13 December 2024, an EU online listing must show the manufacturer (name, postal and electronic address), an EU responsible person when the manufacturer is outside the EU, information to identify the product, and its warnings and safety information. A shop that cannot store these cannot lawfully list the product, so the storage is free and in the core: a merchant must never be unable to comply because they did not pay.

What shipped

Five optional string fields on Product, all free text:

FieldAdmin label (Safety tab)Meant for
gpsr_manufacturerManufacturer (one-line input)name and contact of the maker
gpsr_eu_responsibleEU responsible person (one-line input)name and contact of the EU responsible person, when the maker is outside the EU
gpsr_identifierTraceability identifiertype, batch or serial
gpsr_warningsWarnings and safety information (textarea)shown to the buyer before purchase
gpsr_instructionsCare and use instructions (textarea)where safety depends on them
  • Model: src/core/models.ts, the gpsr_* fields on Product, with the reasoning in the comment above them.
  • Writable through the API: they are on the shared allow-list WRITABLE_PRODUCT_FIELDS in src/lib/product-fields.ts, which POST /api/products and PUT /api/products/:id (and therefore the admin form) use. A partial PUT with only a GPSR field leaves the rest of the product alone. The bulk-edit operations (/api/product-bulk) do not set them.
  • Sanitised and bounded on save: saveProduct runs each of the five (GPSR_TEXT_FIELDS) through the same HTML sanitiser as the description and caps it at 2,000 characters. That holds for every writer, because they all go through saveProduct. One consequence: & is stored as &.
  • Read back as stored: the product API returns them at the top level of the product, like every other field. Plain fields rather than a sub-object, so a feed, a CSV and the form all write the same shape.
  • Admin: a “Product safety (GPSR)” section on the product editor’s Safety tab (src/pages/admin/products.astro), with one list (GPSR_FIELDS) driving both load and save.

What it deliberately does NOT do

  • No structured addresses. There are no separate street, city, postcode or country columns, and no country code. The manufacturer and the responsible person are each one free-text field. Reason: a feed or a CSV can fill one field, and the regulation asks for the contact to be shown, not parsed.
  • No importer fields. Only the manufacturer and the EU responsible person.
  • No CE-marking flag and no compliance-document list. Safety marks are an optional field in the paid package. Documents are not stored anywhere.
  • No format rule on the identifier. A batch code is free text, sanitised and length-capped like the others. It is not checked against a pattern.
  • No validation that the data is complete or correct. The core stores what the merchant enters. Readiness checking, per Article 19, is the paid package’s job.
  • No list filters by GPSR field, no dedicated audit entries, no export columns. Changes appear in the ordinary product audit entry like any other field.
  • No translations. gpsr_* are not in TRANSLATABLE_PRODUCT_FIELDS, so a product has one warning text, whatever the shop’s languages.

Where the rest lives

@astrobaas/compliance in the commercial repository (plan: paid/gpsr-compliance-templates-validation) adds:

  • a per-product readiness check against Article 19, with a dashboard, filters and a CSV of the gaps;
  • reusable manufacturer and EU-responsible-person templates, applied to many products at once;
  • optional “Safety marks” and “Expiry date” product fields, declared through PLUGIN_HOOKS.PRODUCT_FIELD_DEFS and staff-only unless the merchant publishes them.

It reports only, and never blocks a sale.

Acceptance checks

Each of these must be able to fail:

  • PUT /api/products/:id with { "gpsr_manufacturer": "Acme Ltd; 1 Road; 10115 Berlin; Germany" } stores the value and returns it, and leaves every other field unchanged.
  • A GPSR field containing <script> is stored without it, and one longer than 2,000 characters is cut to 2,000, through the API as well as the form.
  • The product editor loads all five fields and saves them back unchanged.
  • A product with no GPSR data returns no gpsr_* keys, and saves without error.

Known issue

The sanitiser stores & as &amp;. Re-saving does not encode it again, because sanitize-html is idempotent here (checked: a second pass returns the same string). But the admin editor puts the stored text into an <input> as-is, so the operator sees &amp;. A storefront that prints the field as escaped text shows the same thing. Anything that renders these fields as plain text should decode the entities first. This is not fixed here.


Corrected by hand on 2026-10-12 to match the code. See docs/COMMERCE-PLAN.md for the full catalogue and CONTRIBUTING.md for how to work on this repository.