Skip to content

PRD: Asset & media management

ModulePlatform (CORE-16)PRD IDPRD-AST-001
StatusShippedOwnerPlatform squad
Date2026-06-15Versionv1.0
Packages@nx/asset · @nx/coreURDAST · MTL · BNK

TL;DR

Gives every KICKO service one shared way to store, address, and serve binary media - product photos, organizer logos, documents - on S3-compatible object storage. An authenticated multipart upload lands each file under a unique, URL-safe object name and records a durable meta-link: the object's storage coordinates plus its mimetype, size, etag, and an optional binding to the domain entity it belongs to (a product, a variant, an organizer, a ledger…). Objects stream back inline or download as attachments by name; meta-links are queryable as a CRUD resource so any object is findable by its owner. The same backbone serves a shared localization bundle and a read-only Vietnamese banks registry with bank logos, so the storefront and back office render bank choices and translations from one source.

1. Context & Problem

Every product module needs to attach media. Products and variants need photos, organizers need logos, ledgers and tickets need supporting documents, and the storefront needs bank logos and a shared translation bundle. Without a shared backbone each service would invent its own upload path, its own object-naming scheme, and its own way of remembering which file belongs to which record - and bank reference data would be copy-pasted and drift.

The gap is twofold. First, a store-and-remember primitive: a place to put a binary, get back a stable link, and keep a durable record that ties the object to the entity it illustrates, so the catalogue can find "the photo for this variant" without scanning a bucket. Second, shared reference media: one localization bundle and one Vietnamese banks registry (with logos) that the whole platform reads instead of each app shipping its own copy.

This PRD specifies that shared media backbone: authenticated upload to object storage with a meta-link record per object, principal binding and a meta-link query API, public serving and listing, the localization bundle, and the banks registry plus logo endpoints.

2. Goals & Non-Goals

Goals

  • An authenticated multipart upload to an S3-compatible bucket that assigns each file a unique, URL-safe object name and returns an addressable link (AST).
  • A durable meta-link record per stored object capturing its storage coordinates, mimetype, size, etag, metadata, and storage type (MTL).
  • Principal binding at upload time - an object can carry a principalType / principalId / variant so it is findable by the entity it belongs to (MTL).
  • Public serving: stream an object inline by name, download it as an attachment, and list a bucket by prefix; plus delete that removes object and meta-link together (AST).
  • A shared localization bundle served inline and downloadable from the configured bucket (AST).
  • A read-only Vietnamese banks registry as JSON - each entry with names, capability flags, and an absolute logo URL - plus a per-bank PNG logo endpoint (BNK).
  • Safe addressing: validated object names / folder depth, strict logo filenames, whitelisted response headers, and nosniff on every stream (AST, BNK).

Non-Goals

  • Image transformation, thumbnailing, or on-the-fly resizing - objects are served as stored.
  • Per-merchant bucket isolation or tenant-scoped storage - a single configured bucket backs the host service.
  • A managed media-library browsing UI - this PRD delivers the API surface, not a gallery screen.
  • Authoring or editing the banks registry - it is read-only reference data bundled with the service.
  • The realtime notification pipeline (ACT, WSS) - specified in PRD-ACT-001.

3. Success Metrics

MetricTarget / signal
Upload integrityEvery successfully stored object has a matching meta-link with its size, mimetype, and etag
FindabilityAn object uploaded with a principal binding is retrievable by that principal's type + id
Naming safetyNo stored object name collides or carries an unsafe path; every served stream sets nosniff
Reference reuseStorefront and back office render bank choices and logos from the one registry, not local copies
Cleanup integrityDeleting an object leaves no orphan meta-link for that bucket + object

4. Personas & Use Cases

PersonaGoal in this feature
Owner / ManagerAttach a photo to a product or a logo to an organization and have it shown everywhere that entity appears
Producing serviceUpload an object, bind it to a record, and later resolve the record's media by meta-link
Storefront / clientStream an image by name, render bank choices and logos, and load the shared translation bundle
Platform operatorTrust that object names are safe and that deletes clean up both storage and metadata

Core scenario: a manager adds a photo while editing a variant. The client uploads the file with the variant's principalType and principalId; the service stores it under a unique object name, returns the link, and writes a meta-link recording the object and its binding. Wherever that variant later appears, its media is resolved by the meta-link. Separately, the checkout screen fetches the banks registry once and renders each provider with its absolute logo URL.

5. User Stories

  • As a manager, I attach an image to a product and it appears wherever the product is shown, so the catalogue looks complete.
  • As a producing service, I bind an uploaded object to the record it illustrates, so I can find that record's media later without scanning the bucket.
  • As a client, I stream an image by its object name and download a document as an attachment, so media just works in the UI.
  • As a client, I read one banks registry with absolute logo URLs, so I render payment choices without joining paths myself.
  • As an operator, I delete an object and trust that its meta-link is cleaned up too, so nothing dangles.
  • As a client, I load one shared localization bundle, so every surface translates from the same source.

6. Functional Requirements

