Skip to main content
Audience: Developers editing Meridian/backend/. Follow these patterns when adding or modifying routes, middlewares, services, and schemas.

Architecture overview

  • Stack: Express, Mongoose, JWT (cookies + Bearer), optional Passport/SAML.
  • Multi-tenant DB: Tenant is derived from subdomain (e.g. rpi from rpi.meridian.study). In development, localhost and IP hosts default to rpi.
  • Per-request DB: Every request gets req.db (a Mongoose connection) and req.school (subdomain string) from a global middleware in app.js.
  • Global DB: Every request also gets req.globalDb for cross-tenant data (GlobalUser, PlatformRole, TenantMembership, Session). Use only via getGlobalModelService(req, ...) in auth and platform-admin logic. See Multi-Tenant Identity & SSO for full architecture.
Never use a default mongoose connection or mongoose.model() directly for app data. Always use the request-scoped connection and getModelService (see below); use getGlobalModelService only for global identity data.

Database and models

Getting models (required pattern)

Always resolve models through getModelService with the current req:
const getModels = require('../services/getModelService');

// In a route or service that has req:
const { User, Event, Org } = getModels(req, 'User', 'Event', 'Org');
  • Why: req.db is the tenant-specific Mongoose connection. getModels(req, ...) registers schemas on req.db and returns the correct model instances for that tenant.
  • Where: Use in routes and in services that receive req (e.g. userServices.js, studySessionService.js). If you add a new service that touches the DB, have the route pass req and use getModels(req, ...) inside the service.
  • Do not: Use require('../schemas/user') and then mongoose.model('User') or any global mongoose connection for app data. That bypasses multi-tenancy.

Adding a new model

1

Add schema

Add a schema under Meridian/backend/schemas/ (or Meridian/backend/events/schemas/ for event-related models).
2

Register in getModelService

In Meridian/backend/services/getModelService.js:
  • Require the schema.
  • Add an entry to the models object: ModelName: req.db.model('ModelName', schema, 'collectionName').
  • Use the exact collection name (third argument) the app expects (e.g. 'users', 'events').
3

Connections

No need to touch connectionsManager.js for a new model; it only provides req.db per school.

Connections manager

  • File: Meridian/backend/connectionsManager.js
  • Role: Maintains a pool of Mongoose connections per school and exposes connectToDatabase(school).
  • Usage: Only app.js calls it. The middleware in app.js sets req.db = await connectToDatabase(subdomain) and req.school = subdomain.
  • Adding a school: Extend the schoolDbMap in getDbUriForSchool() with the new subdomain and env var (e.g. MONGO_URI_<SCHOOL>). Fallback is DEFAULT_MONGO_URI.

Auth and middlewares

Order of use

Typical order on a route: auth first, then org/permission (if needed), then handler.
  • verifyToken or verifyTokenOptional must run before any middleware or handler that uses req.user.
  • Org middlewares (requireOrgPermission, etc.) expect req.user and use getModels(req, ...); they must run after verifyToken.

verifyToken.js

MiddlewareBehavior
verifyTokenReads JWT from req.cookies.accessToken or Authorization: Bearer <token>. On success sets req.user = { userId, roles }. On missing token: 401 with code: 'NO_TOKEN'. On expired: 401 with code: 'TOKEN_EXPIRED'. On invalid: 403 with code: 'INVALID_TOKEN'.
verifyTokenOptionalSame token sources. If no token or invalid, continues without req.user. If token expired, may try to refresh via refreshToken cookie and set new accessToken cookie and req.user.
authorizeRoles(…allowedRoles)Use after verifyToken. Checks req.user.roles; if the user has none of allowedRoles, responds 403 Forbidden.
Example:
const { verifyToken, authorizeRoles } = require('../middlewares/verifyToken');

router.get('/admin-only', verifyToken, authorizeRoles('admin'), async (req, res) => {
  const { User } = getModels(req, 'User');
  // ...
});

orgPermissions.js

