A look one level below the API surface. Knowing how AppEngine is structured helps you reason about latency, consistency, and which parts you can scale independently.
Shape of the codebase
AppEngine is a NestJS modular monolith. One process, many feature modules, all wired together in app.module.ts. The reason it's a monolith and not microservices: the domains share too much data (CRM ↔ Storefront ↔ Banking) for service boundaries to be cheap, and the org-scoped indexing pattern keeps it horizontally scalable as a single deployable.
Each top-level folder under src/ is a NestJS module: users/, storefront/, crm/, chat/, voice/, phone/, broadcast/, finance/, banking/, automation/, ai/, site/, sync/, etc. They expose controllers under their own URL prefix and depend on shared infra modules (repositories/, dynamic-query/, upstream/, connect/).
Global wiring lives in app.module.ts:
JwtAuthGuardregistered asAPP_GUARD— every route auth-checked unless@PublicRoute().AllExceptionsFilterasAPP_FILTER— uniform error envelopes.ContentPermissionInterceptorasAPP_INTERCEPTOR— content-level RBAC.EventEmitterModule.forRoot()— in-process pub/sub for cross-module events.ScheduleModule.forRoot()— cron jobs (purges, escalations, automation triggers).
Storage
MongoDB is the primary data store. Records are stored in collections that mirror their datatype (e.g. contact, product, order). Every record is a BaseModel\<T\> document — system fields on the wrapper, payload in data. Multi-tenant scoping is encoded in the sk (sort key), typically of the form <orgid>/<datatype>/<id>, and indexed accordingly.
Redis plays three roles:
- Queue backing — Bull/BullMQ-style queues for async jobs.
- WebSocket adapter — distributed pub/sub so multiple AppEngine pods can serve the same chat/voice room.
- Hot-path caching — selectively used by services where the cache miss is cheap and the hit ratio is high.
Object storage (S3-compatible) holds uploaded files, broadcast assets, voice recordings, and generated documents.
REST primary, GraphQL minimal
The API surface is REST. Hundreds of routes across the domain modules, all auth-gated and org-scoped. Sample endpoints:
/profile/signinNo auth/repository/find/{datatype}JWT+APIKEY/storefront/cart/update/{cartid}JWT/automation/flows/{flowId}/executeJWTGraphQL is wired up (@nestjs/graphql + Apollo) but contains only a sample resolver. Don't plan around it. The schema is auto-generated to src/schema.gql for tooling — that's the extent of it today.
Realtime
WebSocket gateways carry chat, voice, and presence:
ChatGateway— main support/staff chat.CommunityChatGateway— group chats inside the Community module.SimpleVoiceGateway— OpenAI Realtime API streaming.AIVoiceGateway— AI-driven voice assistant.
A Redis adapter sits behind all four so a chat connected to pod A can receive messages emitted from pod B. Sticky sessions aren't required for correctness — the adapter handles cross-pod fan-out — but they help latency.
For one-way streaming (LLM responses), AppEngine uses SSE:
/ai/agent/streamJWT/ai/stream/{streamId}JWTThe POST returns a streamId; the GET opens the SSE connection and streams tokens until completion.
Async work: events + queues
Two complementary patterns run side-by-side.
EventEmitter (@nestjs/event-emitter) — in-process events for cross-module reactions. CRM emits contact.created; Automation listens and runs flows. CRM emits email.opened; Broadcast updates delivery stats; Automation runs trigger logic. Events are fire-and-forget within a process; if a pod dies mid-handler, the event is lost, so the pattern is reserved for non-critical reactions.
Queue processors (SyncModule, SyncQueueModule) — durable async work. Datatype, OneOff, Schedule, Automation, AutomationTrigger, Notification — each is a BullMQ-style consumer pulling from Redis. Social-platform sync (FB, TikTok, LinkedIn, X, Pinterest, Snapchat, Discord, Twitch, Slack, Reddit, Google Ads), notification dispatch, escalation logic, billing reconciliation all run here. Failed jobs retry with backoff; dead letters land in a separate queue for review.
Specific schedulers worth knowing:
PurgeScheduler— TTL cleanup for soft-deleted records.AutomationSchedulerService— wakes scheduled-trigger flows.
Cross-cutting concerns
- Usage tracking —
UsageMiddlewareruns globally;ServicePricingServiceandAiChargeServiceprice requests and AI tokens. - Health —
/monitoring/healthis rate-limit-skipped (@SkipThrottle) so probes don't burn quota. - Activity logging —
ActivityModulewrites a structured audit trail; CRM consumes it for engagement scoring.
Deployment
AppEngine runs on Kubernetes. Each pod is the same NestJS process — stateless, scalable horizontally. The SiteModule includes a K8sDeploymentService that manages tenant-deployed sites in the same cluster (sites get their own pods/services).
Typical topology:
- Multiple AppEngine pods behind a load balancer.
- A managed MongoDB cluster.
- A Redis cluster (queues + WS adapter share it; can be split if load demands).
- An object store for files.
- Tenant site pods scheduled in a separate namespace by
K8sDeploymentService.
Sticky sessions on the load balancer help WebSocket clients but aren't required.
Where to go next
- Multi-tenancy —
orgid, JWT, RBAC in detail. - Data model —
BaseModel\<T\>, optimistic concurrency, state transitions. - Performance — pagination, caching, sticky sessions.