#RequirementURD ref
FR-1Authenticated multipart upload stores one or more files in the configured bucket, each under a unique URL-safe object name, returning its object name and addressable linkURD-AST-001
FR-2An upload may carry a folder path (validated, max two levels) and a principal binding (principalType / principalId / variant)URD-AST-002 · URD-MTL-002
FR-3After a successful store, a meta-link is created per object capturing bucket, object name, link, mimetype, size, etag, metadata, storage type, and sync flagURD-MTL-001
FR-4A stored object streams inline by object name; nested object paths up to two folder levels are supportedURD-AST-003
FR-5A stored object downloads as an attachment (content-disposition) by object nameURD-AST-004
FR-6An authenticated delete removes the object from storage and clears its meta-link records for that bucket + objectURD-AST-005 · URD-MTL-004
FR-7An authenticated list returns a bucket's objects filtered by prefix, recursion, and max-keysURD-AST-006
FR-8A shared localization bundle is served inline and downloadable as an attachment from the configured bucketURD-AST-007
FR-9Meta-links are exposed as a CRUD resource (find, find-by-id, find-one, count, create, update, delete) over JWT / Basic authURD-MTL-003
FR-10The Vietnamese banks registry is served as JSON - each entry with short name, full name, capability flags (VietQR, disburse, NAPAS), and an absolute logo URLURD-BNK-001..002
FR-11Each bank logo is served as a PNG by <code>.png filename, validated against a strict pattern, with long-lived immutable cachingURD-BNK-003
FR-12Object names, folder paths, and logo filenames are validated; only whitelisted metadata headers are echoed and nosniff is set on every streamURD-AST-008 · URD-BNK-004

Full requirement text and acceptance criteria live in the Platform URD - AST · MTL · BNK. This PRD references them rather than restating them.

7. Non-Functional Requirements

AreaRequirement
Naming safetyObject names are unique and URL-safe; folder depth is capped and every path segment validated before a store or fetch
Header hygieneOnly whitelisted metadata headers are reflected onto responses; nosniff is always set; header values are stripped of CR/LF
Storage abstractionStorage is reached through a single S3-compatible helper configured from the environment (endpoint, access key, secret key, bucket)
ResilienceA failed meta-link write does not lose the stored object - the object is reported with its per-file meta-link error; meta-link cleanup on delete is best-effort and logged
CachingThe banks registry is cacheable (max-age); bank logos are served immutable with a long max-age
AuthUpload, delete, list, and the meta-link CRUD API require JWT / Basic auth; object reads, the localization bundle, and the banks registry / logos are public reads
i18nBank display names and the shared localization bundle are the localization source; bank entries carry both short and full names

8. UX & Flows

The upload surface accepts one or more files with an optional folder path and a principal binding, and returns each object's name, link, and meta-link (or its meta-link error). Reads stream by object name inline or as a download; a bucket can be listed by prefix. The banks registry and per-bank logos are fetched directly into the UI.

9. Data & Domain

EntityRole
MetaLinkThe durable record of a stored object - bucket, object name, link, mimetype, size, etag, metadata, storage type, sync flag - plus an optional principal binding (principalType, principalId, variant)
Stored objectThe binary in the S3-compatible bucket, addressed by its object name and reachable through its link
Bank registry entryA read-only reference record - short name, full name, capability flags (VietQR, disburse, NAPAS), and a logo - bundled with the service

Conceptual only - full schema and invariants live in the asset domain model. Principal relations are soft references resolved by type + id, not database foreign keys.

10. Dependencies & Assumptions

Depends on

  • S3-compatible object storage - an endpoint, access key, secret key, and a default bucket are configured in the environment.
  • @nx/core - owns the MetaLink schema / model / repository, the principal-type registry, and the shared environment keys.
  • A configured explorer base URL - used to project relative bank-logo paths into absolute URLs for the client.

Assumptions

  • The producing service knows the principalType / principalId of the record it is attaching media to.
  • The localization bundle and the banks-VN logo assets are present in the configured bucket / bundled with the service.
  • A single configured bucket is sufficient for the host service; multi-bucket / per-tenant isolation is out of scope here.

11. Risks & Open Questions

Risk / questionMitigation / status
Object stored but meta-link write failsObject is not lost - it is returned with a per-file meta-link error and logged for reconciliation
Unsafe or colliding object namesNames are generated unique and URL-safe; folder depth capped and segments validated; reads reject invalid paths
Meta-link CRUD authorization is permissive todayKnown follow-up - tightening the meta-link API to its permission set is tracked (URD-CON-005)
Bank logo path traversalLogo filenames must match a strict <code>.png pattern; anything else is refused before disk access
Banks registry drift across appsSingle read-only registry served from one source; apps stop shipping local copies

12. Release Plan & Launch Criteria

AspectPlan
PhaseP2 - AST, MTL, BNK in the URD feature catalog
RolloutMounted by host services that need media; no feature flag
MigrationNone - the MetaLink store and the bundled banks assets ship with the service
Launch criteriaUpload stores objects with a meta-link and optional binding; objects stream inline and download by name; delete clears both storage and meta-link; the banks registry returns entries with absolute logo URLs and logos serve as PNG
MonitoringUpload error rate, meta-link write-failure rate, object-vs-meta-link consistency, registry / logo cache hit behaviour

13. FAQ

Where do uploaded files go? Into the configured S3-compatible bucket, each under a unique, URL-safe object name, with an addressable link returned to the caller.

How do I find the media for a specific product or variant? Bind the object at upload time with its principalType and principalId; the meta-link then makes it findable by that entity through the meta-link query API.

What happens if the meta-link can't be written after a file is stored? The object is not lost - the upload result reports that file with a meta-link error so it can be reconciled; other files in the same upload are unaffected.

Are uploaded objects public? Object reads, the localization bundle, and the banks registry / logos are public reads; uploading, deleting, listing, and the meta-link CRUD API require authentication.

Can I edit the banks registry? No - it is read-only reference data bundled with the service. Each entry is projected with an absolute logo URL so the client can use it directly.

Does deleting an object leave its record behind? No - a delete removes the object from storage and clears its meta-link records for that bucket + object.

References

Proprietary and Confidential. Unauthorized copying, distribution, or use of this software is strictly prohibited.