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:
  • 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

Example:

orgPermissions.js

Use these for org-scoped actions. They use getModels(req, 'OrgMember', 'Org') and expect req.user (so use after verifyToken). Permission strings must match constants/permissions.js. Use the constants in code:

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: 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

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:
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

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

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.