Skip to content

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

IncludedExcluded
Event-driven activity-notification pipeline (consume → resolve → render → persist → push)Email / SMS / push-notification channels
Recipient resolution by organization / merchant / explicit-user scopePer-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 deliveryNotification bell / activity-feed UI styling
Stable per-recipient room + topic delivery contract, cross-instance fan-outCross-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, deletePer-merchant bucket isolation or tenant-scoped storage
Per-object meta-link records with principal binding and a meta-link CRUD APIA managed media-library browsing UI (gallery screen)
Shared localization bundle + read-only Vietnamese banks registry with logo servingAuthoring / editing the banks registry (read-only reference data)

Conceptual Model

5. Feature Catalog

Feature IDFeaturePhaseStatusPriority
platform/ACTActivity NotificationsP2BuiltHIGH
platform/WSSRealtime WebSocket StreamP2BuiltHIGH
platform/ASTAsset & Object StorageP2BuiltHIGH
platform/MTLMeta-Link BindingP2BuiltHIGH
platform/BNKVietnamese Banks Reference DataP2BuiltMED
platform/IDXSearch Indexing & CDC SyncP2BuiltHIGH
platform/SCHUnified Search Query APIP2BuiltHIGH

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

IDPRequirement
URD-ACT-001MAn activity event on the platform stream is consumed and turned into one notification record per resolved recipient.
URD-ACT-002MRecipients 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-003MEach 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-004MNotification content is rendered from the event type into a localized, human-readable message.
URD-ACT-005MOnly recognized event types produce a notification; an unknown event type is skipped without raising an error.
URD-ACT-006MA signed-in user can list their own notifications (paginated / filterable), returned with a total count and an unread count.
URD-ACT-007MA user can request a count of their notifications (e.g. unread) for badge display.
URD-ACT-008MA user can mark a single notification read and mark all of their notifications read; each carries a read timestamp.
URD-ACT-009MEvery 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
GivenWhenThen
An activity event with a merchant recipient scope and three members in that merchantThe event is consumedThree 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
GivenWhenThen
An activity whose scope resolves to no membersThe event is consumedExactly one notification is created, addressed to the activity's actor
AC-ACT-03: Unknown event types are skipped
GivenWhenThen
An activity event whose type is not recognizedThe event is consumedNo notification is created and no error is raised; the event is skipped
AC-ACT-04: A user lists only their own notifications with counts
GivenWhenThen
A signed-in user with notifications, some unreadThey list their notificationsOnly their own records are returned, with a total count and an unread count
AC-ACT-05: Mark-all-read clears the unread count
GivenWhenThen
A user with several unread notificationsThey mark all as readAll of their notifications become read with a read timestamp, and a later count returns zero unread
AC-ACT-06: Read API is self-scoped
GivenWhenThen
User A and user B each have notificationsUser A lists, counts, or marks readOnly 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

IDPRequirement
URD-WSS-001MWhen a notification is created it is pushed live to its recipient over WebSocket, to that recipient's private per-recipient channel.
URD-WSS-002MClients 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-003MDelivery uses a fixed room/topic contract - a per-recipient room and a notification-created topic - so clients subscribe against a stable channel.
URD-WSS-004MDelivery reaches a recipient wherever their socket is connected, fanned out across server instances.
URD-WSS-005MIf 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-006MAdministrative 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-007SA 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
GivenWhenThen
A recipient holding an authenticated, encrypted socket subscribed to their channelA notification is created for themThey receive it live on the notification-created topic, encrypted in transit
AC-WSS-02: Unauthenticated or un-handshaked sockets get nothing
GivenWhenThen
A socket without a valid JWT or without a completed ECDH handshakeA notification would be pushedThe connection is rejected or no decryptable payload is delivered
AC-WSS-03: Realtime outage degrades to persistence-only
GivenWhenThen
The realtime channel is unavailableAn activity is processedThe 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
GivenWhenThen
An activity fans out to several recipients and one recipient's push failsThe worker pushes all recipientsThe remaining recipients still receive their notifications; the failure is isolated
AC-WSS-05: Admin transport controls require permission
GivenWhenThen
A user without the transport permissionThey call a broadcast / send / disconnect controlThe 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

IDPRequirement
URD-AST-001MAn 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-002MAn 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-003MA stored object can be streamed inline by its object name, supporting nested object paths up to two folder levels.
URD-AST-004MA stored object can be downloaded as an attachment (content-disposition) by its object name.
URD-AST-005MAn authenticated delete removes the object from storage and clears its meta-link records for that bucket + object.
URD-AST-006MAn authenticated list returns a bucket's objects filtered by prefix, recursion, and a max-keys limit.
URD-AST-007MA shared localization bundle can be fetched inline and downloaded as an attachment from the configured bucket.
URD-AST-008MObject 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
GivenWhenThen
An authenticated caller with a fileThey upload itThe 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
GivenWhenThen
A previously stored objectA client fetches it by object name, then by its download pathIt 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
GivenWhenThen
A stored object with a meta-link recordAn authenticated caller deletes it by object nameThe object is removed from storage and its meta-link records for that bucket + object are cleared
AC-AST-04: Invalid object path is refused
GivenWhenThen
A request whose object name exceeds the allowed depth or fails validationThe object is fetched or deletedThe 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

