Customization Architecture — the DI + overlay + hook engine

How FaceKom turns one core product into N per-client deployments without forking the core into N codebases — at least, that’s the intent. There are three cooperating mechanisms, all anchored on a single shared object called the service container:

  1. DI / service container — one process-wide singleton (serviceContainer) that every module mutates and reads. Carries serviceContainer.service.* (core services), serviceContainer.customizations.* (custom services), .queue, .db, .logger, etc.
  2. Hook + override registry (the ServiceBus) — a WildEmitter subclass on serviceContainer.emitter that adds addHook/callHooks/callOnlyHook (lifecycle + extension points) and registerOverride/callOverride (default-with-fallback behaviour swaps).
  3. The customization/ overlay tree — a parallel directory (api, email, cron, cv, assets, db, flow, ocr, sms, portal, server, ui, user, listeners, overrides, …) whose files are loaded at boot, both by convention paths hard-coded into core and by an entrypoint customization() function.

A customization/<client> Git branch then layers partner code into that overlay (and, in practice, also rewrites core files — see §7, the upgrade risk). This doc documents the engine from vuer_oss source; vuer_css and the esign_* repos mirror it (§8). Branch inventory lives in customization-branch-catalog; repo overview in vuer_oss §- the customization callouts.

See also: INDEX · architecture-overview · vuer_oss · vuer_css · customization-branch-catalog


1. The service container (serviceContainer)

vuer_oss/server/service_container.js is the module the whole app shares. It exports exactly one thing — an emitter:

// server/service_container.js:81-83
module.exports = {
  emitter: new ServiceBus()
}

Everything else on serviceContainer is mutated onto this object at boot by other modules. server.js is where the canonical fields get attached:

// server.js:17-23
const serviceContainer = require('./server/service_container')
serviceContainer.logger = logger
serviceContainer.socketTokenStorage = new SocketTokenStorage()
serviceContainer.auditLog = require('./server/auditlog')
serviceContainer.diagnostic = new Diagnostic()
serviceContainer.communicationLogService = new CommunicationLogService()
serviceContainer.clientErrorLogService = new ClientErrorLogService()

Because require() caches modules, every file that does require('./server/service_container') gets the same object — that’s the DI mechanism. There is no IoC framework; it’s a hand-rolled singleton service locator. Core services are hung off serviceContainer.service.* inside each entrypoint’s setupServices() (e.g. server.js:29-340 instantiates ~100 services — 97 distinct serviceContainer.service.* keys assigned, 93 of them = new …). Custom services are hung off serviceContainer.customizations.*.

Two parallel namespaces. serviceContainer.service.* = core services (always present). serviceContainer.customizations.* = customization-supplied services/queue members/cron jobs. Each per-process customization file starts by resetting serviceContainer.customizations = {} (e.g. customization/customization.js:4, also re-set in cron.js:14, media.js:15, integrationLog.js:15).


2. The ServiceBus — hook emitter + override registry

ServiceBus extends WildEmitter and adds two independent registries (server/service_container.js:1-79):

2a. Hook registry — addHook / callHooks / callOnlyHook

A Map<commandTag, handler[]> (this._commandMap). Multiple handlers can register under one tag.

MethodBehaviourSource
addHook(tag, handler)Append handler to the array for tag (creates it if absent).:27-36
removeHook(tag, fn)Splice a handler out.:38-46
callHooks(tag, ...params)Promise.all of every handler for tag (returns resolved Promise if none). Fan-out, fire-all.:48-55
callOnlyHook(tag, ...params)Calls only the first handler synchronously and returns its value; rejects if the first provider isn’t a function; resolves if none registered. “Handy if you know there’s only one hook … and you care about its result.”:57-74
hasHook(tag)Map membership test.:76-78

callHooks = broadcast extension points (listeners observe/mutate-in-place, e.g. access filters, lifecycle events). callOnlyHook = single-provider “ask the customization for a value” (e.g. self-service:v2:* step logic, email:send, sms:send, cv:setup). The core fires hundreds of named tags; e.g. git grep -E "callHooks\(|callOnlyHook\(" over server/ shows tags like document:virusScan (14 call sites), the whole self-service:v2:* family (~80 tags), customer:beforeCreate, room:created, users.list, auditlog, routes, socket, media_routes, cv:setup, webserver:setup, etc.

