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:
- DI / service container — one process-wide singleton (
serviceContainer) that every module mutates and reads. CarriesserviceContainer.service.*(core services),serviceContainer.customizations.*(custom services),.queue,.db,.logger, etc. - Hook + override registry (the
ServiceBus) — aWildEmittersubclass onserviceContainer.emitterthat addsaddHook/callHooks/callOnlyHook(lifecycle + extension points) andregisterOverride/callOverride(default-with-fallback behaviour swaps). - 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 entrypointcustomization()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 resettingserviceContainer.customizations = {}(e.g.customization/customization.js:4, also re-set incron.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.
| Method | Behaviour | Source |
|---|---|---|
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,35—get-janus-server-for-room/…-selfserviceroom/…-echotest(which media server a room lands on).CustomerService.js:374,437/DeviceChangeService.js:70—customer:inviteEmail/customer:inviteSms/customer:deviceChangeEmail.server/web/routes.js:49,105,114—routes:homepage/routes:after-call-redirect/routes:customer-list(swap whole page handlers).WebServerAuth.js:286—saml:config.room.endpoint.js:170,195—screenshot:downloadName/room:getExtraData.CallsReportService.js— ~20reportCalc:*/reportCallsDownload:rowsoverrides (per-partner report maths).OnlinePresenceService.js:92,290—autoDistribution: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.jsmodule must be required beforesetupServices()so itsaddHook('services', …)handler exists whencallHooks('services')fires; andcustomization()is called last (after queue setup) to require the listeners thataddHookthe 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:
- Module top-level (runs at
requiretime):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. module.exports = () => Promise…(runs whencustomization()is called): chains.then()blocks thatrequire('./listeners/...'),require('./overrides/...'),require('./api/...'), register flow handlers, and load template content. Requiring a listener file is what executes its top-leveladdHook/registerOverride. (Seecustomization/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 file | required at | Hooks it registers (top-level) |
|---|---|---|---|
server.js | customization/customization.js | server.js:27 | queue, services, routes, rab:routes, socket, webserver:setup, cv:setup, auditlog |
background.js | customization/background-customization.js | background.js:13 | queue, services, rab:routes |
cron.js | customization/cron-customization.js | cron.js:17 | queue, cron, services |
convert.js | customization/convert-customization.js | convert.js:12 | queue, services |
media.js | customization/media-customization.js | media.js:17 | queue, services, media_routes, webserver:setup |
storage.js | customization/storage-customization.js | storage.js:12 | services, queue |
integrationLog.jshas no customization file — itrequires none and never callscustomization()(git grep customization integrationLog.js→ only theserviceContainer.customizations = {}reset at:15). It still firescallHooks('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 dir | Files | Loaded by | What it overlays |
|---|---|---|---|
ui/ | 199 | bin/script/script.task.js:15-25, bin/style/style.task.js:23-33 (esbuild + Stylus glob `customization/ui/pages | layouts/**`) |
test/ | 84 | Jest/Playwright | Customization tests. |
email/ | 71 | convention path in server/e-mail/EmailService.js:15 + server/e-mail/Letter.js:36-39 | One folder per letter type (e-mail-invite, e-mail-verification, …) with .letter.{data.js,template.twig,plain.twig,style.styl,trans.js}. |
listeners/ | 55 | require('./listeners/*') inside customization() | Modules that addHook(...) runtime event tags. |
flow/ | 48 | convention path FlowHandler.js:16-17, FlowService.js:24; trans via Flow.trans.js:1,723 | One folder per flow (<name>/<name>.flow.{handler,proto,trans}.js) + flowStatProviders.js. |
ocr/ | 40 | convention path OCRRecognitionClient.js:14, MRZService.js:30-35 | Document validators (ocr/documents/<type>, ocr/mrz/{TD1,TD2,TD3,…}). |
server/ | 32 | mix of 'services' hook + convention paths | server/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/ | 13 | convention path SmsMessage.js:34-35, SmsService.js:12 | sms/<type>/<type>.sms.{data.js,template.twig,trans.js}. |
cv/ | 10 | convention path RecognitionService.js:12 (require('../../customization/cv/instruction.index')) | instruction.index.js + instructions/built-in-* (per-document CV recipes). |
translations/ | 9 | convention path CallsReportService.js:86 etc. | i18n dictionaries. |
portal/ | 7 | convention path in many core files | PortalData.js, PortalData.trans.js, LegacyPortalDataHelpers.js — the partner’s customer-data schema (see warning below). |
overrides/ | 6 | require('./overrides/*') inside customization() | Modules that registerOverride(...). |
cron/ | 4 | 'cron' hook in cron-customization.js | CustomerDeleteCronJob, AutoCloseRoomsCronJob, FlowClearCronJob, DeleteRoomsCronJob (each gated on config). |
api/ | 4 | require('./api/...') inside customization() | api/e-mail/{SMTPEmailApi,MockEmailApi}.js, api/sms/{SmsApi,SmsApiSmsApiCom}.js (the actual SMTP/SMS transport impls). |
user/ | 3 | convention path UserService.js:4, db/model/user.js:5-6, newuser/edituser.endpoint.js | UserData.js, UserScopes.js, UserData.trans.js. |
assets/ | 3 | static / SAML | image/*.png (logos), saml/AuthnRequestTemplate.xml. |
db/ | 1 | convention path server/db/translations/activity.trans.js:333 | db/translations/activity.trans.js. |
docs/ | 1 | bin/validate-config.js:5,26,41 | docs/config/custom.json + docs/config/schemas/ — extends the AJV config schema. |
settings/ | 1 | require('./settings/localJsonHasher') (customization.js, gated systemSettings.localJsonHashing) | Settings hashing. |
resources/ | 1 | resources | — |
service/ | 1 | — | — |
customization-maintenance/, customization-security/ | 1 each | bundled into docs PDF (docs.json) | Dated changelog notes (mirror root maintenance/,security/). |
Two loading mechanisms (both real, both in core)
- Convention-path loading (core hard-codes the overlay path). Core files
require()/path.join()straight intocustomization/.... 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:15→require('../../customization/email/${letterType}/${letterType}.letter.data')server/sms/SmsService.js:12→require('../../customization/sms/${type}/${type}.sms.data')server/flow/FlowService.js:24→require('../../customization/flow/${type}/${type}.flow.handler')server/ocr/mrz/MRZService.js:30-35→ registerscustomization/ocr/mrz/{PassportBooklet,TD1,TD2,TD3,VisaMRV-A,VisaMRV-B}server/service/RecognitionService.js:12→require('../../customization/cv/instruction.index')server/db/model/customer.js:1,user.js:5-6,customerhistory.js:2→ requirecustomization/portal/PortalData,customization/user/UserData|UserScopes,customization/server/db/model/customerhistoryserver/service/BackgroundProcessService.js:56-57→customization/server/backgroundProcess/${name}.processserver/service/ImportService.js:5-9→ 5×customization/server/service/Custom*ImportServicebin/validate-config.js:5,26,41→ importscustomization/docs/config/custom.json+ schemas dir
- Entrypoint
customization()+ hooks. Everything inlisteners/,overrides/,api/, and the'services'/'cron'/'cv:setup'hook bodies, pulled in whencustomization()runs (§3–4).
customization/portal/PortalData.jsis effectively a required core dependency. It'srequired byserver/db/model/customer.js:1,server/db/helpers.js:8,PortalService.js:2,PortalDataRPCServer.js:3,AppointmentService.js, half a dozen*.endpoint.js, and severalbin/tools. The customer-data schema lives in the overlay, so OSS cannot boot without acustomization/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:
develis 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 branchescustomization/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 inassets/, partner config inconfig/*.json, partner schemas indocs/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 toserver.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
develinto a partner branch — the conflicts are in core, not in the isolated overlay, so they need real engineering judgement (a removed in-processGooglePlayIntegrityCheckblock, an added ~50-lineresourceManagerRPC block).- Silent drift: a security/bugfix on
devel:server.jsdoes not reach a partner until someone merges and resolves; branches likecustomization/dunatakarek-1.9.11.4-without-CR1encode “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 editingserver.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 = addcustomization/email/<type>/…; new behaviour =addHook/registerOverrideincustomization/listeners|overrides/…; new config =customization/docs/config/custom.json. Drift only happens when partners step outsidecustomization/.
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.
| Repo | Container module | Entry file(s) | Notes / source |
|---|---|---|---|
| vuer_oss | server/service_container.js (snake_case) | customization/customization.js + background-, cron-, convert-, media-, storage-customization.js (6 process files) | This doc. |
| vuer_css | server/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_oss | server/serviceContainer.js (camelCase) | customization/customization.js + background-customization.js + cron-customization.js | git -C esign_oss show devel:server/serviceContainer.js → byte-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_css | server/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_) vsserviceContainer.js(esign_); single entrycustomizations.jsplural on both CSS-side repos vscustomization.jssingular + per-process files on the OSS-side repos. The mechanism is identical; only the filenames differ.
esign_oss's
registerOverridelacks the duplicate-name guard that vuer_oss has (esign_oss:server/serviceContainer.jsregisterOverrideis a barethis._overrides.set(...), noif (this._overrides.has(...)) throw). vuer_ossservice_container.js:11-13throws 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*Serviceimplementation — I readaccess-resolver.jsandscreenshot-download-name.jsas representatives and enumerated the rest bygit ls-tree+ therequire(...)lines in the*-customization.jsfiles. - Hook-tag count (“hundreds”) is from
git grepaggregation ofcallHooks/callOnlyHookcall sites inserver/; I did not catalogue the semantics of every individual tag. - The 242 / 25 / 18 branch counts are
git for-each-ref refs/remotessnapshots (read-only) at 2026-05-30; branches churn. Full inventory + per-partner drift belongs in customization-branch-catalog (not re-derived here beyond thedapexample). - The
customization/dapdivergence (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 fromorigin/develviagit show. - esign_css’s
serviceContainer.jsServiceBus body was not printed line-by-line (grep forclass ServiceBuson itsdevelreturned via the customizations require, andserver/serviceContainer.jsis 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\("andgit grep -E "callOverride\(|registerOverride\("overserver/+ entrypoints; specific reads ofserver/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 -rofcustomization/{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)"overserver/,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.jsfororigin/customization/dapvsdevel.
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.