IDPRequirement
URD-MTL-001MEach 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-002MA 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-003MMeta-links are exposed as a CRUD resource (find, find-by-id, find-one, count, create, update, delete) over JWT / Basic auth.
URD-MTL-004MRemoving 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
GivenWhenThen
A successful uploadThe store completesA 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
GivenWhenThen
An object uploaded with a principalType and principalIdThe meta-link resource is queried for that principalThe 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
GivenWhenThen
An upload whose meta-link record cannot be writtenThe upload completesThe 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

IDPRequirement
URD-BNK-001MA 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-002MEach 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-003MEach bank logo is served as a PNG by a <code>.png filename validated against a strict pattern, with long-lived immutable caching.
URD-BNK-004MThe 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
GivenWhenThen
The configured explorer base URLA client fetches the banks registryEach 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
GivenWhenThen
A valid <code>.png logo filenameA client requests itThe 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
GivenWhenThen
A logo filename that fails the strict <code>.png pattern, or a code with no assetThe logo is requestedA 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

IDPRequirement
URD-IDX-001MEvery 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-002MSource 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-003MCreate, 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-004MBefore 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-005MA 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-006MEach 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-007MEvents 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-008MWhen 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-009SA 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-010MRecord 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
GivenWhenThen
A product variant is created in commerceIts change-data event is consumedA 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
GivenWhenThen
A merchant with products, categories, and sale-channels is renamedThe merchant change is consumedThe 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
GivenWhenThen
An indexed record is deleted or soft-deletedThe change is consumedA 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
GivenWhenThen
A document already reflects a newer writeAn older or replayed event for the same record arrivesThe stale event is ignored; the newer state stands
AC-IDX-05: Engine outage pauses and resumes without loss
GivenWhenThen
The search engine becomes unavailable mid-streamA batch is processedThe 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
GivenWhenThen
One document's enrichment load fails, or one cascade fan-out failsThe batch is processedThe 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

IDPRequirement
URD-SCH-001MA 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-002MA caller can request a count of matches for a collection under the same query and where clause.
URD-SCH-003MSearch 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-004MA 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-005MA caller can choose which fields to text-search (queryBy); otherwise the collection's default search fields apply.
URD-SCH-006MSearch and count are authenticated (JWT / Basic) and permission-gated.
URD-SCH-007SQuerying 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
GivenWhenThen
A populated collectionA caller searches it by name with a filterMatching 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
GivenWhenThen
A populated collectionA caller counts it with a query and whereThe number of matching documents is returned
AC-SCH-03: Resource-mounted search is tenant-scoped
GivenWhenThen
A resource controller mounts its scoped searchA signed-in user searches that resourceTheir 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
GivenWhenThen
A collection not yet created, then an invalid queryEach is searchedThe missing collection returns an empty result; the invalid query is reported as a bad request

Constraints & Non-Goals

IDConstraint / Non-Goal
URD-CON-001This capability delivers the in-app realtime channel only - email / SMS / push-notification fan-out is out of scope.
URD-CON-002There is no per-user notification preference, mute, or subscription model; addressing is driven by the activity's scope.
URD-CON-003Room 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-004Publishing the activity event is the producing service's responsibility; the platform consumes well-formed events.
URD-CON-005Fine-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-006Storage is backed by a single configured bucket per host service; per-merchant / per-tenant bucket isolation is out of scope.
URD-CON-007The banks registry is read-only reference data bundled with the service; there is no authoring or editing surface.
URD-CON-008Publishing 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-009SaleOrder is a defined CDC source table but is not yet indexed into a search collection; it is reserved for a later increment.
URD-CON-010Re-indexing / backfill is an operational snapshot replay; there is no end-user re-index UI.
URD-CON-011Cross-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

VersionDateChange
v1.02026-06-15Initial URD - ACT activity notifications and WSS realtime WebSocket stream, both Built. Backs PRD-ACT-001.
v1.12026-06-15Added the media backbone - AST asset & object storage, MTL meta-link binding, and BNK Vietnamese banks reference data, all Built. Backs PRD-AST-001.
v1.22026-06-15Added the search backbone - IDX search indexing & CDC sync and SCH unified search query API, both Built. Backs PRD-IDX-001.

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