Audience: Developers and AI agents 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.
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 pages
Backend best practices
Canonical patterns for
getModels, verifyToken, routes, and responses.Multi-tenant identity & SSO
When to use
getGlobalModelService and how req.user is resolved from JWTs.Authentication overview
How login, OAuth, and sessions fit together for correct
req usage in generated code.Testing
How to validate route changes with Supertest and tenant fixtures.
Atlas backend
Org and event HTTP surfaces agents often touch.
Atlas architecture
Product wiring: models, routes, permissions, and UIs.
Atlas permissions
Org roles and middleware enforcement.
Session management
Refresh and multi-device behavior when generating client or server flows.