Skip to main content
Status Sign in

Asset storage (images + manuals)

Product catalog data isn't just text fields. Every serious catalog system carries a few hundred GB of images, manual PDFs, brand logos, and supporting assets. PowersportOS handles this with an EU-resident object-storage bucket that the apps interact with through presigned URLs, no asset ever touches our application servers, and customer-supplied URLs that disappear over time don't break customer catalogs.

Where assets live

Single bucket: powersportos-assets at hel1.your-objectstorage.com (Hetzner Object Storage, Helsinki region). Everything goes here:

Product images
Primary image + extras for every Part (central) and TenantPart (tenant-owned). Multi-image gallery via the ProductMedia table, ordered, with primary flag and alt-text per row.
Brand logos
Per-Brand logo, exposed in storefront responses so customer themes can render brand badges on product pages.
Manual PDFs
Per-Part service / installation / parts-list PDFs. Optional. Surfaced on storefronts and on the internal YMM Lookup result rows.
Imported CSV assets
When a CSV import references an external image_url or manual_url, the asset is fetched once, stored in our bucket, and the original URL is remembered so re-imports of the same row skip the refetch.

How uploads work

Browser-to-bucket via presigned PUT URLs. The app requests a signed URL from /api/uploads/presign-*, gets back a one-shot URL valid for ~15 minutes, and the browser uploads directly to Hetzner. The API server never sees the asset bytes.

This keeps the API container small (no large request buffers) and lets the upload scale independently of the application, a 25 MB PDF doesn't hold up an API request slot.

Bucket versioning

Versioning is enabled on the bucket (since 2026-05-12). Any object that gets overwritten leaves a previous-version copy that's recoverable for 90 days. Forward-protection only, objects uploaded before that date have no version history. This protects against:

  • Accidental overwrites from a CSV import with the wrong image URL.
  • Bad data from a supplier that we replace later but want to roll back if it turns out wrong.
  • Tenant-side delete operations that propagate to the bucket, the file is gone from the active key but the prior version stays.

CSV-import asset caching

The CSV importer's column shape includes optional image_url and manual_url fields. When set, the importer:

  1. Fetches the asset from the external URL (with size limits and content-type validation).
  2. Uploads to our bucket with a content-hash filename.
  3. Stores both the original URL and our bucket URL on the part row.
  4. On re-import of the same row, if the original URL hasn't changed, skips the refetch entirely.

The "cached" outcome is its own counter in the import-result summary, so the operator can see at a glance how many assets needed refetching versus how many reused cached versions.

Why this matters: URL rot

A supplier's image URL today is not guaranteed to resolve in 18 months. PIM systems that point storefront images at external supplier URLs accumulate broken-image debt that's tedious to detect and tedious to fix. By caching assets locally on import, customer catalogs stay intact even when supplier URLs disappear.

Backups

Bucket versioning is the in-place recovery. For full disaster recovery (bucket-level loss), a planned-but-not-built rclone mirror to the Hetzner Storage Box backup target. Deferred until real customer data exists, versioning + Hetzner's own infrastructure SLAs cover the typical recovery scenarios today.