Use these for org-scoped actions. They use getModels(req, 'OrgMember', 'Org') and expect req.user (so use after verifyToken).
MiddlewarePurpose
requireOrgPermission(permission, orgParam = ‘orgId’)Resolves org from params/body/query. Ensures user is active member with the given permission; sets req.orgMember and req.org.
requireAnyOrgPermission(permissions, orgParam)Same but user needs at least one of the listed permissions.
requireOrgOwner(orgParam)Ensures user is the org owner; sets req.org.
Convenience wrappersrequireRoleManagement, requireMemberManagement, requireEventManagement, requireAnalyticsAccess, requireEquipmentManagement, requireEquipmentModification — all take (orgParam = 'orgId').
Permission strings must match constants/permissions.js. Use the constants in code:
const { requireOrgPermission } = require('../middlewares/orgPermissions');
const { ORG_PERMISSIONS } = require('../constants/permissions');

router.post('/orgs/:orgId/events', verifyToken, requireOrgPermission(ORG_PERMISSIONS.MANAGE_EVENTS), async (req, res) => {
  // req.org, req.orgMember set
});

Routes

Structure

  • Routes live in Meridian/backend/routes/ (and under Meridian/backend/events/routes/ for event features).
  • Each file exports an Express Router. Mount in app.js (or in events/index.js for event routes).
  • Prefer middleware + single handler per path; keep handlers thin and delegate to services when logic is non-trivial.

Response shape

Use a consistent JSON shape so clients and agents can rely on it:
ScenarioShape
Successres.status(200).json({ success: true, data?: any, message?: string })
Createdres.status(201).json({ success: true, ... })
Client errorres.status(4xx).json({ success: false, message: string, code?: string })
Server errorres.status(500).json({ success: false, message: string })
Auth middlewares already use success, message, and code. Use code for stable client handling (e.g. NO_TOKEN, TOKEN_EXPIRED, INVALID_TOKEN).

Protected vs public

  • Protected: Put verifyToken (and optionally authorizeRoles or org permission middlewares) in the route chain. Then you can use req.user and getModels(req, ...).
  • Public: Do not use verifyToken. For optional auth (e.g. personalized data when logged in), use verifyTokenOptional.

Services

  • Location: Meridian/backend/services/
  • Role: Encapsulate business logic, external APIs, and shared helpers. Keep route handlers focused on HTTP and validation.
  • DB access: If a service needs DB, the caller (route or other service) must pass req. The service then calls getModels(req, 'ModelName', ...) and uses the returned models. Example: userServices.js, studySessionService.js.
  • No req in context: For background jobs or scripts without req, obtain a Mongoose connection and pass something that has a db property (or refactor to accept a connection/model factory) so tenant and model resolution still work. Do not introduce a global default connection for app data.

Events submodule

  • Mount: Meridian/backend/events/index.js is required in app.js and mounted as app.use(eventsRoutes).
  • Routes: Under Meridian/backend/events/routes/ (e.g. eventSystemConfigRoutes.js, analyticsRoutes.js). These are aggregated in events/index.js.
  • Schemas: Event-related schemas live in Meridian/backend/events/schemas/. They are required and registered in getModelService.js; event routes and middlewares use the same getModels(req, ...) pattern.
When adding event features: add schemas under events/schemas/, register in getModelService.js, add routes under events/routes/, then mount in events/index.js if needed.

Testing

The backend uses a three-layer testing framework (Jest + Supertest + mongodb-memory-server). See Meridian/docs/TESTING_FRAMEWORK.md for full details.

Layout

LayerLocationPurpose
Unitbackend/tests/unit/**/*.test.jsPure utilities (e.g. semesterHelpers, timeBlockHelper).
Integrationbackend/tests/integration/**/*.test.jsHTTP route wiring, middleware behavior (e.g. /health).
Route outcomesbackend/tests/route-outcomes/**/*.test.jsEnd-to-end API correctness against in-memory MongoDB.

Commands

From repo root:
  • npm run test:backend — backend coverage
  • npm run test:ci — backend + frontend (CI gate)
From backend:
  • npm --prefix backend run test:unit
  • npm --prefix backend run test:integration
  • npm --prefix backend run test:routes — route-outcome tests
  • npm --prefix backend run test:coverage

Multi-tenant route-outcome tests

Route-outcome tests must set req.db, req.school, and (when applicable) req.globalDb so handlers get the correct tenant context:
app.use((req, _res, next) => {
  req.db = mongo.connection;
  req.globalDb = mongo.globalConnection;  // when route uses authGlobalService
  req.school = 'rpi';
  next();
});
For auth routes that call authGlobalService (e.g. register, login):
  • Use createMongoMemoryConnection({ withGlobalDb: true }).
  • Set req.globalDb = mongo.globalConnection.
  • Mock createGlobalSession in sessionUtils (tests use in-memory DB, not global Session persistence).
