Platform - User Requirement Document
Module:
CORE-16· Version: v1.0 · Last reviewed: 2026-06-15
Purpose
Platform is KICKO's cross-cutting realtime backbone. It turns a noteworthy domain activity - produced anywhere in the system - into a durable, per-person notification and pushes it to the right user's screen the instant it happens, over a private, encrypted channel. Services produce activities; the platform decides who hears about them and delivers them live.
Platform is also KICKO's shared media backbone. It gives every service one way to store a binary on S3-compatible object storage, get back a stable link, and keep a durable meta-link record that ties the object to the entity it illustrates - plus shared reference media (a localization bundle and a Vietnamese banks registry with logos) that the whole platform reads from one source.
Scope
| Included | Excluded |
|---|---|
| Event-driven activity-notification pipeline (consume → resolve → render → persist → push) | Email / SMS / push-notification channels |
| Recipient resolution by organization / merchant / explicit-user scope | Per-user notification preferences, mute, or subscription model |
| Self-scoped notification read API (list, count, mark-one-read, mark-all-read) | The producing services' own activity logic |
| Authenticated, end-to-end-encrypted realtime WebSocket delivery | Notification bell / activity-feed UI styling |
| Stable per-recipient room + topic delivery contract, cross-instance fan-out | Cross-tenant room authorization hardening (known follow-up) |
| Administrative transport controls (status, list, send, broadcast, disconnect) | Image transformation / thumbnailing / on-the-fly resizing |
| Shared object storage: authenticated upload, inline / attachment serving, listing, delete | Per-merchant bucket isolation or tenant-scoped storage |
| Per-object meta-link records with principal binding and a meta-link CRUD API | A managed media-library browsing UI (gallery screen) |
| Shared localization bundle + read-only Vietnamese banks registry with logo serving | Authoring / editing the banks registry (read-only reference data) |
Conceptual Model
5. Feature Catalog
| Feature ID | Feature | Phase | Status | Priority |
|---|---|---|---|---|
platform/ACT | Activity Notifications | P2 | Built | HIGH |
platform/WSS | Realtime WebSocket Stream | P2 | Built | HIGH |
platform/AST | Asset & Object Storage | P2 | Built | HIGH |
platform/MTL | Meta-Link Binding | P2 | Built | HIGH |
platform/BNK | Vietnamese Banks Reference Data | P2 | Built | MED |
platform/IDX | Search Indexing & CDC Sync | P2 | Built | HIGH |
platform/SCH | Unified Search Query API | P2 | Built | HIGH |
Features
ACT - Activity Notifications Built
Feature ID: platform/ACT · Phase: P2 · PRDs: PRD-ACT-001 · Dev: @nx/signal
What it does for users: a noteworthy activity anywhere in KICKO (e.g. a successful payment) becomes a notification addressed to exactly the right people - everyone in an organization, everyone in a merchant, or a named list - rendered as a readable message and kept durably so each person can read and clear it on their own terms.
Requirements
| ID | P | Requirement |
|---|---|---|
| URD-ACT-001 | M | An activity event on the platform stream is consumed and turned into one notification record per resolved recipient. |
| URD-ACT-002 | M | Recipients are resolved by the activity's scope - the whole organization, the whole merchant, or an explicit list of users - and fall back to the activity's actor when none resolve. |
| URD-ACT-003 | M | Each notification stores its recipient, type, organizer, rendered content (text + HTML), an optional action URL, structured data (including the actor), and a read flag with a read timestamp. |
| URD-ACT-004 | M | Notification content is rendered from the event type into a localized, human-readable message. |
| URD-ACT-005 | M | Only recognized event types produce a notification; an unknown event type is skipped without raising an error. |
| URD-ACT-006 | M | A signed-in user can list their own notifications (paginated / filterable), returned with a total count and an unread count. |
| URD-ACT-007 | M | A user can request a count of their notifications (e.g. unread) for badge display. |
| URD-ACT-008 | M | A user can mark a single notification read and mark all of their notifications read; each carries a read timestamp. |
| URD-ACT-009 | M | Every notification read and write is scoped to the authenticated recipient - a user never sees or mutates another user's notifications. |
Acceptance
AC-ACT-01: A scoped activity fans out to one record per recipient
| Given | When | Then |
|---|---|---|
| An activity event with a merchant recipient scope and three members in that merchant | The event is consumed | Three notification records are created, one per member, each with the rendered content, type, organizer, and isRead = false |
AC-ACT-02: No audience falls back to the actor
| Given | When | Then |
|---|---|---|
| An activity whose scope resolves to no members | The event is consumed | Exactly one notification is created, addressed to the activity's actor |
AC-ACT-03: Unknown event types are skipped
| Given | When | Then |
|---|---|---|
| An activity event whose type is not recognized | The event is consumed | No notification is created and no error is raised; the event is skipped |
AC-ACT-04: A user lists only their own notifications with counts
| Given | When | Then |
|---|---|---|
| A signed-in user with notifications, some unread | They list their notifications | Only their own records are returned, with a total count and an unread count |
AC-ACT-05: Mark-all-read clears the unread count
| Given | When | Then |
|---|---|---|
| A user with several unread notifications | They mark all as read | All of their notifications become read with a read timestamp, and a later count returns zero unread |
AC-ACT-06: Read API is self-scoped
| Given | When | Then |
|---|---|---|
| User A and user B each have notifications | User A lists, counts, or marks read | Only user A's records are returned or changed; user B's are never visible or mutable to A |
WSS - Realtime WebSocket Stream Built
Feature ID: platform/WSS · Phase: P2 · PRDs: PRD-ACT-001 · Dev: @nx/signal
What it does for users: the live, private pipe that carries a notification to its owner's screen the moment it is created - authenticated, end-to-end encrypted, and stable enough for clients to subscribe against a fixed contract, with thin controls for operators to inspect and steer connections.
Requirements
| ID | P | Requirement |
|---|---|---|
| URD-WSS-001 | M | When a notification is created it is pushed live to its recipient over WebSocket, to that recipient's private per-recipient channel. |
| URD-WSS-002 | M | Clients connect over an authenticated socket (JWT bearer) and complete an ECDH key exchange; all payloads after the handshake are end-to-end encrypted. |
| URD-WSS-003 | M | Delivery uses a fixed room/topic contract - a per-recipient room and a notification-created topic - so clients subscribe against a stable channel. |
| URD-WSS-004 | M | Delivery reaches a recipient wherever their socket is connected, fanned out across server instances. |
| URD-WSS-005 | M | If the realtime channel is unavailable, notification persistence still succeeds and the live push is skipped without failing the pipeline; a push that fails for one recipient never blocks the others. |
| URD-WSS-006 | M | Administrative transport controls - connection status, list connected clients, get a client, broadcast, send-to-room, send-to-client, and disconnect - are available behind permissions. |
| URD-WSS-007 | S | A connected client receives only its own channel's notifications and not those addressed to other users. |
Acceptance
AC-WSS-01: A new notification arrives live on the recipient's channel
| Given | When | Then |
|---|---|---|
| A recipient holding an authenticated, encrypted socket subscribed to their channel | A notification is created for them | They receive it live on the notification-created topic, encrypted in transit |
AC-WSS-02: Unauthenticated or un-handshaked sockets get nothing
| Given | When | Then |
|---|---|---|
| A socket without a valid JWT or without a completed ECDH handshake | A notification would be pushed | The connection is rejected or no decryptable payload is delivered |
AC-WSS-03: Realtime outage degrades to persistence-only
| Given | When | Then |
|---|---|---|
| The realtime channel is unavailable | An activity is processed | The notification records are still persisted and the pipeline completes without error; live push is skipped |
AC-WSS-04: One failed push does not block others
| Given | When | Then |
|---|---|---|
| An activity fans out to several recipients and one recipient's push fails | The worker pushes all recipients | The remaining recipients still receive their notifications; the failure is isolated |
AC-WSS-05: Admin transport controls require permission
| Given | When | Then |
|---|---|---|
| A user without the transport permission | They call a broadcast / send / disconnect control | The request is denied; a permitted operator can inspect status and steer connections |
AST - Asset & Object Storage Built
Feature ID: platform/AST · Phase: P2 · PRDs: PRD-AST-001 · Dev: @nx/asset
What it does for users: one shared way to put a binary file - a product photo, an organization logo, a document - into object storage and get back a stable link, then stream it back inline or download it by name. Uploads are authenticated; reads are public; a bucket can be listed and an object deleted; a shared localization bundle rides the same surface.
Requirements
| ID | P | Requirement |
|---|---|---|
| URD-AST-001 | M | An authenticated multipart upload stores one or more files in the configured bucket, assigns each a unique, URL-safe object name, and returns its object name and addressable link. |
| URD-AST-002 | M | An upload may carry an optional folder path (validated, capped at two levels) and a principal binding (principalType / principalId / variant) applied to each stored object. |
| URD-AST-003 | M | A stored object can be streamed inline by its object name, supporting nested object paths up to two folder levels. |
| URD-AST-004 | M | A stored object can be downloaded as an attachment (content-disposition) by its object name. |
| URD-AST-005 | M | An authenticated delete removes the object from storage and clears its meta-link records for that bucket + object. |
| URD-AST-006 | M | An authenticated list returns a bucket's objects filtered by prefix, recursion, and a max-keys limit. |
| URD-AST-007 | M | A shared localization bundle can be fetched inline and downloaded as an attachment from the configured bucket. |
| URD-AST-008 | M | Object names and folder segments are validated before any store or fetch; only whitelisted metadata headers are reflected onto responses and nosniff is set on every stream. |
Acceptance
AC-AST-01: Upload stores an object and returns its link
| Given | When | Then |
|---|---|---|
| An authenticated caller with a file | They upload it | The file is stored under a unique, URL-safe object name and the response carries that object name and an addressable link |
AC-AST-02: An object streams back inline and downloads by name
| Given | When | Then |
|---|---|---|
| A previously stored object | A client fetches it by object name, then by its download path | It streams inline on the first, and arrives as an attachment (content-disposition) on the second; both set nosniff |
AC-AST-03: Delete removes object and meta-link together
| Given | When | Then |
|---|---|---|
| A stored object with a meta-link record | An authenticated caller deletes it by object name | The object is removed from storage and its meta-link records for that bucket + object are cleared |
AC-AST-04: Invalid object path is refused
| Given | When | Then |
|---|---|---|
| A request whose object name exceeds the allowed depth or fails validation | The object is fetched or deleted | The request is refused with a bad-request error before any storage access |
MTL - Meta-Link Binding Built
Feature ID: platform/MTL · Phase: P2 · PRDs: PRD-AST-001 · Dev: @nx/asset
What it does for users: every stored object carries a durable record - its storage coordinates and descriptive attributes - that can be bound to the domain entity it illustrates (a product, a variant, an organizer, a ledger, a category, a ticket), so a service can find "the media for this record" without scanning a bucket. Meta-links are queryable as a CRUD resource.
Requirements
| ID | P | Requirement |
|---|---|---|
| URD-MTL-001 | M | Each stored object has a durable meta-link holding its bucket, object name, link, mimetype, size, etag, metadata, storage type, and a sync flag. |
| URD-MTL-002 | M | A meta-link can reference a domain principal by type + id and an optional variant tag, so an object is findable by the entity it belongs to. |
| URD-MTL-003 | M | Meta-links are exposed as a CRUD resource (find, find-by-id, find-one, count, create, update, delete) over JWT / Basic auth. |
| URD-MTL-004 | M | Removing the underlying object clears its meta-link records for that bucket + object. |
Acceptance
AC-MTL-01: Upload writes a meta-link with the object's attributes
| Given | When | Then |
|---|---|---|
| A successful upload | The store completes | A meta-link is created capturing the object's bucket, name, link, mimetype, size, etag, metadata, and storage type, with the sync flag set |
AC-MTL-02: A bound object is findable by its principal
| Given | When | Then |
|---|---|---|
An object uploaded with a principalType and principalId | The meta-link resource is queried for that principal | The object's meta-link is returned, located by the entity it belongs to |
AC-MTL-03: A meta-link write failure does not lose the object
| Given | When | Then |
|---|---|---|
| An upload whose meta-link record cannot be written | The upload completes | The object remains stored and is reported in the result with a per-file meta-link error; other files in the same upload are unaffected |
BNK - Vietnamese Banks Reference Data Built
Feature ID: platform/BNK · Phase: P2 · PRDs: PRD-AST-001 · Dev: @nx/asset
What it does for users: one read-only source of Vietnamese banks and payment providers - each with its short and full name, capability flags (VietQR, disburse, NAPAS), and a logo - so the storefront and back office render bank choices and logos from one registry instead of local copies.
Requirements
| ID | P | Requirement |
|---|---|---|
| URD-BNK-001 | M | A read-only registry of Vietnamese banks / payment providers is served as JSON, each entry carrying a short name, a full name, and capability flags (VietQR, disburse, NAPAS). |
| URD-BNK-002 | M | Each registry entry's logo is projected to an absolute URL (joined with the configured explorer base) so the client can use it directly. |
| URD-BNK-003 | M | Each bank logo is served as a PNG by a <code>.png filename validated against a strict pattern, with long-lived immutable caching. |
| URD-BNK-004 | M | The registry response is cacheable (max-age); an invalid or unknown logo filename returns a structured bad-request / not-found error. |
Acceptance
AC-BNK-01: The registry returns entries with absolute logo URLs
| Given | When | Then |
|---|---|---|
| The configured explorer base URL | A client fetches the banks registry | Each entry is returned with its short name, full name, capability flags, and an absolute bankLogoUrl, with a cache-control max-age |
AC-BNK-02: A bank logo serves as an immutable PNG
| Given | When | Then |
|---|---|---|
A valid <code>.png logo filename | A client requests it | The PNG is streamed with an image content-type, nosniff, and a long-lived immutable cache-control |
AC-BNK-03: An invalid logo filename is refused
| Given | When | Then |
|---|---|---|
A logo filename that fails the strict <code>.png pattern, or a code with no asset | The logo is requested | A structured bad-request (invalid) or not-found (missing) error is returned before any traversal |
IDX - Search Indexing & CDC Sync Built
Feature ID: platform/IDX · Phase: P2 · PRDs: PRD-IDX-001 · Dev: @nx/search
What it does for users: keeps a single, always-fresh search surface in step with the whole platform without any service writing to the search engine. Every committed database change flows out as a change-data event and a single consumer mirrors it into the matching search collection, joins in the related data each document needs, and fans a shared-record change (a rename, a price edit, a shared code) out to every dependent document - so a search hit is complete and current the moment it is read.
Requirements
| ID | P | Requirement |
|---|---|---|
| URD-IDX-001 | M | Every committed change to an indexed source table is captured from the platform's change-data stream and reflected in the matching search collection, without the producing service writing to search. |
| URD-IDX-002 | M | Source tables map to nine search collections (organizers, merchants, categories, devices, sale-channels, products, product-variants, inventories, users); each collection has exactly one document-source table and its other inputs are related / derived. |
| URD-IDX-003 | M | Create, update, and snapshot events upsert the document; a delete or a soft-delete writes a tombstone so the record leaves results while its version ordering is preserved. |
| URD-IDX-004 | M | Before indexing, each document is enriched by joining its related data (owning merchant / organizer names, category set, price, images, scan codes, stock-by-location, option facets, user identity / roles / organizers) so a single hit is self-contained. |
| URD-IDX-005 | M | A change to a shared or parent record fans out to every dependent document (e.g. a merchant or organizer rename, a location rename, a shared code) via targeted patches, never a full re-index. |
| URD-IDX-006 | M | Each document carries a version stamp (source log position) so out-of-order or replayed events never overwrite newer state; child→parent partial patches update only their own fields and never flip the parent's live / deleted state. |
| URD-IDX-007 | M | Events are processed in per-topic batches; a batch that wholly fails to parse, or whose index writes fail, is reported as failed so it can be retried - the stream never silently skips data. |
| URD-IDX-008 | M | When the search engine or an enrichment dependency is unavailable, a circuit breaker pauses the stream and probes for recovery; un-routable / poison messages divert to a dead-letter topic rather than blocking the pipeline. |
| URD-IDX-009 | S | A failed enrichment degrades gracefully - the document is still indexed with the data already on it; a single failed cascade never blocks the other fan-outs in the batch. |
| URD-IDX-010 | M | Record identifiers embedded in search-engine filters are validated so a malformed id can never alter a fan-out's target set. |
Acceptance
AC-IDX-01: A committed write reaches its collection enriched
| Given | When | Then |
|---|---|---|
| A product variant is created in commerce | Its change-data event is consumed | A document appears in the product-variants collection carrying its joined data (name, price, images, scan codes, stock-by-location, option facets) |
AC-IDX-02: A parent rename fans out to dependents
| Given | When | Then |
|---|---|---|
| A merchant with products, categories, and sale-channels is renamed | The merchant change is consumed | The merchants document and every dependent product / category / sale-channel document show the new name, updated by targeted patch, not a re-index |
AC-IDX-03: A delete tombstones the document
| Given | When | Then |
|---|---|---|
| An indexed record is deleted or soft-deleted | The change is consumed | A tombstone is written so the record no longer appears in results, with its version ordering preserved |
AC-IDX-04: A replayed event does not overwrite newer state
| Given | When | Then |
|---|---|---|
| A document already reflects a newer write | An older or replayed event for the same record arrives | The stale event is ignored; the newer state stands |
AC-IDX-05: Engine outage pauses and resumes without loss
| Given | When | Then |
|---|---|---|
| The search engine becomes unavailable mid-stream | A batch is processed | The circuit breaker trips and the stream pauses; on recovery it resumes from where it left off with no events lost |
AC-IDX-06: Enrichment and cascade failures are isolated
| Given | When | Then |
|---|---|---|
| One document's enrichment load fails, or one cascade fan-out fails | The batch is processed | The document is still indexed with the data on it, the other cascades still apply, and the batch is not failed wholesale |
SCH - Unified Search Query API Built
Feature ID: platform/SCH · Phase: P2 · PRDs: PRD-IDX-001 · Dev: @nx/search
What it does for users: one consistent way for any app to full-text search any collection - by name, barcode, option facet, and more - with the same filter, paging, and response contract as every other list endpoint, keyword or semantic, and automatically scoped to what the caller may see.
Requirements
| ID | P | Requirement |
|---|---|---|
| URD-SCH-001 | M | A caller can full-text search any registered collection by name with a query term and an Ignis-style filter (where / limit / skip / order / include / fields), returned in the platform's list envelope-or-array shape with range headers. |
| URD-SCH-002 | M | A caller can request a count of matches for a collection under the same query and where clause. |
| URD-SCH-003 | M | Search supports hybrid keyword + semantic (vector) matching; the generic endpoint runs hybrid by default while resource-mounted search defaults to keyword-only and lets the caller opt in. |
| URD-SCH-004 | M | A resource controller can mount its own scoped /search + /search/count that transparently merges a caller-scope (e.g. tenant) into the query, so a user only searches within what they may see. |
| URD-SCH-005 | M | A caller can choose which fields to text-search (queryBy); otherwise the collection's default search fields apply. |
| URD-SCH-006 | M | Search and count are authenticated (JWT / Basic) and permission-gated. |
| URD-SCH-007 | S | Querying a collection that has not yet been created returns an empty result rather than an error; an invalid query is reported as a bad request. |
Acceptance
AC-SCH-01: Search returns the standard list contract
| Given | When | Then |
|---|---|---|
| A populated collection | A caller searches it by name with a filter | Matching hits are returned in the envelope-or-array shape with range headers, paged by the filter's limit / skip |
AC-SCH-02: Count answers under the same query
| Given | When | Then |
|---|---|---|
| A populated collection | A caller counts it with a query and where | The number of matching documents is returned |
AC-SCH-03: Resource-mounted search is tenant-scoped
| Given | When | Then |
|---|---|---|
| A resource controller mounts its scoped search | A signed-in user searches that resource | Their caller-scope is merged into the query and only records within their scope are returned |
AC-SCH-04: Missing collection returns empty, bad query errors
| Given | When | Then |
|---|---|---|
| A collection not yet created, then an invalid query | Each is searched | The missing collection returns an empty result; the invalid query is reported as a bad request |
Constraints & Non-Goals
| ID | Constraint / Non-Goal |
|---|---|
| URD-CON-001 | This capability delivers the in-app realtime channel only - email / SMS / push-notification fan-out is out of scope. |
| URD-CON-002 | There is no per-user notification preference, mute, or subscription model; addressing is driven by the activity's scope. |
| URD-CON-003 | Room authorization on the transport is permissive today (any authenticated client may subscribe to a room); cross-tenant hardening is a known follow-up. Admin / global collection rooms are not emitted to. |
| URD-CON-004 | Publishing the activity event is the producing service's responsibility; the platform consumes well-formed events. |
| URD-CON-005 | Fine-grained per-action authorization on the meta-link CRUD API is permissive today; tightening it to the meta-link permission set is a known follow-up. |
| URD-CON-006 | Storage is backed by a single configured bucket per host service; per-merchant / per-tenant bucket isolation is out of scope. |
| URD-CON-007 | The banks registry is read-only reference data bundled with the service; there is no authoring or editing surface. |
| URD-CON-008 | Publishing change-data events is the platform's CDC infrastructure (Debezium) responsibility; search consumes well-formed nx.bana.cdc.<schema>.<Table> topic events and does not own capture. |
| URD-CON-009 | SaleOrder is a defined CDC source table but is not yet indexed into a search collection; it is reserved for a later increment. |
| URD-CON-010 | Re-indexing / backfill is an operational snapshot replay; there is no end-user re-index UI. |
| URD-CON-011 | Cross-tenant scoping on the search query relies on the mounting controller supplying the caller-scope; the raw generic search endpoint is gated by collection permission, not by per-record tenant scope. |
Version History
| Version | Date | Change |
|---|---|---|
| v1.0 | 2026-06-15 | Initial URD - ACT activity notifications and WSS realtime WebSocket stream, both Built. Backs PRD-ACT-001. |
| v1.1 | 2026-06-15 | Added the media backbone - AST asset & object storage, MTL meta-link binding, and BNK Vietnamese banks reference data, all Built. Backs PRD-AST-001. |
| v1.2 | 2026-06-15 | Added the search backbone - IDX search indexing & CDC sync and SCH unified search query API, both Built. Backs PRD-IDX-001. |
Related Pages
- Platform overview - module identity + traceability
- PRDs - PRD-ACT-001 · PRD-AST-001 · PRD-IDX-001
- Related: Device signal & notifications - rides this backbone
- Developer: @nx/signal · @nx/asset · @nx/search · @nx/core