Skip to content

Architecture

1. System Context (C4 L1)

2. Container View (C4 L2)

No Kafka consumer, no BullMQ worker. The only background fan-out is the Redis-backed WebSocket emitter.

3. Component View (C4 L3) — Internal Layering

LayerResponsibility
RoutesHTTP surface; custom routes declared in RestPaths + controller definitions.ts
ControllersAuth + permission gate, DTO mapping, fire WS notify on submit
ServicesSubscriberService subscribe/unsubscribe/stats logic
RepositoriesDrizzle queries, soft-delete, getStatistics() rollup
ComponentsApplicationWebSocketComponent — Redis emitter + socket event service

4. State Machines Index

EntityStatesDiagram
InquiryNEW, PROCESSING, COMPLETED, CLOSED, CANCELLED→ jump
SubscriberACTIVATED, DEACTIVATED, ARCHIVED→ jump

Both use IGNIS Statuses. Transitions are not machine-enforced — admins move Inquiry freely via CRUD update; Subscriber transitions are driven by SubscriberService (subscribe ↔ unsubscribe). The diagrams below show the intended lifecycle.

Inquiry

FromEventToGuards
[*]POST /inquiries/submitNEWdefault status
NEWadmin updatePROCESSINGnone (manual)
PROCESSINGadmin updateCOMPLETED / CLOSEDnone (manual)
*admin updateCANCELLEDnone (manual)

Subscriber

FromEventToGuards
[*]subscribe (no existing row)ACTIVATEDunique email
ACTIVATEDsubscribe (existing active)ACTIVATEDidempotent — returns existing
DEACTIVATEDsubscribeACTIVATEDreactivate, clear unsubscribedAt
ACTIVATEDunsubscribe(token)DEACTIVATEDtoken must resolve to a row

5. Runtime Scenarios

5.1 Inquiry submission + real-time notify

StepDetail
1-3Inquiry persisted with default status=NEW, type=000_CONSULT unless overridden
4-6WS notify is fire-and-forget (not awaited); skipped if emitter not ready
6Broadcast to outreach/inquiries and outreach/inquiries/{id} via Promise.allSettled
7201 returned regardless of WS outcome

5.2 Subscribe (idempotent)

5.3 Unsubscribe (token)

6. Crosscutting Concerns

ConcernHow this service handles it
AuthNJWT (Issuer = identity), JWKS verified per request; public endpoints skip auth
AuthZCasbin permissions seeded via migrations; CRUD + stats gated, public submit/subscribe/unsubscribe open
i18nSubscriber.locale (vi/en) chooses newsletter language; permission labels carry { en, vi }
LoggingStructured key-value (key: %s); WS notify logs inquiry id + rooms
TracingNo-op (no tracer wired)
Idempotencysubscribe idempotent on email; unsubscribe idempotent (re-applying DEACTIVATED is safe)
Soft-deleteSoftDeletableRepository (deletedAt); default filter deletedAt IS NULL
IDsSnowflake via IdGenerator, worker 10; unsubscribeToken is also a Snowflake

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