When adding new route-outcome tests (e.g. for Events-Backend routes mounted via backend/events), mock backendRoot for verifyToken, requireAdmin, and getModelService; use modulePaths: ['<rootDir>/node_modules'] in Jest so Events-Backend files resolve backend deps correctly.

Permissions constants

  • File: Meridian/backend/constants/permissions.js
  • Exports: ORG_PERMISSIONS, EVENT_PERMISSIONS, USER_PERMISSIONS, SYSTEM_PERMISSIONS, PERMISSION_GROUPS, PERMISSION_DESCRIPTIONS, plus helpers like getPermissionDescription, validatePermission.
  • Usage: Use these constants instead of string literals when checking or assigning permissions (e.g. in requireOrgPermission(ORG_PERMISSIONS.MANAGE_EVENTS)). This keeps permission names consistent and refactor-safe.

Conventions summary

ConcernRule
DB / modelsAlways getModels(req, 'ModelName', ...) with the request-scoped req. Never use global mongoose for app data.
TenantUse req.db and req.school set by app middleware; do not infer tenant elsewhere.
AuthUse verifyToken for protected routes; verifyTokenOptional for optional auth.
RolesUse authorizeRoles(...) after verifyToken for role checks.
Org permissionsUse requireOrgPermission / requireAnyOrgPermission / requireOrgOwner from orgPermissions.js with constants from constants/permissions.js.
ResponsesUse { success, message?, code?, data? } JSON and appropriate status codes.
New modelAdd schema, then register in getModelService.js with req.db.model('Name', schema, 'collectionName').
New school/tenantAdd mapping in connectionsManager.js getDbUriForSchool and corresponding env var.

Known inconsistencies and gotchas

Be aware of these when editing the backend.
  • Duplicate key in getModelService: The models object in getModelService.js defines EventAnalytics twice (same schema/collection). Prefer a single entry to avoid confusion.
  • Token expiry: verifyToken.js and authRoutes.js use different ACCESS_TOKEN_EXPIRY values (e.g. 15m vs 1m). Align these in one place (e.g. a shared auth constants file) so middleware and token issuance stay in sync.
  • verifyTokenOptional: Uses a callback-style jwt.verify and calls next() from inside the callback; ensure next() is never called twice (e.g. on refresh path) to avoid double-response issues.
  • StudySession service: studySessionService.js expects req and calls getModels(req, ...). Any caller must pass the real req from the route.

Quick reference: key files

PurposeFile(s)
App entry, CORS, DB middleware, route mountingMeridian/backend/app.js
Per-tenant DB connectionsMeridian/backend/connectionsManager.js
Request-scoped modelsMeridian/backend/services/getModelService.js
JWT authMeridian/backend/middlewares/verifyToken.js
Org permission checksMeridian/backend/middlewares/orgPermissions.js
Permission constantsMeridian/backend/constants/permissions.js
Route definitionsMeridian/backend/routes/*.js, Meridian/backend/events/routes/*.js
Event route mountingMeridian/backend/events/index.js
Mongoose schemasMeridian/backend/schemas/*.js, Meridian/backend/events/schemas/*.js
Test helpers, unit, integration, route-outcomesMeridian/backend/tests/, Meridian/docs/TESTING_FRAMEWORK.md

Authentication overview

Login methods, JWT access/refresh, web cookies vs mobile headers, and admin MFA.

Multi-tenant identity & SSO

Global users, memberships, platform roles, and per-request req.user resolution.

Session management

Global sessions, device metadata, and revoke endpoints used after login.

SAML

Institution SSO per school and how it issues the same token model.

Multi-tenant test scenarios

Route-outcome tests and tenant isolation patterns for auth-aware code.

Testing

Backend test layout, CI jobs, and conventions aligned with this repo.

Atlas backend

Org and event routes; pairs with getModels and permission middleware.

Atlas architecture

How Atlas is wired: models, routes, permissions, and UIs.

Atlas permissions

Org roles, permission constants, and orgPermissions usage.

Web client best practices

useFetch, postRequest, routing, and styling next to these APIs.