Assets Library
The Assets Library is the platform's digital-asset substrate. It owns:
- A central catalogue of files (images, video, PDFs, documents) reusable across Product media, Product attachments, Category main images, and CMS pages.
- A folder tree for organising assets.
- Pluggable storage backends — Local filesystem, Amazon S3, GCP Cloud Storage — configured from Settings, exactly one active at a time.
- Public/private visibility per asset.
- Reference protection: assets in use by Catalog or CMS cannot be deleted until every reference is detached.
Quick start
- Active adapter — open Settings → Storage and pick one of
local,s3, orgcs. Defaults tolocal. - Local FS — set
assets.local.base_dir(defaultvar/assets). The admin server creates the directory tree at upload time. Setassets.local.public_url_baseif the backend is fronted by a different public hostname (e.g. a CDN or a reverse proxy); leave it blank and every URL is built on this deployment's public API origin instead. - Amazon S3 — set
assets.s3.bucket,assets.s3.region,assets.s3.access_key_id,assets.s3.secret_access_key. Optional:assets.s3.endpoint(for S3-compatible providers like MinIO),assets.s3.prefix,assets.s3.public_base_url(CDN base URL prefix). - GCP Cloud Storage — set
assets.gcs.bucketand eitherassets.gcs.service_account_json(paste the full JSON) or rely on ambient credentials. Optional:assets.gcs.prefix,assets.gcs.public_base_url. - Test the connection —
POST /api/v1/admin/assets/storage/self-check. Returns{ ok: true }on success or{ ok: false, reason: ... }on misconfiguration. The dashboard is atGET /api/v1/admin/assets/storage/state.
Upload constraints
Two settings gate uploads, both checked before any byte reaches the active adapter:
assets.allowed_file_types— list of file extensions or MIME types (e.g.["jpg", "png", "image/jpeg", "application/pdf"]). The sentinel["*"](default) disables the gate. Wildcards likeimage/*match any MIME under that prefix. The check considers both the declared MIME and the magic-number-sniffed MIME (file-type) and rejects when either fails.assets.max_file_size_mb— integer cap in MB.0(default) disables the cap.
Visibility & private URLs
Each asset has visibility ∈ {public, private}. Public URLs are stable;
private URLs are short-lived (TTL = assets.private_url_ttl_sec, default
300s) and re-issued by the admin or storefront on every read.
For the local FS adapter, private URLs are HMAC-signed by the backend.
The signing key is sourced from the ASSETS_LIBRARY_HMAC_KEY env var
(generate with openssl rand -hex 32); rotate to invalidate every
outstanding private URL.
For S3 and GCS, private URLs are V4-signed through the vendor SDK; nothing in the backend needs to be configured beyond the credentials.
Soft-delete & hard-delete
Deletes go through a two-step lifecycle:
DELETE /api/v1/admin/assets/:id→ soft-delete. SetsdeletedAtandpurgeAfterAt = now + assets.soft_delete_retention_days(default 30). The asset disappears from listings and pickers but remains recoverable viaPOST /api/v1/admin/assets/:id/restore.- The repeating
HardDeleteAssetWorkerfinalises afterpurgeAfterAt: removes the row and asks the adapter to delete the underlying file. If the backend cannot remove the file (read-only mount, revoked credentials, network partition), the row stays andpendingCleanupis set totrue; the worker retries on the next tick.
Reference protection
Soft-delete is rejected with 409 ASSET_REFERENCED when any of the
following points at the asset:
gallery_items.asset_id(Product gallery)product_attachments.asset_id(Product attachments)products.download_asset_id(virtual-download products)categories.main_image_asset_id(Category main image)cms_pages.bodycontaining a{ type: "asset_ref", assetId }node
The reference list is included in the error envelope's details array so
the admin UI can show "in use by" prompts.
Pluggable storage adapters
Each backend implements the StorageAdapter SPI in
packages/modules/assets_library/src/backend/services/storage/. Adding a fourth
backend (Azure Blob, Backblaze B2, …) is a matter of dropping in a new
class that implements:
selfCheck(): Promise<{ ok: boolean; reason?: string }>;
newLocator({ assetId, originalFilename }): string;
put({ locator, mimeType, visibility, stream, sizeBytes }): Promise<void>;
resolveUrl({ locator, visibility, ttlSec? }): Promise<{ url; expiresAt }>;
open({ locator }): Promise<NodeJS.ReadableStream>;
delete({ locator }): Promise<void>;
setVisibility?({ locator, visibility }): Promise<void>;
…and registering it in AdapterRegistry.build. See the existing
LocalFsStorageAdapter, S3StorageAdapter, and GcsStorageAdapter for
templates.
Legacy escape hatch
Pre-existing assets rows whose storage_url was a URL this platform did not
issue are tagged storage_backend = 'legacy' at migration time. The legacy
resolver returns the URL verbatim for public assets — rebasing it onto the
public API origin when the stored value is host-relative — and refuses to flip
them to private (we cannot sign URLs we didn't issue), which surfaces as
409 ASSET_LEGACY_LOCATOR_CANNOT_HARDEN to the admin. Re-upload through
the active adapter to upgrade.