PRD: Asset & media management
| Module | Platform (CORE-16) | PRD ID | PRD-AST-001 |
| Status | Shipped | Owner | Platform squad |
| Date | 2026-06-15 | Version | v1.0 |
| Packages | @nx/asset · @nx/core | URD | AST · 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/variantso 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
nosniffon 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
| Metric | Target / signal |
|---|---|
| Upload integrity | Every successfully stored object has a matching meta-link with its size, mimetype, and etag |
| Findability | An object uploaded with a principal binding is retrievable by that principal's type + id |
| Naming safety | No stored object name collides or carries an unsafe path; every served stream sets nosniff |
| Reference reuse | Storefront and back office render bank choices and logos from the one registry, not local copies |
| Cleanup integrity | Deleting an object leaves no orphan meta-link for that bucket + object |
4. Personas & Use Cases
| Persona | Goal in this feature |
|---|---|
| Owner / Manager | Attach a photo to a product or a logo to an organization and have it shown everywhere that entity appears |
| Producing service | Upload an object, bind it to a record, and later resolve the record's media by meta-link |
| Storefront / client | Stream an image by name, render bank choices and logos, and load the shared translation bundle |
| Platform operator | Trust 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
| # | Requirement | URD ref |
|---|---|---|
| FR-1 | Authenticated 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 link | URD-AST-001 |
| FR-2 | An upload may carry a folder path (validated, max two levels) and a principal binding (principalType / principalId / variant) | URD-AST-002 · URD-MTL-002 |
| FR-3 | After a successful store, a meta-link is created per object capturing bucket, object name, link, mimetype, size, etag, metadata, storage type, and sync flag | URD-MTL-001 |
| FR-4 | A stored object streams inline by object name; nested object paths up to two folder levels are supported | URD-AST-003 |
| FR-5 | A stored object downloads as an attachment (content-disposition) by object name | URD-AST-004 |
| FR-6 | An authenticated delete removes the object from storage and clears its meta-link records for that bucket + object | URD-AST-005 · URD-MTL-004 |
| FR-7 | An authenticated list returns a bucket's objects filtered by prefix, recursion, and max-keys | URD-AST-006 |
| FR-8 | A shared localization bundle is served inline and downloadable as an attachment from the configured bucket | URD-AST-007 |
| FR-9 | Meta-links are exposed as a CRUD resource (find, find-by-id, find-one, count, create, update, delete) over JWT / Basic auth | URD-MTL-003 |
| FR-10 | The Vietnamese banks registry is served as JSON - each entry with short name, full name, capability flags (VietQR, disburse, NAPAS), and an absolute logo URL | URD-BNK-001..002 |
| FR-11 | Each bank logo is served as a PNG by <code>.png filename, validated against a strict pattern, with long-lived immutable caching | URD-BNK-003 |
| FR-12 | Object names, folder paths, and logo filenames are validated; only whitelisted metadata headers are echoed and nosniff is set on every stream | URD-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
| Area | Requirement |
|---|---|
| Naming safety | Object names are unique and URL-safe; folder depth is capped and every path segment validated before a store or fetch |
| Header hygiene | Only whitelisted metadata headers are reflected onto responses; nosniff is always set; header values are stripped of CR/LF |
| Storage abstraction | Storage is reached through a single S3-compatible helper configured from the environment (endpoint, access key, secret key, bucket) |
| Resilience | A 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 |
| Caching | The banks registry is cacheable (max-age); bank logos are served immutable with a long max-age |
| Auth | Upload, 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 |
| i18n | Bank 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
| Entity | Role |
|---|---|
MetaLink | The 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 object | The binary in the S3-compatible bucket, addressed by its object name and reachable through its link |
Bank registry entry | A 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 theMetaLinkschema / 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/principalIdof 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 / question | Mitigation / status |
|---|---|
| Object stored but meta-link write fails | Object is not lost - it is returned with a per-file meta-link error and logged for reconciliation |
| Unsafe or colliding object names | Names are generated unique and URL-safe; folder depth capped and segments validated; reads reject invalid paths |
| Meta-link CRUD authorization is permissive today | Known follow-up - tightening the meta-link API to its permission set is tracked (URD-CON-005) |
| Bank logo path traversal | Logo filenames must match a strict <code>.png pattern; anything else is refused before disk access |
| Banks registry drift across apps | Single read-only registry served from one source; apps stop shipping local copies |
12. Release Plan & Launch Criteria
| Aspect | Plan |
|---|---|
| Phase | P2 - AST, MTL, BNK in the URD feature catalog |
| Rollout | Mounted by host services that need media; no feature flag |
| Migration | None - the MetaLink store and the bundled banks assets ship with the service |
| Launch criteria | Upload 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 |
| Monitoring | Upload 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
- URD: Platform - AST · MTL · BNK
- Sibling PRD: Activity notifications & websocket push
- Module: Platform - overview + traceability
- Developer: @nx/asset · storage · meta-links · @nx/core