2b. Override registry — registerOverride / callOverride

A separate Map<overrideName, fn> (this._overrides). At most one override per name.

// server/service_container.js:10-25
registerOverride (overrideName, overrideFn) {
  if (this._overrides.has(overrideName)) {
    throw new Error(`Duplicate override handler: '${overrideName}'`)   // hard-fail on collision
  }
  this._overrides.set(overrideName, overrideFn)
}
 
callOverride (overrideName, ...args) {
  const defaultOverride = args.pop()                 // LAST arg is the core default fn
  const overrideFn = this._overrides.get(overrideName)
  if (typeof overrideFn === 'function') {
    return overrideFn(...args)                        // customization wins…
  } else if (typeof defaultOverride === 'function') {
    return defaultOverride(...args)                   // …else run the core default
  }
}

This is the strategy / default-with-fallback pattern: core code always passes its own default implementation as the final argument, so the app works with zero customizations, but a partner can registerOverride('<name>', fn) to replace just that one decision. Core call sites (git grep callOverride over server/ on devel, ~60 sites — 62 exact) include:

  • JanusService.js:12,24,35get-janus-server-for-room / …-selfserviceroom / …-echotest (which media server a room lands on).
  • CustomerService.js:374,437 / DeviceChangeService.js:70customer:inviteEmail / customer:inviteSms / customer:deviceChangeEmail.
  • server/web/routes.js:49,105,114routes:homepage / routes:after-call-redirect / routes:customer-list (swap whole page handlers).
  • WebServerAuth.js:286saml:config. room.endpoint.js:170,195screenshot:downloadName / room:getExtraData.
  • CallsReportService.js — ~20 reportCalc:* / reportCallsDownload:rows overrides (per-partner report maths).
  • OnlinePresenceService.js:92,290autoDistribution:gracePeriodInSec.

Override names are a flat global namespace and collisions throw at registration (:11-13). Two customization modules registering the same override name will crash boot. Hooks (addHook) do not collide — they stack.


3. The boot order (why load-order matters)

Every long-running process follows the same chain. The crucial property: the customization file is required early (so its addHook/registerOverride calls run and populate the registries), but the core fires the hooks later during setupServices/setupQueue/etc., and finally calls the exported customization() to pull in listeners/overrides/APIs.

server.js boot chain (server.js:728-744):

config.loaded
  .then(setupServices)        // fires callHooks('services')  (server.js:337) + callOnlyHook('cv:setup') (server.js:319)
  .then(() => require('./server/bootstrap/connection/db')(true))   // only server.js migrates
  .then(() => require('./server/bootstrap/connection/redis')())
  .then(() => require('./server/bootstrap/connection/rabbitmq')())
  .then(initServices)
  .then(setupQueue)           // fires callHooks('queue', connectionPool)  (server.js:591)
  .then(() => customization()) // runs the customization() module body (listeners/overrides/apis/flows/content)
  .then(StartWebServer)       // WebServer → routes.js fires callHooks('routes', app, {csrfProtection,isAllowed,…}) + callHooks('webserver:setup')
  .then(StartSocketServer)    // socket-server fires callHooks('socket', modules, restrictedModules)
  .then(() => serviceContainer.emitter.emit('system.start'))
  .catch(err => { logger.error('CANNOT START VUER_OSS', err); process.exit(2) })

The customization = require('./customization/customization') at server.js:27 is what registers the hooks before setupServices runs. cron.js makes this explicit with a comment:

// cron.js:16-17
// Require outside setupCustomizations to register hooks in time!
const customization = require('./customization/cron-customization')

