PRD: Asset & media management
| Module | Platform (CORE-16) | PRD ID | PRD-AST-001 |
| Status | Shipped | Owner | Phát Nguyễn |
| Date | 2026-06-15 | Version | v1.0 |
| Capability | Media storage · reference data | URD | AST · MTL · BNK |
TL;DR
One shared way for every BANA service to store, address, and serve binary media - product photos, organizer logos, documents - on object storage: an authenticated upload lands each file under a unique, URL-safe name and writes a durable media record (storage coordinates, content type, size, integrity fingerprint, plus an optional binding to the entity it belongs to), so any object is findable by its owner and streams inline or downloads by name. The same backbone also serves a shared localization bundle and a read-only Vietnamese banks registry with 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 - photos on products and variants, logos on organizers, supporting documents on ledgers and tickets - and the storefront needs bank logos and a shared translation bundle. Without a shared backbone, each service would invent its own upload path, object-naming scheme, and way of remembering which file belongs to which record, and bank reference data would be copy-pasted until it drifted.
The gap is twofold. First, a store-and-remember primitive: put a binary down, get back a stable link, and keep a durable record tying the object to the entity it illustrates - so the catalogue finds "the photo for this variant" without scanning storage. Second, shared reference media: one localization bundle and one Vietnamese banks registry (with logos) the whole platform reads, instead of each app shipping its own copy.
This PRD specifies that backbone: authenticated upload to object storage with a media record per object, owner binding and a media-record 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 a configured storage bucket that assigns each file a unique, URL-safe object name and returns an addressable link (
AST). - A durable media record per stored object capturing its storage coordinates, content type, size, integrity fingerprint, metadata, and storage type (
MTL). - Owner binding at upload time - an object can carry an owner type / owner id / 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 media record together (
AST). - A shared localization bundle served inline and downloadable from the configured bucket (
AST). - A read-only Vietnamese banks registry as structured data - each entry with names, capability flags, and an absolute logo URL - plus a per-bank logo image endpoint (
BNK). - Safe addressing: validated object names / folder depth, strict logo filenames, whitelisted response headers, and content-type-sniffing disabled 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
| Metric | Target / signal |
|---|---|
| Upload integrity | Every successfully stored object has a matching media record with its size, content type, and integrity fingerprint |
| Findability | An object uploaded with an owner binding is retrievable by that owner's type + id |
| Naming safety | No stored object name collides or carries an unsafe path; every served stream disables content-type sniffing |
| 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 media record 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 organizer 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 its media record |
| 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 owner type and id; the service stores it under a unique name, returns the link, and writes a media record capturing the object and its binding. Wherever that variant later appears, its media resolves through the record. 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 storage.
- 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 media record 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 an owner binding (owner type / owner id / variant) | URD-AST-002 · URD-MTL-002 |
| FR-3 | After a successful store, a media record is created per object capturing bucket, object name, link, content type, size, integrity fingerprint, 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 by object name | URD-AST-004 |
| FR-6 | An authenticated delete removes the object from storage and clears its media-record entries for that bucket + object | URD-AST-005 · URD-MTL-004 |
| FR-7 | An authenticated list returns a bucket's objects filtered by name prefix, depth, and a result cap | 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 | Media records are exposed as a full create/read/update/delete management resource (list, get-by-id, get-one, count, create, update, delete), authenticated | URD-MTL-003 |
| FR-10 | The Vietnamese banks registry is served as structured data - 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 an image by a strict per-bank logo filename, validated against that 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 content-type sniffing is disabled 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; content-type sniffing is always disabled; header values are stripped of line breaks |
| Storage abstraction | Storage is reached through a single storage abstraction configured from the environment (endpoint, access key, secret key, bucket) |
| Resilience | A failed media-record write does not lose the stored object - the object is reported with its per-file media-record error; media-record cleanup on delete is best-effort and logged |
| Caching | The banks registry is cacheable; bank logos are served immutable with a long cache lifetime |
| Auth | Upload, delete, list, and the media-record management API require authentication; 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 an owner binding, returning each object's name, link, and media record (or its media-record error). Reads stream by 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 |
|---|---|
| Media record | The durable record of a stored object - bucket, object name, link, content type, size, integrity fingerprint, metadata, storage type, sync flag - plus an optional owner binding (owner type, owner id, variant) |
| Stored object | The binary in the configured 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. Owner relations are soft references resolved by type + id, not database foreign keys.
10. Dependencies & Assumptions
Depends on
- Object storage - an endpoint, access key, secret key, and a default bucket are configured in the environment.
- Platform core - owns the media-record definition, the owner-type registry, and the shared environment keys.
- A configured public base URL - used to project relative bank-logo paths into absolute URLs for the client.
Assumptions
- The producing service knows the owner type / owner id of the record it is attaching media to.
- The localization bundle and the Vietnamese bank 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 media-record write fails | Object is not lost - it is returned with a per-file media-record 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 |
| Media-record management authorization is permissive today | Known follow-up - tightening the media-record API to its permission set is tracked (URD-CON-005) |
| Bank logo path traversal | Logo filenames must match a strict per-bank 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 media-record store and the bundled banks assets ship with the service |
| Launch criteria | Upload stores objects with a media record and optional binding; objects stream inline and download by name; delete clears both storage and media record; the banks registry returns entries with absolute logo URLs and logos serve as images |
| Monitoring | Upload error rate, media-record write-failure rate, object-vs-record consistency, registry / logo cache hit behaviour |
13. FAQ
Where do uploaded files go? Into the configured storage bucket, each under a unique, URL-safe 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 with its owner type and id; the media record then makes it findable by that entity through the query API.
What happens if the media record can't be written after a file is stored? The object is not lost - the upload result flags that file with a media-record error for reconciliation; other files in the same upload are unaffected.
Are uploaded objects public? Object reads, the localization bundle, and the banks registry / logos are public; uploading, deleting, listing, and the media-record management API require authentication.
Can I edit the banks registry? No - it is read-only reference data bundled with the service, each entry projected with an absolute logo URL the client uses directly.
Does deleting an object leave its record behind? No - a delete removes the object from storage and clears its media-record entries for that bucket + object.
References
- URD: Platform - AST · MTL · BNK
- Sibling PRD: Activity notifications & realtime push
- Module: Platform - URD
- Developer: @nx/asset · storage · meta-links · @nx/core