Architecture
GCMS is built as a layered Node.js application following a classic Model-Controller pattern, served by Express with real-time updates powered by Socket.io. This page documents the major components, how they fit together, and how a request flows through the system.
High-level overview
flowchart TB
Browser["Browser<br/>(EJS views + JS)"]
Listen["listen.js<br/>(starts HTTP server)"]
Server["server.js<br/>(HTTP + Socket.io)"]
App["app.js<br/>(Express app)"]
Routes["routes/<br/>(URL → controller)"]
Controllers["controllers/<br/>(request handling)"]
Models["models/<br/>(SQL queries)"]
Utils["utils/<br/>(shared helpers)"]
DB[("Supabase<br/>PostgreSQL + Storage")]
External["External services<br/>Microsoft Graph<br/>Google Gemini<br/>Mailtrap"]
Browser <--> Listen
Listen --> Server
Server --> App
App --> Routes
Routes --> Controllers
Controllers --> Models
Controllers --> Utils
Models --> DB
Utils --> DB
Utils --> External
Project layout
The repository is organised by responsibility, not by feature:
Folder |
Purpose |
|---|---|
|
Express route definitions; map URLs to controllers |
|
Request handlers; validate input, call models, send responses |
|
SQL queries and database access logic |
|
Cross-cutting concerns (auth, sessions, sockets, email, AI) |
|
EJS templates rendered server-side |
|
Static assets (CSS, client-side JS, images) |
|
Schema definition and seed scripts |
|
Standalone Node scripts (e.g. |
|
Jest unit and integration tests, one pair of files per user requirement |
The layered structure means most features touch one file per layer.
For example, the task feature uses routes/taskRoutes.js,
controllers/taskControllers.js, and models/taskModels.js.
Request flow
A typical HTTP request flows through the application like this:
Browser sends a request (e.g.
POST /api/tasks)listen.js is what’s actively listening; Node hands the request to the HTTP server it started
server.js forwards the request to the Express app (and routes socket frames to the Socket.io handlers)
app.js routes it to the correct
app.usemountRoute file matches the URL pattern and calls the controller
Controller validates the request, checks authentication, and calls one or more models
Model executes a parameterised SQL query against Supabase
Controller formats the result and sends an HTTP response
Socket.io (where relevant) broadcasts the change to other connected clients in the same project
Entry points
GCMS uses a three-file entry point that separates concerns cleanly and makes each layer independently testable. Tests can import the Express app, the HTTP+Socket.io server, or trigger live listening behaviour as needed without touching the others.
app.js
app.js is the testable Express application. It:
Loads environment variables from
.env.authConfigures Express middleware (JSON parsing, cookies, EJS templating, static assets)
Sets up sessions and authentication via the
utils/helpersMounts each route file under the appropriate path (
/for page routes,/apifor everything else)Exports the configured app
When NODE_ENV=test, app.js injects a bypass middleware that
populates req.user with a stub user, so integration tests can
exercise authenticated routes without going through the full OAuth
flow.
server.js
server.js wraps the Express app in a Node HTTP server and attaches
Socket.io to it. It:
Imports the configured Express app from
app.jsCreates an HTTP server wrapping the app
Attaches Socket.io to the HTTP server
Initialises socket event handlers via
utils/socket.jsExports the configured server (but does not call
listen)
This separation allows integration tests for socket features to import the server directly without binding to a port.
listen.js
listen.js is the runtime entry point invoked by npm run dev
and npm run prod. Its single responsibility is to import the
configured server and begin listening on port 3000.
This means production code, integration tests, and socket tests can each pick the entry point that matches their needs without pulling in irrelevant behaviour.
Routes
Route files (in routes/) define the URL surface of the application.
Each route file groups related endpoints — for example
taskRoutes.js handles all task-related URLs.
There are two categories of routes:
- Page routes (
pageRoutes.js) Server-side rendered EJS views, mounted at
/. Examples:/,/projects,/projects/:id,/profile.- API routes (everything else)
JSON endpoints, mounted at
/api. Used by client-side JS for asynchronous operations. Grouped by resource:authRoutes.js— Microsoft OAuth flowuserRoutes.js— user profile and account operationsprojectRoutes.js— project CRUDtaskRoutes.js— task CRUD and assignmentcalendarRoutes.js— meetings and Microsoft calendar syncnotificationsRoutes.js— notification fetch and read statecontributionsRoutes.js— contribution calculationsnoteRoutes.js— sticky-note widgets on the project boardfileRoutes.js— shared file upload and listingaiRoutes.js— AI assistant chat
A complete list of endpoints with parameters and responses is in the API Reference.
Controllers
Controllers (in controllers/) contain the per-endpoint logic:
Validate the incoming request body and parameters
Verify the user is authenticated and authorised for the action
Call one or more model functions to read or write data
Shape the response and send it back to the client
Trigger notifications or socket broadcasts where appropriate
Controllers do not contain SQL. All database access is delegated to the model layer.
Models
Models (in models/) are the only layer that talks to the database
directly. Each model file exposes async functions that:
Build parameterised SQL queries (using either the
pgpool or the Supabase JS client depending on the operation)Execute the query and return the result
Handle row-level edge cases (not found, empty results, etc.)
Keeping SQL in models means controllers stay clean and unit tests can mock the data layer without spinning up a database.
Utils
The utils/ folder holds cross-cutting helpers that don’t belong
to any single feature:
File |
Purpose |
|---|---|
|
Passport.js Microsoft OAuth strategy and middleware |
|
Express session configuration |
|
Socket.io event handlers and room management |
|
Supabase JS client + |
|
Google Gemini API client and prompt construction |
|
File download and Office text extraction |
|
Nodemailer transporter for notification emails |
|
Mailtrap SMTP configuration |
Real-time updates
GCMS uses Socket.io for real-time features. utils/socket.js sets
up the server-side socket and joins each authenticated client to a
room for every project they belong to.
When a relevant event occurs — a new chat message, a task status change, a widget moved on the project board — the controller emits a socket event to the project’s room. All connected clients in that project receive the update and refresh their UI without a page reload.
Socket events used:
chat:message— new chat messagetask:update— task created, updated, or deletedwidget:update— widget added, moved, or removednotification— new notification for a specific user
Authentication flow
User authentication uses the OAuth 2.0 Authorization Code flow with Microsoft as the identity provider, implemented via Passport.js.
sequenceDiagram
participant User
participant GCMS
participant Microsoft
User->>GCMS: Click "Sign in with Microsoft"
GCMS->>Microsoft: Redirect to /authorize
User->>Microsoft: Enter credentials
Microsoft->>GCMS: Redirect to /api/auth/callback with code
GCMS->>Microsoft: Exchange code for access token
Microsoft->>GCMS: Access token + refresh token
GCMS->>GCMS: Create or update user record
GCMS->>User: Redirect to dashboard with session cookie
The access token is stored in the session and used for subsequent Microsoft Graph API calls (e.g. creating calendar events). The refresh token is used to obtain a new access token when the current one expires.
External integrations
GCMS integrates with four external services. See External Integrations for full details.
Microsoft Graph API — authentication and calendar sync
Google Gemini — AI assistant
Supabase — database and file storage
Mailtrap — email notifications
Testing
GCMS has a dedicated automated test suite with both unit and integration coverage. See Testing for the full testing strategy, how to run tests, and conventions for writing new ones.