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.

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

Multi-Tenant Identity & SSO

Global identity, platform admins, cross-tenant events, SSO, and migration

Atlas Backend

Endpoint map, auth, and request/response shapes for org routes

Atlas Architecture

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

Atlas Permissions

Org roles, permissions, and middleware enforcement

Session Management

Multi-device sessions and token refresh