docs
/
Platform

Architecture

What runs where, what each dependency is for, and how a request travels through AppEngine.

Appmint is a modular monolith with thin clients. One deployable backend, one shared model package, and clients that hold almost no business logic.

The backend process

appengine boots a single NestJS application that serves five transports off one HTTP server:

TransportWhere
REST123 controllers
GraphQLApollo driver, schema auto-generated, playground on
Socket.IOsix gateways, Redis adapter for cross-instance fan-out
Raw WebSocket/ws/hub — the hub-agent gateway, attached to the same server
Server-Sent Eventsstreaming AI and voice responses

Swagger UI is mounted at /documentation, built at boot with deepScanRoutes.

Feature modules

app.module.ts imports 45 feature modules. Grouped by what they are for:

GroupModules
Core dataRepositoryModule, DynamicQueryModule, HistoryModule, SyncModule
IdentityUsersModule, ClientAccountModule, OrgManagementModule
Content & sitesSiteModule, ContentStudioModule, NoticeModule, ToolsModule
CommerceStorefrontModule, SalesChannelModule, FinanceModule, AffiliateModule
CRM & marketingCRMModule, DataEnrichmentModule, BroadcastModule, AnalyticsModule (analytics dashboards, the /stats engagement engine and Studio overviews)
OperationsBusinessMadeModule, WorkflowModule, CheckinModule, LogisticsModule, StaffPortalModule
CommunicationsChatModule, PhoneModule, VoiceModule, CommentsModule, NotesModule
SocialCommunityModule, EventsModule
MoneyBankingModule
Storage marketplaceStowboModule
HardwareDeviceIntegrationsModule, PrintModule
AIAIModule, DiscoveryModule
Platform opsMonitoringModule, UsageModule, K8sManagementModule, ConnectModule, UpstreamModule

DiscoveryModule is worth singling out: it serves self-describing API documentation so AI agents can introspect the platform at runtime.

The request pipeline

Order matters here, and several behaviors only make sense once you know it.

1. Express middleware helmet → cookieParser → compression → rate limit → express.json({ limit: '4mb' }).

Rate limiting is 15-minute windows, RATE_LIMIT_MAX requests (default 10,000), keyed on IP with trust proxy set to exactly TRUST_PROXY_HOPS (default 2 — Cloudflare, then Traefik) so X-Forwarded-For cannot be spoofed. /monitoring/health is exempt.

2. CurrentUserMiddleware Mounted on *. Drops scanner probes, resolves orgid, resolves the identity from API key / bearer token / cookie, and pre-loads the target record into request.currentData for permission checks. See Identity and access.

3. JwtAuthGuard Layered authorization — public route, own profile, record ownership, per-record roles, decorator roles and permissions, then token validation.

4. ValidationPipe Global, with whitelist: true, forbidNonWhitelisted: true and transform: true. Unknown body properties are rejected, not ignored — a client sending an extra field gets a 400.

5. Controller and service The handler runs. Domain services talk to the repository layer.

6. ResponseTimeInterceptor Global, stamps timing on the way out.

CORS runs after the rate limiter

The rate limiter is an app.use() middleware and Nest applies CORS during init(), which is later. A short-circuited 429 therefore skipped the CORS headers and browsers reported a generic network error instead. The limiter's handler now sets Access-Control-Allow-Origin itself. Keep that in mind if you add any other early short-circuit.

Data stores

StoreRole
MongoDBPrimary. One database per organization; the connection is resolved from the org record and cached per process.
RedisCache, Socket.IO adapter, BullMQ backing store.
Elasticsearch / OpenSearchFull-text and faceted search. The repository falls back to Mongo queries when a search index is not in play.
S3-compatible object storageFiles, via flydrive. Uploads return a signed URL plus generated size variants (xs, sm, md).

Background work

Three Bull queues, all on Redis:

QueueCarries
datatype-queuePer-datatype record processing
schedule-queuePer-org schedule records and automation timers
Sync queue (SYNC_QUEUE)Platform-level syncs — deliberately separate, so a slow Facebook walk cannot delay org schedules. It also overrides Bull's default 30-second lock, which was handing long jobs to a second worker mid-flight.

Separate workers handle ads, billing, escalation and notifications.

@nestjs/schedule provides cron; @nestjs/event-emitter carries in-process domain events.

Realtime

Six Socket.IO gateways:

GatewayPurpose
chat.gatewayCustomer and operator chat
community-chat.gatewayCommunity group messaging
ai-voice.gatewayCRM AI assistant voice
realtime-voice.gateway, simple-voice.gatewayOpenAI Realtime voice streaming
device-events.gatewayHardware events fanned out to subscribed UIs

All share the Redis adapter, so any instance can deliver to any connected client.

Separately, HubGatewayService attaches a plain ws server to the same HTTP listener, scoped to paths starting /ws/hub. It is attached in a try/catch — if it throws, the API still boots and logs hub-gateway: not attached.

Resilience

main.ts installs process-level handlers for unhandledRejection and uncaughtException that log and keep serving. This exists because a fire-and-forget outbound call — a Twilio request whose upstream 401s — would otherwise take the whole server down. It is a safety net, not a substitute for local error handling.

HTTP keep-alive is 65 s with a 66 s headers timeout, deliberately above a typical 60 s load-balancer idle timeout so connections are closed by the LB rather than accumulating.

The clients

Every client is deliberately thin. Business rules live on the server, so the same behavior applies whether a record is changed from Studio, the mobile app, a storefront or your own code.

The shared contract is @jaclight/dbsdk: DataType, RoleType, BaseModel<T>, permission tables and default schemas, imported by the backend and the web clients. Change a model there and both sides move together.