Generated page. Written from IdaMilk/SunApps_Gateway at commit c6dbae9 on 2026-07-28, version 2.4.2. The repository README is four lines long, so most of this is inferred from source. Corrections belong in the repository.
SunApps Gateway
What is it?
The single Express server that every SunApps front-end talks to. It authenticates users against Active Directory, forwards most API traffic to the specialised services behind it, owns a handful of endpoints itself, and pushes real-time events to connected clients over Socket.IO.
It is the seam between the applications and everything else — NetSuite, MES, printers, EDI, Jira and the shared SQL Server database.
Purpose
Before the gateway, every front-end would have had to know the address, protocol and credentials of every backend system. The gateway means an application makes one authenticated call to one host, and the routing, credential handling and fan-out happen server-side.
It also gives the platform a single place to put things that would otherwise be duplicated everywhere: login, feature flags, app version checks, activity logging and notifications.
How it Works
Everything starts in server.js. Middleware is registered, then service proxies first, local routes second — and that ordering is the heart of the design.
Each proxy is created by createServiceProxy(envKey) in utils/serviceProxy.js, which reads a target URL from an environment variable. If that variable is set, the request is forwarded to the downstream service with its method, headers and body intact. If the variable is not set, the proxy calls next() and the request falls through to a locally-defined route.
That single fallthrough is what has let the platform be split apart incrementally. A domain can be carved into its own service by setting one environment variable, and rolled back by unsetting it — no code change, no redeploy of callers.
Requests that are not proxied are handled locally: authentication, admin, feature flags, KPI goals, user preferences, Jira, Kaizen, manual entry, notifications and app versions. These are backed by SQL Server through Sequelize.
A second, quieter path runs the other way. /internal exists for the sub-services to call back into the gateway — to trigger a Socket.IO broadcast, send a notification, or check a feature flag. The sub-services do not hold Socket.IO connections themselves; they ask the gateway to do it.
Authentication goes to Active Directory over LDAPS. services/ldapService.js binds with a service account, searches for the user by userPrincipalName, then re-binds as that user with the supplied password to verify it. On success it returns the user's display name, email, sAMAccountName and AD group memberships — group names are extracted from the memberOf DNs and become the basis for authorisation.
Core Features
| Feature | Purpose |
|---|---|
| Service proxying | Routes API traffic to downstream services, with fallthrough to local routes |
| Active Directory authentication | LDAPS bind-search-bind against AD; returns group membership |
| Real-time broadcasts | Socket.IO events for staging, KPI displays, and app updates |
| Internal callback API | Lets sub-services trigger broadcasts, emails and flag checks |
| Feature flags | Runtime on/off switches backed by the SAAppControl table |
| App version checks | Broadcasts current app versions so clients know to reload |
| Admin data | User groups, app control, diversion config, KPI goals, preferences |
| Activity logging | Records user activity to SQL Server |
| Jira and Kaizen integration | Issue creation and continuous-improvement submissions |
| Email and Teams notification | SMTP mail and Teams webhook alerts |
Routing map
Proxied to downstream services — active only when the corresponding URL variable is set:
| Path | Target service variable |
|---|---|
/api/netsuite | NETSUITE_SERVICE_URL |
/api/historian, /api/mes | MES_SERVICE_URL |
/api/mes/pallet-completion | MANUFACTURING_SERVICE_URL |
/api/manufacturing, /api/workorder, /api/staging | MANUFACTURING_SERVICE_URL |
/api/edi | EDI_SERVICE_URL |
/api/printer, /api/zebra | PRINTER_SERVICE_URL |
/api/mes/pallet-completion is registered before the /api/mes catch-all and points somewhere different. The source comment explains why: it is a manufacturing-owned endpoint that predates the MES split, and external callers still POST to the old path. Reordering those two lines would silently send pallet completions to the wrong service.
Owned locally by the gateway:
| Path | Purpose |
|---|---|
/api | Authentication |
/api/admin | Admin and user group management |
/api/logger | Front-end log collection |
/api/jira | Jira issue creation |
/api/kaizen | Kaizen submissions |
/api/version | App version registry |
/api/manual | Manual data entry |
/api/notify | Notification dispatch |
/api/preferences | Per-user preferences |
/api/kpi/goals | KPI goal configuration |
/api/diversion/config | Milk diversion configuration |
/internal | Sub-service callbacks |
/ | Health check — returns pong |
Real-time events
Clients join named rooms and receive targeted broadcasts.
| Room | Events |
|---|---|
staging-dashboard | staging:updated |
stagers | staging:material-low, staging:queue-reminder |
kpi-tv | kpi:update |
production-scheduler | scheduler:updated |
| (all clients) | app:update, app:version-check, staging:buffer-full |
staging:buffer-full is broadcast to everyone rather than to a room — it carries a line-stop condition, fired when a line has used all five buffer completions without staged pallets.
A version-broadcast poller runs every 60 seconds, gated by both the ENABLE_POLLERS feature flag and the DISABLE_POLLERS environment variable.
Data model
SQL Server via Sequelize. Models are auto-loaded from models/, and associations run afterwards if a model defines them.
| Model | Holds |
|---|---|
SAAdmin | Administrative users |
SAAppControl | Feature flags |
SAAppControlUserGroup | Flag-to-group mapping |
SAUserGroups | AD group to application role mapping |
SAUserPreference | Per-user settings |
SADiversionConfig | Milk diversion configuration |
SAKpiGoal | KPI targets |
AppVersion | Current version per application |
ManualEntry | Manually entered data |
UserActivityLog | User activity audit trail |
Two schema behaviours are worth knowing. SAAdmin resolves to a different table in development (SAAdminDev) based on ENVIRONMENT. And db.ensureRuntimeSchema() runs a guarded ALTER TABLE on startup to add periodEnd to SAKpiGoals if it is missing — a migration performed at boot rather than through a migration tool.
Tech Stack
| Technology | Version | Purpose |
|---|---|---|
| Express | 5.2.1 | HTTP framework |
| Sequelize | 6.37.8 | ORM |
| tedious | 19.2.1 | SQL Server driver |
| socket.io | 4.8.3 | Real-time events |
| ldapts | 8.1.7 | Active Directory authentication |
| nodemailer | 8.0.7 | SMTP mail |
| morgan | 1.10.1 | HTTP request logging |
| rotating-file-stream | 3.2.9 | Log rotation |
| cors | 2.8.6 | Cross-origin support |
| dotenv | 17.4.2 | Environment configuration |
| Jest | 30.4.2 | Testing |
| supertest | 7.2.2 | HTTP integration testing |
Node 24 is used by the deployment workflow. Tests live in tests/ and cover the EDI, manufacturing, MES, NetSuite and printer proxy paths.
Interfaces
| System | Direction | Purpose |
|---|---|---|
| SunApps front-ends | inbound | All application API traffic |
| Active Directory | outbound | Authentication and group membership over LDAPS |
| SQL Server | outbound | Admin, preferences, flags, logging |
| NetSuite | outbound | Via the NetSuite Service |
| MES / Historian | outbound | Production data |
| Manufacturing Service | outbound | Work orders, staging, pallet completion |
| EDI | outbound | Trading partner documents |
| Printer service | outbound | Label and document printing |
| Jira | outbound | Issue creation |
| Microsoft Teams | outbound | Deployment and staging alerts via webhook |
| SMTP | outbound | Email notification |
Operations
Deployment is driven by GitHub Actions. Publishing a release triggers the production workflow; workflow_dispatch allows a manual deploy of a specific tag. The workflow builds release notes (release body, then the GitHub API by tag, then a git log fallback), writes a .env file from repository secrets, copies the tree to an internal application server, runs npm ci, restarts the app under a process supervisor, and posts an Adaptive Card to Teams with the release notes. A separate workflow handles the development environment.
Configuration is entirely environment variables, written at deploy time from GitHub secrets. The names are listed below; values are held as repository secrets and never appear in source.
| Group | Variables |
|---|---|
| Server | PORT, ENVIRONMENT, DISABLE_POLLERS |
| Active Directory | AD_URL, AD_BASE_DN, AD_SERVICE_USERNAME, AD_SERVICE_PASSWORD, AD_TLS_PEM |
| NetSuite | NETSUITE_ACCOUNT_ID, NETSUITE_REALM, NETSUITE_CONSUMER_KEY, NETSUITE_CONSUMER_SECRET, NETSUITE_TOKEN_ID, NETSUITE_TOKEN_SECRET, NETSUITE_PALLET_REPRINT_SUITLET_ID |
| SQL Server | SQL_SERVER, SQL_DATABASE, SQL_USER, SQL_PASSWORD |
| Service URLs | NETSUITE_SERVICE_URL, MES_SERVICE_URL, MANUFACTURING_SERVICE_URL, EDI_SERVICE_URL, PRINTER_SERVICE_URL |
| SMTP | SMTP_HOST, SMTP_PORT, SMTP_SECURE, SMTP_USER, SMTP_PASS, SMTP_FROM, SMTP_EDI_REJECTION_TO, SMTP_DFI_REJECTION_TO |
| Jira | JIRA_BASE_URL, JIRA_EMAIL, JIRA_API_TOKEN |
| Teams | TEAMS_STAGING_HOOK |
GET / returns pong and is the simplest liveness check. Request logging goes through morgan into a rotating file stream. A proxy failure returns HTTP 502 with the name of the environment variable whose service could not be reached, which makes misconfiguration easy to spot.
Two things to know when diagnosing:
- An unset
*_SERVICE_URLdoes not fail loudly. The proxy falls through to local routes, so a missing variable looks like a 404 or unexpected behaviour rather than a connection error. - Socket.IO CORS is currently
origin: '*'inservices/socketService.js, even thoughSOCKET_CORS_ORIGINexists in the config. The config value appears not to be applied.
Repository
| Repository | IdaMilk/SunApps_Gateway |
| Version | 2.4.2 |
| Primary language | JavaScript (Node, CommonJS) |
| Files | 77 |
| Last activity | 2026-07-14 |
| Status | Active |
The README still points at Suntado/SunApps_API for cloning. The organisation is now IdaMilk and the repository is SunApps_Gateway; the clone instruction is stale.