Load-order is load-bearing. The customization/*-customization.js module must be required before setupServices() so its addHook('services', …) handler exists when callHooks('services') fires; and customization() is called last (after queue setup) to require the listeners that addHook the runtime event tags. Re-order these and hooks silently don't fire.

Two phases inside one customization file

Each *-customization.js does work in two phases:

  1. Module top-level (runs at require time): serviceContainer.emitter.addHook('services'|'queue'|'cron'|'routes'|'socket'|'cv:setup'|'auditlog'|'webserver:setup', …). These handlers construct custom services/queue members/cron jobs when the core later fires the matching tag.
  2. module.exports = () => Promise… (runs when customization() is called): chains .then() blocks that require('./listeners/...'), require('./overrides/...'), require('./api/...'), register flow handlers, and load template content. Requiring a listener file is what executes its top-level addHook/registerOverride. (See customization/customization.js:115-260.)

4. The per-process customization entrypoints

There is one customization file per OSS process (all under customization/, all git ls-tree devel:customization):

Process (entry)Customization filerequired atHooks it registers (top-level)
server.jscustomization/customization.jsserver.js:27queue, services, routes, rab:routes, socket, webserver:setup, cv:setup, auditlog
background.jscustomization/background-customization.jsbackground.js:13queue, services, rab:routes
cron.jscustomization/cron-customization.jscron.js:17queue, cron, services
convert.jscustomization/convert-customization.jsconvert.js:12queue, services
media.jscustomization/media-customization.jsmedia.js:17queue, services, media_routes, webserver:setup
storage.jscustomization/storage-customization.jsstorage.js:12services, queue

integrationLog.js has no customization file — it requires none and never calls customization() (git grep customization integrationLog.js → only the serviceContainer.customizations = {} reset at :15). It still fires callHooks('services')/callHooks('queue'), so nothing is registered for it.

Each module.exports is an idempotent async factory returning a Promise chain. customization/customization.js (the server.js one) is by far the largest: its body requires ~55 listeners (./listeners/*), several overrides (./overrides/*), the SMS+email APIs (./api/sms/SmsApiSmsApiCom, ./api/e-mail/SMTPEmailApi), flow handlers (serviceContainer.service.flow.registerFlowHandlers()), and template-content modules — many gated on config.get('features.*') (e.g. password-feedback, storage, verify-password-history). The 'services' hook in it news up CustomFlowService, CustomArchiveService, PartnerServiceLogicFactory, CustomCustomerDeleteService, CustomDashboardService, CustomSelfServiceRoomService, CustomCustomerListService, CustomFlowListService onto serviceContainer.customizations.service.* (customization/customization.js:19-37).

The 'cv:setup' hook (customization.js:71-110) is a callOnlyHook (single provider) — it builds serviceContainer.service.cvServices[] (one VuerCVService per config.get('hosts.cvs') entry) and sets serviceContainer.service.cvHandler = VuerCVService; it throws 'All CV services are unavailable!' if none are reachable, and logs a deprecation warning about backwards-compatible single-CV mode (PR #1199). The 'webserver:setup' hook adds a firewall-bypass rule for /api/admin/room/delete/bulk (customization.js:64-68).


5. The customization/ overlay tree

git ls-tree devel:customization — a parallel mirror of the core layout. Per-subdir file counts (devel, git ls-tree -r):

Overlay dirFilesLoaded byWhat it overlays
ui/199bin/script/script.task.js:15-25, bin/style/style.task.js:23-33 (esbuild + Stylus glob `customization/ui/pageslayouts/**`)
test/84Jest/PlaywrightCustomization tests.
email/71convention path in server/e-mail/EmailService.js:15 + server/e-mail/Letter.js:36-39One folder per letter type (e-mail-invite, e-mail-verification, …) with .letter.{data.js,template.twig,plain.twig,style.styl,trans.js}.
listeners/55require('./listeners/*') inside customization()Modules that addHook(...) runtime event tags.
flow/48convention path FlowHandler.js:16-17, FlowService.js:24; trans via Flow.trans.js:1,723One folder per flow (<name>/<name>.flow.{handler,proto,trans}.js) + flowStatProviders.js.
ocr/40convention path OCRRecognitionClient.js:14, MRZService.js:30-35Document validators (ocr/documents/<type>, ocr/mrz/{TD1,TD2,TD3,…}).
server/32mix of 'services' hook + convention pathsserver/service/Custom*Service.js, server/db/model/customerhistory.js, server/sancionedPersonSearcher/*, server/backgroundProcess/*.process.js, server/web/routes/*.endpoint.js + *.content.js.
features/16(config/feature assets)Feature definitions.
sms/13convention path SmsMessage.js:34-35, SmsService.js:12sms/<type>/<type>.sms.{data.js,template.twig,trans.js}.
cv/10convention path RecognitionService.js:12 (require('../../customization/cv/instruction.index'))instruction.index.js + instructions/built-in-* (per-document CV recipes).
translations/9convention path CallsReportService.js:86 etc.i18n dictionaries.
portal/7convention path in many core filesPortalData.js, PortalData.trans.js, LegacyPortalDataHelpers.js — the partner’s customer-data schema (see warning below).
overrides/6require('./overrides/*') inside customization()Modules that registerOverride(...).
cron/4'cron' hook in cron-customization.jsCustomerDeleteCronJob, AutoCloseRoomsCronJob, FlowClearCronJob, DeleteRoomsCronJob (each gated on config).
api/4require('./api/...') inside customization()api/e-mail/{SMTPEmailApi,MockEmailApi}.js, api/sms/{SmsApi,SmsApiSmsApiCom}.js (the actual SMTP/SMS transport impls).
user/3convention path UserService.js:4, db/model/user.js:5-6, newuser/edituser.endpoint.jsUserData.js, UserScopes.js, UserData.trans.js.
assets/3static / SAMLimage/*.png (logos), saml/AuthnRequestTemplate.xml.
db/1convention path server/db/translations/activity.trans.js:333db/translations/activity.trans.js.
docs/1bin/validate-config.js:5,26,41docs/config/custom.json + docs/config/schemas/ — extends the AJV config schema.
settings/1require('./settings/localJsonHasher') (customization.js, gated systemSettings.localJsonHashing)Settings hashing.
resources/1resources
service/1
customization-maintenance/, customization-security/1 eachbundled into docs PDF (docs.json)Dated changelog notes (mirror root maintenance/,security/).

Two loading mechanisms (both real, both in core)

  1. Convention-path loading (core hard-codes the overlay path). Core files require()/path.join() straight into customization/.... This is mandatory — the overlay folder must exist or core throws. Verified examples (git grep -E "customization/(flow|cv|...)" devel -- server/ engines/ bin/):
    • server/e-mail/EmailService.js:15require('../../customization/email/${letterType}/${letterType}.letter.data')
    • server/sms/SmsService.js:12require('../../customization/sms/${type}/${type}.sms.data')
    • server/flow/FlowService.js:24require('../../customization/flow/${type}/${type}.flow.handler')
    • server/ocr/mrz/MRZService.js:30-35 → registers customization/ocr/mrz/{PassportBooklet,TD1,TD2,TD3,VisaMRV-A,VisaMRV-B}
    • server/service/RecognitionService.js:12require('../../customization/cv/instruction.index')
    • server/db/model/customer.js:1, user.js:5-6, customerhistory.js:2 → require customization/portal/PortalData, customization/user/UserData|UserScopes, customization/server/db/model/customerhistory
    • server/service/BackgroundProcessService.js:56-57customization/server/backgroundProcess/${name}.process
    • server/service/ImportService.js:5-9 → 5× customization/server/service/Custom*ImportService
    • bin/validate-config.js:5,26,41 → imports customization/docs/config/custom.json + schemas dir
  2. Entrypoint customization() + hooks. Everything in listeners/, overrides/, api/, and the 'services'/'cron'/'cv:setup' hook bodies, pulled in when customization() runs (§3–4).

customization/portal/PortalData.js is effectively a required core dependency. It's required by server/db/model/customer.js:1, server/db/helpers.js:8, PortalService.js:2, PortalDataRPCServer.js:3, AppointmentService.js, half a dozen *.endpoint.js, and several bin/ tools. The customer-data schema lives in the overlay, so OSS cannot boot without a customization/portal/PortalData.js. devel ships a default; partners override it.

Representative listener (hook consumer) — customization/listeners/access-resolver.js

// customization/listeners/access-resolver.js:1-20 (excerpt)
const serviceContainer = require('../../server/service_container')
 
serviceContainer.emitter.addHook('rooms.list.other', async (user, sessionRole, where) => {
  // it is expected to mutate the where param here or return nothing
})
serviceContainer.emitter.addHook('users.list', async (user, sessionRole, query) => {
  // it is expected to return with additional where params as object
  return {}
})

On devel these are empty stubs (no-op extension points) — partners fill them in on their branch. Loaded because customization/customization.js does require('./listeners/access-resolver').

Representative override (override consumer) — customization/overrides/screenshot-download-name.js

// customization/overrides/screenshot-download-name.js (full)
const serviceContainer = require('../../server/service_container')
 
serviceContainer.emitter.registerOverride('screenshot:downloadName', (attachments, room, customer) => {
  for (const attachment of attachments) {
    attachment.downloadName = `${attachment.content.screenshotCategory || 'screenshot'}_room-${room.id}.png`
  }
  return `YYYYMMDD[_${room.id}.pdf]`
})

Matches the core call site server/web/routes/room.endpoint.js:170 (and flow.endpoint.js:168, importDataRoom.endpoint.js:56): serviceContainer.emitter.callOverride('screenshot:downloadName', attachments, room, room.customer) — the override wins, otherwise core’s default (the last arg) runs.


6. How a customization/<client> branch extends devel

There are 242 customization/* branches on origin (git for-each-ref refs/remotes | grep -c 'customization/'). The model:

  • devel is the core product (origin/HEAD -> origin/devel). It ships the overlay tree populated with defaults/stubs/examples (so devel itself boots and the e2e suite runs).
  • A partner deployment lives on a long-lived customization/<client> branch (e.g. customization/dap, customization/nusz, customization/allianz, customization/cetelem, customization/binance, …; many feature branches customization/FKITDEV-####-... exist for in-flight work — see customization-branch-catalog).
  • The branch diverges from devel by editing the overlay tree (filling in listeners/*, overrides/*, portal/PortalData.js, email/*, flow/*, partner logos in assets/, partner config in config/*.json, partner schemas in docs/config/custom.json).
  • Releases are cut from the partner branch; merging devel forward into each partner branch is the upgrade path.

Concrete divergence (verified on customization/dap):

merge-base(dap, devel) = 5d3ba13a6c…   devel ahead of merge-base: 11   dap ahead: 465 commits
git diff origin/devel...origin/customization/dap  →  247 files changed   (--shortstat / --name-status, == git diff <merge-base> origin/customization/dap)
  →  212 under customization/   (the intended overlay edits)
  →   35 OUTSIDE customization/ (CORE files)   ← the problem

7. The core-file-override upgrade risk

The overlay/hook/override engine is designed so partners only touch customization/. In reality, partner branches also rewrite core files, which is the central maintenance hazard.

On customization/dap, the 35 changed core files (paths not under customization/, --name-status destination paths, de-duplicated) include server.js, background.js, googlePlayIntegrityCheck.js, package.json, config/{dev,docker,roles}.json, client/ui/pages/flow/flow.script.js, client/ui/regions/header/header.styl, server/web/WebServer.js, server/service/{ResourceManagerService,SelfServiceV2Service,DeviceIntegrityCheckService}.js, a cluster of new server/queue/rpc_{client,server}/*Identification* RPC clients/servers + server/queue/rpc_server/GooglePlayIntegrityCheck.js, supervisor_vuer_oss_{dev,docker}.conf, and yarn.lock. The actual server.js diff (git diff origin/devel...origin/customization/dap -- server.js):

@@ const io = require('./server/socket/socket-server')
+const { RPCClient } = require('@techteamer/mq')
@@ function setupServices () {
-    if (config.get('integrityCheck.play.enable')) {
-      const GooglePlayIntegrityCheckService = require('./server/service/IntegrityCheckServices/GooglePlayIntegrityCheckService')
-      serviceContainer.service.googlePlayIntegrityCheckService = new GooglePlayIntegrityCheckService()
-    }
@@
+  if (config.get('resourceManager.active')) {
+    const ResourceManagerService = require('./server/service/ResourceManagerService')
+    serviceContainer.service.resourceManager = new ResourceManagerService()
+  }
@@ function setupQueue () {
+    if (config.get('integrityCheck.play.enable')) {
+      serviceContainer.rpcClient.googlePlayIntegrityCheck = serviceContainer.queue.getRPCClient('rpc-google-play-integrity-check', RPCClient)
+    }
+    // …plus a ~50-line resourceManager.active RPC block wiring RM* identification clients/servers (+ mock servers)

The upgrade risk. Because partner branches edit the same core files the core team edits on devel, every devel change to server.js / background.js / engines/** / bin/** / config/** must be hand-merged into up to 242 branches, each of which has independently rewritten those files (added requires, inserted whole feature blocks, removed feature blocks, retuned config). This produces:

  • Merge conflicts on every forward-merge of devel into a partner branch — the conflicts are in core, not in the isolated overlay, so they need real engineering judgement (a removed in-process GooglePlayIntegrityCheck block, an added ~50-line resourceManager RPC block).
  • Silent drift: a security/bugfix on devel:server.js does not reach a partner until someone merges and resolves; branches like customization/dunatakarek-1.9.11.4-without-CR1 encode “we deliberately held back change CR1,” i.e. partial cherry-picks of core.
  • Divergent core behaviour across deployments that look like “the same product.” The hook/override system exists precisely to avoid this; the discipline of “extend via customization/, never edit core” is not enforced by the engine (nothing stops a branch editing server.js), so it leaks.

The branch-catalog (customization-branch-catalog) is the place to track which partner sits on which core baseline and how far each has drifted.

Mitigations the engine does provide (when used as intended): the overlay tree + hooks/overrides mean a well-behaved partner change is 0 core-file edits — new flow = add customization/flow/<name>/…; new letter = add customization/email/<type>/…; new behaviour = addHook/registerOverride in customization/listeners|overrides/…; new config = customization/docs/config/custom.json. Drift only happens when partners step outside customization/.


8. How vuer_css and esign_* mirror it

The exact same engine is copied across the four Node services. All define an identical ServiceBus extends WildEmitter with addHook/callHooks/callOnlyHook/registerOverride/callOverride, export { emitter }, reset serviceContainer.customizations = {}, register hooks at require-time, and default-export an async customization() called last in each entrypoint’s boot.

RepoContainer moduleEntry file(s)Notes / source
vuer_ossserver/service_container.js (snake_case)customization/customization.js + background-, cron-, convert-, media-, storage-customization.js (6 process files)This doc.
vuer_cssserver/service_container.js (snake_case)customization/customizations.js (single file, plural)git -C vuer_css show devel:customization/customizations.js → same addHook('queue'|'services'|'routes'|'socket', …) + module.exports = () => Promise… requiring ./listeners/* (e.g. webrtc-media-options, client-gate, self-service-v2). CSS has a smaller process set, so one customization file (no per-daemon split). Default branch origin/devel.
esign_ossserver/serviceContainer.js (camelCase)customization/customization.js + background-customization.js + cron-customization.jsgit -C esign_oss show devel:server/serviceContainer.jsbyte-for-byte the same ServiceBus class (:1-.. identical registerOverride/callOverride/addHook/…). customization.js:7 registers the rpc-esign:external RPC server (serviceContainer.customizations.rpcServer.vuerExternal = …getRPCServer('rpc-esign:external', VuerExternalRPCServer)) — this is exactly the queue vuer_oss calls as the e-sign client (server.js consumes rpc-esign:external). server.js:81 requires the customization file; callHooks('services'|'queue'|'webserver:setup') at :167/310/347. 25 customization branches.
esign_cssserver/serviceContainer.js (camelCase)customization/customizations.js (single, plural)git -C esign_css show devel:customization/customizations.js → same addHook('queue'|'services'|'routes'|'socket', …), trivial module.exports = () => Promise.resolve(). 18 customization branches.

Naming inconsistency across the family (verified): service_container.js (vuer_) vs serviceContainer.js (esign_); single entry customizations.js plural on both CSS-side repos vs customization.js singular + per-process files on the OSS-side repos. The mechanism is identical; only the filenames differ.

esign_oss's registerOverride lacks the duplicate-name guard that vuer_oss has (esign_oss:server/serviceContainer.js registerOverride is a bare this._overrides.set(...), no if (this._overrides.has(...)) throw). vuer_oss service_container.js:11-13 throws on duplicate. Minor divergence between the copied implementations.


Unverified / gaps

  • I did not read every one of the ~55 listener files, 6 overrides, or each Custom*Service implementation — I read access-resolver.js and screenshot-download-name.js as representatives and enumerated the rest by git ls-tree + the require(...) lines in the *-customization.js files.
  • Hook-tag count (“hundreds”) is from git grep aggregation of callHooks/callOnlyHook call sites in server/; I did not catalogue the semantics of every individual tag.
  • The 242 / 25 / 18 branch counts are git for-each-ref refs/remotes snapshots (read-only) at 2026-05-30; branches churn. Full inventory + per-partner drift belongs in customization-branch-catalog (not re-derived here beyond the dap example).
  • The customization/dap divergence (247 files, 35 core; git diff origin/devel...origin/customization/dap, snapshot at 2026-05-30) is one concrete partner; I did not diff all 242 branches. Other branches will differ in count but the same class of core-file edits is expected.
  • vuer_css working tree was on bugfix/FKITDEV-8787-…; all vuer_css facts here are from origin/devel via git show.
  • esign_css’s serviceContainer.js ServiceBus body was not printed line-by-line (grep for class ServiceBus on its devel returned via the customizations require, and server/serviceContainer.js is present); esign_oss’s was printed and confirmed identical to vuer_oss.

Sources

Files actually read (all read-only via git show <ref>:<path> / git ls-tree / git diff / git for-each-ref; working trees never modified):

vuer_oss (devel unless noted):

  • server/service_container.js (full).
  • Entrypoints: server.js (header :1-30, boot chain :718-744, hook call sites via grep), cron.js (:1-20, :250-310), background.js, convert.js, media.js, storage.js, integrationLog.js (customization/hook grep on each).
  • Customization entrypoints (full): customization/customization.js, customization/background-customization.js, customization/cron-customization.js, customization/convert-customization.js, customization/media-customization.js, customization/storage-customization.js.
  • Hook/override call sites: git grep -E "callHooks\(|callOnlyHook\(" and git grep -E "callOverride\(|registerOverride\(" over server/ + entrypoints; specific reads of server/web/routes.js:345-360, server/web/media_routes.js:30-42, server/socket/socket-server.js:50-66, server/auditlog.js:365.
  • Overlay tree: git ls-tree [-r] devel:customization (+ per-subdir counts); customization/listeners/access-resolver.js, customization/overrides/screenshot-download-name.js (full); git ls-tree -r of customization/{server,overrides,api,email,cron,cv,assets,db,flow}.
  • Convention-path loaders: git grep -E "customization/(flow|cv|db|api|sms|portal|server|translations|ocr|resources|user|features|settings|docs)" over server/,engines/,bin/,config.js — incl. server/e-mail/{EmailService.js:15,Letter.js:36-39}, server/sms/{SmsService.js:12,SmsMessage.js:34-35}, server/flow/{FlowService.js:24,FlowHandler.js:16-17,Flow.trans.js:1,723}, server/ocr/{OCRRecognitionClient.js:14,mrz/MRZService.js:30-35}, server/service/{RecognitionService.js:12,BackgroundProcessService.js:56-57,ImportService.js:5-9}, server/db/model/{customer.js:1,user.js:5-6,customerhistory.js:2}, bin/validate-config.js:5,26,41.
  • Build wiring: bin/script/script.task.js:6-55, bin/style/style.task.js:7-49.
  • Branches: git for-each-ref refs/remotes (count 242); git merge-base / rev-list --count / diff --name-only / diff -- server.js for origin/customization/dap vs devel.

vuer_css (origin/devel): customization/customizations.js (head), branch resolution via git show-ref/for-each-ref.

esign_oss (devel): server/serviceContainer.js (head, ServiceBus class confirmed), customization/customization.js (head, rpc-esign:external registration), server.js/background.js/cron.js customization+hook grep, branch count.

esign_css (devel): customization/customizations.js (head), git ls-tree devel:customization, git ls-tree -r for serviceContainer.js path, branch count.