This document describes how the server-side code of the main app (apps/meteor/) is organized, what each folder is responsible for, and — most importantly — how to decide where new code goes. It replaces the migration plan that drove the 2026 restructure (PRs #40259, #41115, #41126, #41155, #41225, #41315, #41381), which moved everything out of the old app/*/server/ feature folders.
server/<responsibility>/<domain>/<file>
Code is grouped by responsibility first (what kind of code it is: an API endpoint, a hook, a domain function, a Meteor method) and by domain second (what feature area it belongs to: rooms, users, messages, omnichannel…). This is the opposite of the old layout, which grouped by feature first (app/livechat/server/…) and made responsibilities (methods, hooks, API, lib) repeat inside every feature.
Practical consequence: when you look for code, ask "what kind of thing is it?" before "what feature is it for?". A livechat REST endpoint is in server/api/v1/omnichannel/, a livechat hook in server/hooks/omnichannel/, a livechat domain function in server/lib/omnichannel/.
| Path | What it is |
|---|---|
server/ |
All community server code. Described in detail below. |
ee/server/ |
Enterprise server code — an exact mirror of server/'s responsibility layout. See License boundary. |
client/, ee/client/ |
Frontend (React) code. |
app/ |
Legacy remnant, do not add server code here. What's left is genuinely shared client+server code (app/*/lib/), client-only feature code (app/*/client/), and app/apps/server/ (Apps-Engine bridges/converters, kept intentionally). |
lib/ |
Small utilities shared between client and server (isomorphic, no Meteor/server imports). |
imports/ |
Legacy Meteor-style feature folders (personal-access-tokens, message-read-receipt client parts). Avoid adding to it. |
packages/ (repo root) |
Workspace packages (@rocket.chat/*). Cross-app logic, typings (core-typings, rest-typings), and services SDK (core-services) live here — prefer a package when code must be shared beyond apps/meteor. |
tests/ |
Test suites — see Tests. |
main.ts— the server entry point. Import order here matters (models → settings definitions → startup →importPackages→ methods → publications).importPackages.ts— the big list of side-effect imports that load every feature at startup. If you create a module that must run at boot and nothing imports it, register it here (or in the closer aggregator:meteor-methods/index.ts,startup/index.ts).models.ts— registers the MongoDB model classes from@rocket.chat/models.
| Folder | Responsibility | Domain layout |
|---|---|---|
api/ |
REST API. ApiClass.ts/router.ts are the Hono-based framework, v1/ holds one file per endpoint group (users.ts, chat.ts, …), v1/omnichannel/ the livechat endpoints, v1/middlewares/ auth/cors/metrics, lib/ request helpers, validation/ the ajv setup, webhooks.ts the /hooks/* incoming-webhook API. Endpoint payload types live in packages/rest-typings. |
|
services/ |
Internal services registered on the @rocket.chat/core-services broker (one folder per service: room/, team/, omnichannel/, federation/, …). Callable cross-process in microservices deployments. Preferred home for new business logic — see the decision guide. |
|
lib/ |
Everything that is a plain function/class library: domain functions and shared server utilities. See server/lib/ domains. |
|
hooks/ |
Event handlers registered on the callbacks system (callbacks.add('afterSaveMessage', …)). Domain subfolders: messages/, rooms/, auth/, omnichannel/; a few cross-cutting ones sit flat (afterUserActions.ts, sauMonitorHooks.ts). A hook file only registers listeners — real logic should live in lib/ or services/ and be called from the hook. |
|
meteor-methods/ |
Deprecated. Meteor DDP RPC handlers, kept only for backward compatibility with existing clients. Domain subfolders (users/, rooms/, messages/, auth/, omnichannel/, settings/, platform/, media/, import/, integrations/). Registration happens via side-effect import from meteor-methods/index.ts — a method file that nobody imports silently stops existing (lint/tsc/tests all stay green), so never remove an import without removing the method. Do not add new methods; add a REST endpoint instead. |
|
publications/ |
Deprecated. Meteor DDP publications. Same story as methods: don't add, use REST + streams. | |
settings/ |
The settings system. index.ts is the registry barrel (settings, settingsRegistry — what everything imports); definitions.ts loads the per-domain setting definition files (accounts.ts, email.ts, omnichannel.ts, …) that call settingsRegistry.addGroup(...); theme/ is the theme compiler; lib/ settings-specific helpers. New setting? Add it to the matching definitions file, not to the registry. |
|
slashcommands/ |
One file per slash command (ban.ts, invite.ts, …). |
|
bridges/ |
Adapters to external systems: irc/, slack/ (slackbridge), smarsh/, webdav/, nextcloud/. Leaf code — imports the core, nothing imports it. |
|
cron/ |
Scheduled jobs. | |
startup/ |
Boot-time configuration and one-off initialization (initialData.ts, rateLimiter.js, robots.js, migrations/). |
|
routes/ |
Non-REST HTTP routes (avatar serving, etc.). | |
database/ |
Mongo connection utilities (trash, readSecondaryPreferred, transaction helpers). |
|
modules/ |
Larger self-contained subsystems (core-apps, listeners, notifications, streamer). | |
features/ |
Feature-flag style subsystems (e.g. EmailInbox/). |
|
configuration/ |
Runtime configuration glue (OAuth, CAS, LDAP wiring). | |
email/, ufs/, oauth2-server/, deasync/ |
Infrastructure kept as-is: mailer transport, Upload-File-System storage engine, OAuth2 provider implementation, deasync shim. |
lib/ is the largest folder. Its immediate children are domains (plus a handful of flat cross-cutting utilities like callbacks.ts, i18n.ts, notifyListener.ts, RateLimiter.js, debug.js, bugsnag.ts, deprecationWarningLogger.ts, validateEmailDomain.js, defaultBlockedDomainsList.ts).
| Domain | Contents |
|---|---|
users/ |
User domain functions: deleteUser, setRealName, saveUserIdentity, getFullUserData, availability checks, status/ (custom user status)… |
rooms/ |
Room domain functions: createRoom, deleteRoom, addUserToRoom, roomCoordinator, plus settings/ (save-room-* functions), invites/, retention/ (retention-policy cron). |
messages/ |
Message domain functions: sendMessage, insertMessage, deleteMessage, updateMessage, loadMessageHistory… |
messaging/ |
Messaging features around messages: threads/, discussions/, reactions/, pins/, stars/, unread/, mentions/, markdown/, emoji, msgStream, Message.ts (formatting), getHiddenSystemMessages, validateCustomMessageFields. Rule of thumb: messages/ = lifecycle of a message document; messaging/ = features built on top of messages. |
omnichannel/ |
The livechat/omnichannel engine: QueueManager, RoutingManager (+ routing/ strategies), business-hour/, contacts/, analytics/, transcripts, visitors, guests, departments… The biggest domain. |
authorization/ |
Permission/role checks: hasPermission, canAccessRoom, getRoles, constant/permissions.ts (the permission list), streamer/ (permission change notifications). |
auth/ |
Authentication utilities: passwordPolicy, generatePassword, login attempt logging/restriction, oauth2-server/ (OAuth app admin), token-login. |
auth-providers/ |
One folder/file per external login provider: apple/, crowd/, custom-oauth/, oauth/ (core OAuth glue), github.ts, gitlab.ts, google.js, wordpress.ts, … |
saml/, cas/, ldap/, 2fa/, e2e/ |
Protocol-specific auth/crypto subsystems. |
notifications/ |
Notification delivery: push/, push-config/, email/ (Mailer API), mail-messages/, queue/, core/ (Notifications streams), message/ (per-message desktop/email/mobile decision helpers), direct-reply email processing. |
media/ |
Files and media: file-upload/ (FileUpload + storage configs), file/, emoji-custom/, emoji-native/, custom-sounds/, assets/. |
import/ |
Importer framework (classes/, converters) + one folder per importer (csv/, slack/, slack-users/, omnichannel-contacts/, pending-avatars/, pending-files/). |
integrations/ |
Incoming/outgoing webhook integrations engine (the REST surface is server/api/webhooks.ts). |
cloud/ |
Rocket.Chat Cloud connectivity, version-check/, license sync. |
search/, autotranslate/, statistics/, metrics/, moderation/, dataExport/, oauth/, roles/, bot-helpers/, cors/, error-handler/, ui-master/ |
Smaller single-purpose domains, named after what they do. |
utils/ |
Generic server utilities without a domain (getURL, slashCommand registry, restrictions, JWT helper, timezone…). If a function clearly belongs to a domain above, it does not go here. |
shared/ |
Tiny validators shared across domains (validateName, validateNameChars…). |
ee/server/ mirrors the same responsibility layout: api/, hooks/, lib/ (with omnichannel/, license/, ldap/, canned-responses/, abac/, audit/, …), meteor-methods/, settings/, cron/, models/, patches/, startup/, configuration/, and local-services/ (EE internal services — note: ee/server/services/ is docker/build scaffolding for the microservices images, not a code folder).
The directory boundary is the license boundary. Code under an ee/ path is governed by the Enterprise license (apps/meteor/ee/LICENSE); everything else is community-licensed:
- Never move a file across the
ee/boundary in either direction — that silently relicenses it. - Imports may cross the boundary (community code imports EE modules behind license checks and vice versa); files may not.
- EE features are gated at runtime via
License.onLicense(...)/ license modules — moving a file intoee/is not what gates it, but EE-licensed code must live there. - Standalone EE workspaces (
ee/apps/*,ee/packages/*at the repo root) and EE subtrees inside packages (packages/*/src/ee/) are separate fromapps/meteor/ee/.
- A REST endpoint →
server/api/v1/<group>.ts(omnichannel ones inv1/omnichannel/), payload schemas inpackages/rest-typings. This is the default way to expose anything to clients. - Business logic → prefer a service in
server/services/(registered via@rocket.chat/core-services) when the logic has a clear service boundary or must be callable cross-process. Plain domain functions inserver/lib/<domain>/are fine for logic tied to one domain that services and endpoints compose. Long-term direction: business logic trends towardservices/;lib/<domain>/is the pragmatic middle ground. - Reacting to an event ("when a message is saved…", "after a user logs in…") →
server/hooks/<domain>/. Keep the hook file thin; put the logic inlib//services/. - A Meteor method or publication → don't. Both are deprecated; expose a REST endpoint (+ streamer event if clients need pushes). If you must touch an existing method, it lives in
server/meteor-methods/<domain>/and must stay imported bymeteor-methods/index.ts. - A setting → the matching definitions file under
server/settings/. - A slash command →
server/slashcommands/. - A scheduled job →
server/cron/. - Startup-only wiring →
server/startup/(and an import inmain.ts/importPackages.tsif nothing else loads it). - Code shared with the frontend →
apps/meteor/lib/for app-local isomorphic helpers, or apackages/*package if shared beyond the app. Never import server code from client code. - Enterprise-only → same decision tree inside
ee/server/, gated by license checks. - A new domain? If your code doesn't fit an existing
lib/domain, create a newserver/lib/<domain>/folder rather than dropping files flat inlib/or stretchingutils/. Name it after the business concept, not the implementing tech.
Anti-patterns to avoid:
- Adding server code under
app/(the folder is frozen; only shared/client code remains there). - Barrel
index.tsfiles that just re-export — import the concrete file. (Existing barrels likesettings/index.tsandauthorization/index.tsare legacy API surfaces, kept until their consumers are rewritten.) - Putting logic in a hook/method/endpoint file instead of
lib/services— those files should be thin entry points. - "Utility" dumping: if it has a domain, it goes in the domain.
- Co-located specs (
server/**/**.spec.ts) and mirrored specs (tests/unit/server/**mirrors the source tree; EE code is mirrored underee/tests/unit/**) both exist. When you move source, move its mirror. - Two runners with separate globs: mocha (
.mocharc.jsspeclist) and jest (jest.config.tstestMatch). A spec in a folder not matched by any glob silently never runs; a jest spec caught by a mocha glob throwsjest is not defined. proxyquire/jest.mockkeys are string literals matched against the loaded module's import specifiers — lint and tsc cannot validate them. If you move a module, every mock key referencing it (or referencing its dependencies' specifiers) must be updated, and the affected suites actually run. A stale key silently loads the real dependency; if that dependency reachesserver/settings/index.ts, the whole mocha run dies with a top-level-await transform error — that error is the signature of a stale settings mock.- Specs must not depend on suite execution order (e.g. on another spec having installed a mocked core-services broker) — mock
@rocket.chat/core-serviceslocally.
The structure above is the result of a 7-phase migration (finished 2026-07) that dissolved the old Meteor-package-style app/*/server/ folders: slash commands (#40259), bridges (#41115), REST API (#41126), domain functions (#41155), meteor-methods (#41225), lib/hooks/feature code (#41315), omnichannel + final cleanup (#41381). Files were moved as-is, so git log --follow works across the moves. The old app/<feature>/server/ path of any file can be found in those PRs' manifests if needed.