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.
rpifromrpi.meridian.study). In development,localhostand IP hosts default torpi. - Per-request DB: Every request gets
req.db(a Mongoose connection) andreq.school(subdomain string) from a global middleware inapp.js. - Global DB: Every request also gets
req.globalDbfor cross-tenant data (GlobalUser, PlatformRole, TenantMembership, Session). Use only viagetGlobalModelService(req, ...)in auth and platform-admin logic. See Multi-Tenant Identity & SSO for full architecture.
Database and models
Getting models (required pattern)
Always resolve models throughgetModelService with the current req:
- Why:
req.dbis the tenant-specific Mongoose connection.getModels(req, ...)registers schemas onreq.dband 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 passreqand usegetModels(req, ...)inside the service. - Do not: Use
require('../schemas/user')and thenmongoose.model('User')or any globalmongooseconnection 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
modelsobject: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.jscalls it. The middleware inapp.jssetsreq.db = await connectToDatabase(subdomain)andreq.school = subdomain. - Adding a school: Extend the
schoolDbMapingetDbUriForSchool()with the new subdomain and env var (e.g.MONGO_URI_<SCHOOL>). Fallback isDEFAULT_MONGO_URI.
Auth and middlewares
Order of use
Typical order on a route: auth first, then org/permission (if needed), then handler.verifyTokenorverifyTokenOptionalmust run before any middleware or handler that usesreq.user.- Org middlewares (
requireOrgPermission, etc.) expectreq.userand usegetModels(req, ...); they must run afterverifyToken.
verifyToken.js
Example:
orgPermissions.js
Use these for org-scoped actions. They usegetModels(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 underMeridian/backend/events/routes/for event features). - Each file exports an Express
Router. Mount inapp.js(or inevents/index.jsfor 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 optionallyauthorizeRolesor org permission middlewares) in the route chain. Then you can usereq.userandgetModels(req, ...). - Public: Do not use
verifyToken. For optional auth (e.g. personalized data when logged in), useverifyTokenOptional.
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 callsgetModels(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 adbproperty (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.jsis required inapp.jsand mounted asapp.use(eventsRoutes). - Routes: Under
Meridian/backend/events/routes/(e.g.eventSystemConfigRoutes.js,analyticsRoutes.js). These are aggregated inevents/index.js. - Schemas: Event-related schemas live in
Meridian/backend/events/schemas/. They are required and registered ingetModelService.js; event routes and middlewares use the samegetModels(req, ...)pattern.
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). SeeMeridian/docs/TESTING_FRAMEWORK.md for full details.
Layout
Commands
From repo root:npm run test:backend— backend coveragenpm run test:ci— backend + frontend (CI gate)
npm --prefix backend run test:unitnpm --prefix backend run test:integrationnpm --prefix backend run test:routes— route-outcome testsnpm --prefix backend run test:coverage
Multi-tenant route-outcome tests
Route-outcome tests must setreq.db, req.school, and (when applicable) req.globalDb so handlers get the correct tenant context:
authGlobalService (e.g. register, login):
- Use
createMongoMemoryConnection({ withGlobalDb: true }). - Set
req.globalDb = mongo.globalConnection. - Mock
createGlobalSessioninsessionUtils(tests use in-memory DB, not global Session persistence).
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 likegetPermissionDescription,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
- Duplicate key in getModelService: The
modelsobject ingetModelService.jsdefinesEventAnalyticstwice (same schema/collection). Prefer a single entry to avoid confusion. - Token expiry:
verifyToken.jsandauthRoutes.jsuse differentACCESS_TOKEN_EXPIRYvalues (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.verifyand callsnext()from inside the callback; ensurenext()is never called twice (e.g. on refresh path) to avoid double-response issues. - StudySession service:
studySessionService.jsexpectsreqand callsgetModels(req, ...). Any caller must pass the realreqfrom the route.
Quick reference: key files
Related pages
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.