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
Add schema
Add a schema under
Meridian/backend/schemas/ (or Meridian/backend/events/schemas/ for event-related models).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').
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
| Middleware | Behavior |
|---|---|
| verifyToken | Reads 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'. |
| verifyTokenOptional | Same 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. |
orgPermissions.js
Use these for org-scoped actions. They usegetModels(req, 'OrgMember', 'Org') and expect req.user (so use after verifyToken).
| Middleware | Purpose |
|---|---|
| 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 wrappers | requireRoleManagement, requireMemberManagement, requireEventManagement, requireAnalyticsAccess, requireEquipmentManagement, requireEquipmentModification — all take (orgParam = 'orgId'). |
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:| Scenario | Shape |
|---|---|
| Success | res.status(200).json({ success: true, data?: any, message?: string }) |
| Created | res.status(201).json({ success: true, ... }) |
| Client error | res.status(4xx).json({ success: false, message: string, code?: string }) |
| Server error | res.status(500).json({ success: false, message: string }) |
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.
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
| Concern | Rule |
|---|---|
| DB / models | Always getModels(req, 'ModelName', ...) with the request-scoped req. Never use global mongoose for app data. |
| Tenant | Use req.db and req.school set by app middleware; do not infer tenant elsewhere. |
| Auth | Use verifyToken for protected routes; verifyTokenOptional for optional auth. |
| Roles | Use authorizeRoles(...) after verifyToken for role checks. |
| Org permissions | Use requireOrgPermission / requireAnyOrgPermission / requireOrgOwner from orgPermissions.js with constants from constants/permissions.js. |
| Responses | Use { success, message?, code?, data? } JSON and appropriate status codes. |
| New model | Add schema, then register in getModelService.js with req.db.model('Name', schema, 'collectionName'). |
| New school/tenant | Add mapping in connectionsManager.js getDbUriForSchool and corresponding env var. |
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
| Purpose | File(s) |
|---|---|
| App entry, CORS, DB middleware, route mounting | Meridian/backend/app.js |
| Per-tenant DB connections | Meridian/backend/connectionsManager.js |
| Request-scoped models | Meridian/backend/services/getModelService.js |
| JWT auth | Meridian/backend/middlewares/verifyToken.js |
| Org permission checks | Meridian/backend/middlewares/orgPermissions.js |
| Permission constants | Meridian/backend/constants/permissions.js |
| Route definitions | Meridian/backend/routes/*.js, Meridian/backend/events/routes/*.js |
| Event route mounting | Meridian/backend/events/index.js |
| Mongoose schemas | Meridian/backend/schemas/*.js, Meridian/backend/events/schemas/*.js |
Related docs
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