Skip to main content
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. 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.
Never use a default mongoose connection or mongoose.model() directly for app data. Always use the request-scoped connection and getModelService (see below).

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.

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

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.