Structure
server/ core/ HTTP router, auth, DB (node:sqlite), event bus, templates, time zones, public pages services/ Business logic that several modules share: contacts, sending (email/SMS), opportunities, workflow engine, reviews, GHL import modules/ One module per area: its own tables (migrations) + routes (register) modules/index.ts Registry – register new modules here assets/forms/ Browser scripts and styles of the public forms (parts that `npm run build` minifies; docs/forms.md)web/src/ React UI (one file per area under pages/)tests/ Unit and end-to-end API tests (vitest)Adding a module
Section titled “Adding a module”import type { Module } from '../core/app.js';
export const membershipsModule: Module = { name: 'memberships', migrations: [['001-init', `CREATE TABLE courses (id TEXT PRIMARY KEY, location_id TEXT NOT NULL REFERENCES locations(id) ON DELETE CASCADE, …)`]], register(app) { app.router.get('/api/courses', 'location', (ctx) => app.db.all('SELECT * FROM courses WHERE location_id = ?', ctx.locationId)); app.events.on('invoice.paid', (e) => { /* unlock access */ }); },};…and register it in server/modules/index.ts. Never change migration names, only append new ones.
-
Auth per route:
public,user(signed in),admin,location(signed in or API key + sub-account). Everything withlocationfilters onctx.locationId. -
CSRF: Writing requests with a cookie need the header
x-pghl: 1(the UI does this automatically). -
Events (
app.events.emit(locationId, type, data)) run after the response; workflows and webhooks hook into them.data.contactIdstarts workflows for that contact. -
Permissions:
app.guardsrun after sign-in; the agency module checks the areas of restricted logins there (server/core/permissions.tsmaps routes to areas). New modules register their routes there – the SaaS guard (saas-gate.ts) uses the same mapping to block areas outside the plan and to check limits on creation. -
UI language: Texts stay German in the code and go through
t()fromweb/src/i18n(const t = useT('bereich'), the German text is the message ID); the English translations live per area inweb/src/i18n/en/*.ts.npm run i18n:checkkeeps areas withcomplete: trueinweb/src/i18n/registry.tscomplete. Each person’s language is stored bymodules/language.ts(users.lang,/api/me/language) – guide: translating.md (English). -
Performance: no query per sub-account/row within a request (group instead, see
services/location-stats.ts), an index in the module’s own migration for every newWHERE/ORDER BYon large tables, read-only lists/reports throughcached()(core/cache.ts) orapp.db.memo, background jobs in slices of at most ~50 ms (sliceTimer), new pages withlazyPages– the rules with examples: docs/performance.md.app.perf(event loop, slow requests/queries, job times) →GET /api/admin/metrics. -
Background jobs via
app.onTick(fn)(workflow waits, reminders, scheduled campaigns, subscription invoices, iCal sync, date triggers, backups). -
Hooks between modules via
app.servicesinstead of direct imports:instrumentEmail/personalizeLinks(tracking → sending and mailbox sending;instrumentEmailsets click links and the counting pixel,trackingCommittakes over pending clicks – docs/email-tracking.md),externalBusy(iCal → calendar),secondFactor(2FA → login),workflowActions(custom workflow actions, e.g.social_post,ai_text,whatsapp_send),workflowTriggers/workflowRecipes(custom triggers and templates, e.g.call.missed),messageChannels(further channels forPOST /api/messages, e.g. WhatsApp),mcpTools(custom MCP tools),aiRun/aiFetch(AI call with limit and log / test hook for the Claude API),reviewWidgetGoogle(Google stars in the reviews widget),twilioFetch/googleFetch(test fakes for external APIs),publicBaseUrl(agency → links),sitePaths/siteSitemap(custom paths such as/blogand sitemap entries on websites, e.g. blog → funnels),pageServe(serve a different version of a funnel page per visitor, or post-process the HTML, e.g. A/B tests → funnels),buyPageHtml/buyCheckout/buyInvoiceItems/buyAfterPaidandstripeBuy(extend the purchase page, checkout session, invoice lines and the step after payment, e.g. order bump, discount codes, upsell → Stripe),renderLessonPreview/sendMemberLoginLink(member area → courses),portalKit/portalHead/portalHeader/portalDashboard/portalMemberCheck(further areas in the member area, e.g. the community: page frame, links at the top, cards on the start page, who may sign in),snapshotParts(custom parts in snapshots and when copying, e.g. blog posts, courses, community groups – withprepareso that references point to the copies),invoicePayHtml(Stripe → “Pay now” on the invoice),pushNotify(push notification to team/users),usageGate(SaaS plan: counts email/SMS/AI calls right before sending or the Claude call and returns a reason if the limit is reached or the account is paused),saasCompleteSignup/saasPriceId(SaaS sign-up ↔ agency Stripe),oauth(OAuth connections:token(id)returns a valid access token, no other module sees refresh tokens; test fakeoauthFetch),googleBusinessReady/googleBusinessFetch/googleBusinessStatus(Business Profile → Google reviews instead of the Places API).externalBusyis a chain: iCal (calendar-sync) and Google/Outlook (calendar-connect) append their busy times,twilioFetch/googleFetch/todoistFetch(test fakes for external APIs),todoistSync(trigger the Todoist sync),aiReady(whether an AI call would work right now – AI assistant on, key, limit),voiceAgent(the AI phone agent takes over an incoming call → TwiML),voiceDialTargets/voiceDialHeld(the browser phone rings on incoming calls or holds a call),cspExtra({connect: [...]}– additional https/wss origins in the app’s Content-Security-Policy),pageVisit(funnels → attribution: token and hint per page view),contactFilters(custom filters for contact lists and smart lists, e.g. source/campaign, company properties, lead score),contactSorts(custom sort orders of the contact list, e.g.score),contactOrder(sorting by any column including custom fields and direction –modules/smart-lists.ts, fixed SQL list, field path as a parameter),associations(associations between arbitrary records:registerTypefor custom types such as tickets,link/unlink/list/setPrimary, deletion cleans up via trigger),objectSchema(properties per object with type validation, activity and notes per record),companies/customObjects(create/change companies and custom objects, also for imports),supabaseTables(custom tables for the Supabase mirror including SQL),workflowTriggerFilters({trigger: (config, data) => boolean}– filters for custom workflow triggers),workflowActionMeta/workflowTriggerMeta(description for the action picker and the generated form),workflowStepCleaners(validate/limit steps on save),workflowStepSkips((step) => reason | undefined– the engine skips the step with this log line, e.g. paused steps),workflowImportKit(reId/warningsFor/stripSecrets/refsOfof the workflow import – copying and pasting steps uses them),channelDnd(do-not-disturb per channel when sending),connectorFetch/sheetsApi/stripeKey(outgoing calls and credentials of the app steps),wfxNow/wfxRandom/wfxSleep/wfxRunScans(test hooks: clock, randomness, pauses, time triggers) – details in docs/workflow-actions.md,attribution(visit/capture/checkout: forms, appointments, chat and purchase links report their contacts),attributionHooks(onTouch/onLink– affiliates count clicks and leads, ads report conversions),attributionReport(report for the ads evaluation),rawBodyLimit((method, path, req, cookies) → bytes: the route accepts a raw binary body, which is then inctx.rawBuffer– e.g. the APK upload),mobileAppInfo(current Android app for/api/mobile/me),mobileFeed/mobileScanDue(add notifications for the app / check due tasks).companies/associations/ticketsImport(company and helpdesk modules that the HubSpot import uses if present – contract in docs/integrations.md),hubspotStatus(HubSpot connection → integrations catalog),aiKnowledge(further knowledge sections for the AI assistant, e.g. published help articles),portalNextPrefixes(further member pages that the login link leads back to, e.g.tickets),helpdesk(createTicket/updateTicketfor other modules),csatLink/sendCsat/npsStats/surveysOfContact(surveys → tickets, dashboard, contact page),healthCompute(recompute the health score).catalogExtra(custom entries in the integrations catalog including a status probe),videoStudio(video studio: create/fetch jobs, schedule clips (scheduleClip), background tasks – they use the YouTube/AI shorts and the recycling module),videoStudioReady(list of hooks that run after a finished shorts job, e.g. recycling/autopilot),videoClipThumb(thumbnail URL of a clip),videoRecycle(create/adopt content recycling, autopilot),socialPostwithmediaUrls+video(video via a signed link to Postiz, then also YouTube/TikTok) andthread(further parts as replies, e.g. an X thread),aiRunwithtimeoutMsfor long texts. Further channels (modules/channels.ts+services/channel-kit.ts):channelStatus/channelMedia(composer and files per channel),channelSend(send on any channel, e.g. replies from Slack),emailTransports/smsTransports(providers behinddeliverEmail/deliverSms– they find the app through the non-enumerable propertyappof the sub-account row fromapp.location()), test hookschannelFetch/discordSocket(docs/channels.md). ARawResultmay have a readable stream asbody(it is streamed, e.g. the clip proxy).aiConnections(AI providers/BYOK:list/resolve(locationId, capability|usage, id?)/models/textProvider/fetchFor– every text and media connection, docs/ai-providers.md),aiProvider(locationId, purpose?)(provider foraiRun; a mapping in the AI providers takes precedence),agent(AI agent:run/confirm/runTool),agentDefs(custom agents: configuration, visibility, owner),agentBridge(local bridge),agentFetch/aiConnectionsFetch(test hooks for model calls),mcpToolsis used by the AI agent in-process – new tools are available to it immediately (docs/agent.md). Mailbox sync (Gmail/Outlook):conversationFilters((ctx, where, params)– filters of the conversation list, e.g.?mailbox=),conversationRows((ctx, rows) → rows– extra fields such as folder marks),messageRows((ctx, conversationId, rows) → rows– attachments, labels, flags per message),oauthPurposes({ mail: { callback } }– a module finishes the shared OAuth callback for its own purpose),mailbox(get/sync/flush),mailboxPush(register/stop/renewDue); the providermailboxplugs intodeliverEmailviaemailTransports(docs/mailbox-sync.md). If the module is missing, the core keeps running without the feature. Calling other routes in-process (same sign-in, guards and audit log):callRoutefromservices/route-call.ts– used by the REST hook actions and the integrations catalog. Creator & Commerce (payment-plans, dunning, quizzes-certificates, comment-to-dm, workflow-ai):stripeTaps([(locationId, event) => {handled} | undefined]– every verified, new Stripe event goes to these taps first, e.g. payment plans and dunning; a result ends the processing),lessonGates/lessonExtras/lessonCompleteBlock(member area: lock a lesson, HTML after the lesson text, block ticking off by hand – quizzes and assignments),metaKit(accountFor/ids/graph/windowState– Meta access for modules without handing out tokens),metaWebhookHooks(receives{payload, secret, locationId}) and the optionpayloadofmetaSend(messages with buttons),creatorPlans,dunning/dunningInvoices/billingPage(dunning and subscription page),quizKit/assignmentKit/certificates(quizzes, assignments, certificates),igComments. The AI usageworkflows(mapping in the AI provider) serves the AI workflow builder (aiRunwith the purposeworkflow_builder). -
Knowledge memory (Cognee):
modules/cognee.ts(tables, agency + sub-account routes, governance status),cognee-sync.ts(scheduler job,privacyErasehook queues the deletion of a contact’s data items,app.services.cognee),cognee-tools.ts(MCP/agent toolsghl_memory_recall/ghl_memory_remember); logic inservices/cognee/*(client.tsREST client with the sub-account-boundLocationClient,sources.tsoneKindDefper kind of record,sync.tsengine + deletion queue,recall.ts,redact.ts,model.ts). Hooks:app.services.cogneeFetch/cogneeSleep/cogneeNow/cogneeGapMs(test hooks), the optionaluntrusted: trueon an MCPTool(the agent treats its output as untrusted). Dataset isolation, erasure and what is sent: docs/cognee.md. -
Consent & legal:
modules/consent.ts(ledgerconsent_events/consent_texts/consent_doi, double-opt-in tick, routes, triggersconsent.confirmed/consent.revoked),consent-capture.ts(forms, webinar, community, import, API),consent-pages.ts(confirmation page, settings page),consent-banner.ts(cookie banner/CMP),legal-pages.ts(legal texts),checkout-law.ts(purchase page under German law); logic inservices/consent-model.ts(pure),consent.ts,consent-proof.ts,cmp.ts(pure),legal-texts.ts(pure),legal-pdf.ts. Hooks inapp.services:consentGate(services/messaging.tsand WhatsApp sending ask it before every marketing message;purpose: 'transactional'for service messages),consent.*(capture, status,allowMarketingManyfor audiences – callers use?.),cmp(attributionConsent,linkCmp),htmlTransforms(core: post-process public HTML pages, in atry/catch),buyButtonLabel,legalTexts(profile,facts,generate,render,hostedUrl,prefill,providers– for other modules such as the hosted sign-in). Contract, limits and open legal questions: docs/consent.md. -
Undo, history, trash:
modules/undo.ts(tableundo_entries, guard + after-write hook, routes/api/undo/*, retention, GDPR step),modules/undo-entities.ts(the records and their routes),modules/undo-tools.ts(MCPghl_undo_list/ghl_undo,/api/mobile/undo/*); engine inservices/undo-journal.ts,undo-core.ts,undo-table.ts,undo-versioned.ts. Hooks:app.services.undowithregister(type, {snapshot, restore, label, …})(what a record is and how it comes back –tableEntitycovers the table including foreign keys),track({method, pattern, entity, targets, action, when, notice, coalesceMs, quiet})(which route changes which records),enabled,cleanup(at);ctx.undoSinkcollects the entries of an agent tool;Db.txnests (savepoints). New modules register their records there – contract and limits: docs/undo.md. -
Website builder (sites.md):
modules/funnels.ts(pages, deliveryservePage, preview) with one module per topic –site-routing.ts(redirects, search, translations, deep paths),site-design.ts(components),site-cms.ts(collections),site-product.ts(product block),site-a11y.ts(BFSG check – the German accessibility law),site-analytics.ts(statistics per step, board widgetfunnel_overview, MCPghl_funnel_analytics),site-templates.ts(23 templates),site-media.ts(image sizes,/uploads-opt),site-snapshot.ts(copying); pure logic inservices/site-*.ts(blocks, layout, page, design tokens, redirects, search, check, images, templates). Hooks inapp.services:siteBlocks(map of block renderers – custom blocks register there),siteRedirect,siteResolve(dynamic paths, e.g. collection items),siteSearchDocs,siteSitemap,siteServe,siteNotFound,siteMissed,siteImages(dims,srcset),imageEngine({name, formats, resize};sharpis detected, never required) as well as the existingpageServe,pageVisit,htmlTransforms,buyPageHtml,buyCheckout,boardWidgets,mcpTools,snapshotParts. UI:web/src/pages/site/*as separate chunks (lazyPages), tree operations purely inmodel.ts. -
Funnel & Website AI (site-ai.md):
modules/site-ai.ts(brief → page plan → block JSON, runs, preview, apply, MCPghl_site_generate, agent hint) andsite-ai-drafts.ts(unpublished AI websites through a wrapper aroundapp.services.siteRedirect, restore points, AI writer for blog and email) – one registry line...siteAiModules; pure logic inservices/site-ai-schema.ts(block schema againstsiteBlocks(app), validating, compiling, merging a section),services/site-ai.ts(prompts, plan/page validation, section commands, diff, estimate) andservices/site-ai-kit.ts(model, budget, estimate, ask-check-repeat loopaskChecked– also used bymodules/agent-ai.ts, “Build with AI” for AI agents → draft agent, MCPghl_agent_generate). Hooks:siteAiDrafts(mark/isDraft/restorePoint); BYOK usagesites. -
Custom Values (custom-values.md):
modules/custom-values.ts(tablescustom_values,custom_value_form_targets, routes,pageServereplacement on published pages, snapshot part with placeholders, undo entry, MCP), pure helpers inservices/custom-values.ts. For this,core/template.tshas a small namespace registry (templateNamespace(name, resolver)):{{custom_values.key}}therefore works in everyrender(); the sub-account comes from the template data (location.id,contact.location_id…). -
Sales:
modules/sales.ts(one registry line for seven modules) –record-access.ts(teams, rules, the routerwrappershook that checks every request of a restricted person,app.services.recordAccess),deal-lines.ts,quotes.ts(+quote-model/engine/page, signature viacreateSignedContractincontracts.ts, deposit invoice through the invoice flow),sequences.ts(+sequence-model/engine, sends throughsendMail, reply sweep on the tick),contact-duplicates.ts(+contact-dedupe/merge, undo through the journal entitycontact_merge),quotas.ts(+ board widgets),meeting-notes.ts(app.services.attachCallSummary); internal approvalquote-approvals.ts(+services/quote-approval.ts) through the hookapp.services.quoteSendGates–sendQuoteasks every gate before sending. Details: sales.md. -
Living Boards:
modules/boards.ts(boards, versions with undo/redo/restore, proposals, preferences, SSE stream, widget catalogue and option sources),boards-compose.ts(the prompt bar:aiRunwith a strict patch schema, proposals bound to the asker),boards-templates.ts(starters, saved and Marktplatz templates, review/consent, export/import),boards-tools.ts(MCP tools,/api/mobile/boards*, Agent hint). Logic inservices/boards-*.ts:boards-layout.tsandboards-consts.tsare pure, import-free modules shared with the browser (grid maths:settle,placeNew,derive),boards-patch.ts(the one patch engine for editor, agent, MCP and API),boards-widgets.ts(registry) withboards-widgets-core.ts(the 13 built-in widgets) andboards-presets.ts,boards-data.ts(widget data throughcallRouteas the viewer),boards-store.ts(state, versions, access),boards-hub.ts(SSE hub with revalidation),boards-embed.ts(host allow-list). Hooks inapp.services:boardWidgets(useboardWidgets(app).register(def)fromservices/boards-widgets.ts; or push a definition ontoboardWidgetDefsbefore the module starts),boards(apply/load/full/data/list/registry/hub),boardsEmbedHosts,boardsStart,boardsHub. Permission areadashboard(/api/boards,/api/board-templates);callRoute(..., {quiet: true})skips the audit line of read-like POSTs. Contract, widget API and self-hosting notes: docs/living-boards.md. -
XR (Raum-Modus, Arbeitsfläche, “Open in XR”):
modules/xr.ts+services/xr-model.ts(per-person layouts: board panels, canvas plane, workspace; validation shared with the browser),web/src/xr/*(detection, glass tokens + lazyspatial.css, type scale,Workspace.tsx,XrViewer.tsxand the in-house WebGL2 engine inweb/src/xr/engine/), the preferencespatialinappearance-model.ts. See docs/xr.md. -
Back-office hooks (backoffice.md):
stripeEvents(list of(app, event) => void, called by the Stripe module for events it does not handle itself – bookkeeping uses it to book payouts, fees and refunds),accounting(settings/enabled/post/storno),absences(forUser/targetHours– absences and target hours per login, for time tracking),workingTimeProvider(hoursByUser(locationId, from, to)– the counterpart that time tracking supplies to the HR module),hrBalance/hrHolidays,accountingPending/accountingFlush(wait for background work). Ledger entries, receipts, stock movements and HR data are protected by triggers or rules of their own (immutability, period lock, sealed fields) – new modules write only throughapp.services.accounting.post. -
Workflow builder (canvas, copy/paste, pause, validation): one module each –
modules/workflow-canvas.ts(keepssettings.layoutclean, viaapp.router.wrappers),workflow-clipboard.ts(POST /api/workflows/:id/copy|paste, formatgohigh-steps),workflow-pause.ts(disabled: true→workflowStepSkips),workflow-validate.ts(issuesin the responses ofGET/PATCH /api/workflows/:id,POST …/validate); pure logic inservices/workflow-validate.tsandservices/workflow-layout.ts, which the browser uses too. UIweb/src/workflow-canvas/*:tools.tsx(list/canvas switch, menu, toggles, hints – for both editors),WorkflowCanvas.tsx(own chunk vialazyPages),layout.ts/model.ts/stats.tspure. Details: workflow-actions.md. -
Version, demo mode, TLS:
modules/version.ts(GET /api/version: name, version frompackage.jsonviaserver/version.ts, demo info),modules/demo.ts(PGHL_DEMO_MODE=1: seed fromservices/demo-seed.tsviacallRoute, nightly reset,app.guardsagainst dangerous writes,app.services.demowithinfo/reset/blocked) andservices/outbound-guard.ts(blocks every outgoing connection and DNS lookup process-wide except loopback – usable in tests too),modules/tls-ask.ts(GET /api/tls/askfor Caddy’s on-demand certificates, readsapp.services.wantedDomainsfrom the agency module). -
Test hooks:
memberMailerreplaces the sending of login links (member area),communityMailerthat of the community digest (communityDigest(at)starts it),affiliateMailerthat of the partner area,adsFetchthe Meta/Google APIs,restHookFetchthe delivery of REST hooks,hubspotFetch/hubspotSleep/hubspotPauseMsthe HubSpot API and its waiting,openshortsFetchthe OpenShorts API,feedFetchthe retrieval of the autopilot feeds – like*Factoryin other modules. -
AI tools & providers:
aiProvider(locationId)(modules/ai-providers.ts: null = Claude, else the OpenAI provider with model, key,callandcost–aiRundispatches to it, so every AI consumer works with either),openaiFetch/githubFetch(test hooks for the OpenAI and GitHub APIs),mcpAuth(list of resolversbearer → {agency, locationId}– further credentials for/mcp, e.g. OAuth access tokens from modules/mcp-oauth.ts),mcpChallenge(theWWW-Authenticateheader of a 401 from/mcp, pointing to the protected resource metadata),mcpOAuth(list/revokeconnected apps for the AI tools page),brandName(white-label name for MCP metadata). Pure OAuth helpers (redirect URI rules, PKCE, metadata) live inservices/mcp-oauth.ts, the Responses API client inservices/openai.ts. -
AI studio (media generation):
modules/genmedia.ts(routes, callbacks, files, app API) with one adapter per provider inservices/genmedia/(fal, kie, replicate, openai, gemini, stability, elevenlabs, runway, luma, custom-http) and the job engineservices/genmedia/engine.ts. It reads keys only throughaiConnections(resolve(locationId, capability, connectionId?)/list(locationId)– the BYOK registry of the agent module) and offersgenmedia(create/get/out/process/pollDue/idle) to other modules;genmediaFetchis the test hook for all provider HTTP (URL checks still apply). Workflow actionsgenmedia_image/genmedia_video/genmedia_tts, triggergenmedia.completed, five MCP tools. -
Voice layer (Stimme):
services/voice-engine.ts(synthesize/transcribe/listVoices/liveTts/openLiveStt/phonePlanover the BYOK registry – the speech providers are AI studio adapters inservices/voice-adapters.ts, registered withregisterAdapter),services/voice-phone.ts(Twilio Media Stream session: protocol, turn loop, barge-in, fallback to Twilio’s voices),services/voice-ws.ts(WebSocket server endpoint;core/app.tshands HTTP upgrades toapp.services.upgradeHandlers),services/voice-audio.ts(G.711, resampling, WAV, VAD).modules/voice.ts(settings, push-to-talk, prefs, MCP, mobile relay) andmodules/voice-phone.ts(Twilio hooks, recorded turns, voicemail). The phone agent’s brain stays inmodules/voice-ai.tsand is offered asapp.services.voiceBrain;voiceTransportlets the voice layer carry a call. Test hooks:voiceFetch,voiceWs. Details: docs/voice.md. -
Focus mode with companion / Jarvis live:
modules/companion.ts(prefs, briefing, check-ins and their scheduler; other modules add to a check-in throughapp.services.companionCheckinHooks) andmodules/companion-live.ts(event triggers viaapp.eventslisteners and its own scan inonTick, the initiative engine overaiRun, the Gemini Live proxy onupgradeHandlersat/ws/companion-live); pure logic inservices/companion-prefs.ts,briefing.ts,companion-triggers.ts(registry, routing, rate limit),companion-initiative.ts(schema, cool-down, levels, learning) andgemini-live.ts(Live API framing). Test hooks:companionLiveNow,companionLiveWs,companionLiveBase,companionLiveTimeoutMs. Details: living-boards.md. -
Telephony & call intelligence:
modules/telephony-audio.ts(audio files, hold melody),telephony-numbers.ts(number registry, search/buy/release over the Twilio API, route kinds),telephony-ivr.ts(menus and schedules; the position in a menu travels in the signed webhook URL),telephony-queues.ts(ring groups and poll-based queues),telephony-dni.ts(pools, the page snippet, attribution, report),telephony-calls.ts(notes, cold and announced transfer, retention sweeper),dialer.ts(lists, the compliance gate, outcomes),call-intel.ts(stereo split → per-channel STT → counted metrics + AI analysis → timeline/tasks/events) andcall-scorecards.ts; pure logic inservices/ivr.ts,call-groups.ts,call-audio.ts,twilio-numbers.ts,dni.ts,dialer.ts,call-intel.ts. Hooks inapp.services:callRouters/callRouteKinds(take over an inbound call by dialled number),callInbound(run when an inbound call row is created),telephonyKit(hookRoute,voicemail,unanswered,settleDial…),callGroups.enter,voiceUserClient,audioUsage,dialer.authorize,callIntel,callScorecards.score,callSummaries(list/get/subscribe– summaries for deal timelines),telephonyRetention.sweep;VoiceAgentInput.force. Details and the contract: telephony.md. -
Webinars (evergreen webinars):
services/webinar-schedule.ts(pure schedule engine: sessions are computed from rules, never stored;roomClockdecides waiting/running/late/ended),webinar-model.ts(types, defaults, honesty guardLIVE_CLAIM, signed join/preview tokens),webinar-video.ts(sources, Range parsing),webinar-store.ts/webinar-register.ts(registration, confirmation with .ics, reminders),webinar-track.ts(heartbeat on the server clock, questions into Conversations, polls, offer, purchase attribution, end-of-session bookkeeping),webinar-stats.ts,webinar-pages.ts+webinar-room-client.ts(server-rendered registration page and room with a nonce-CSP). Modules:modules/webinars.ts(admin API, tables, upload),webinar-public.ts(/w/:slug…),webinar-automation.ts(six triggers, actionwebinar_register, form and purchase bridges, tick),webinar-tools.ts(MCP and app endpoints). Test hook:app.services.webinarNow(virtual clock). Details: docs/webinars.md. -
Umzug (switch-over importers) and deliverability:
modules/umzug.ts(tables, routes, one background job per sub-account, housekeeping),services/umzug-engine.ts(runJob: sources are async generators of{records, cursor, total}batches over normalised records, applied byumzug-write.ts’sWriterwith a savepoint per record;buildPreview,undoJob),umzug-core.ts(record types,SourceDef, auto-mapping),umzug-http.ts(VendorHttp: retries,Retry-After, German errors; hosts are fixed per source, links in responses are never followed), oneumzug-<vendor>.tsper source,umzug-files.ts/umzug-zip.ts(CSV/JSON/HTML/ZIP exports),umzug-automation.ts(automations → workflow drafts),modules/umzug-tools.ts(MCP).modules/umzug-mailcheck.ts+services/umzug-mailcheck.tscheck SPF/DKIM/DMARC/MX with an injectableDnsResolver;messaging.tsadds the RFC 8058 headers (unsubscribeHeaders,OutMail.headers,SendInput.bulk). Details: umzug.md, deliverability.md. Hooks:app.services.umzug.idle()(resolves when no job runs),privacyErase(an erased contact leaves the id map and undo list); test hooksumzugFetch/umzugSleep/umzugPauseMs(fake vendor servers:tests/umzug-fakes.ts),mailDns(fake resolver) andmailCheckFetch(provider APIs).outbound-guard.tsalso blocks the promise API’s DNSResolverin demo mode. -
Team inbox and team calendars:
modules/team-people.ts(availability and out-of-office, one source for assignment and round robin),teaminbox.ts(columns onconversations, views, assign / status / snooze / priority / notes / bulk, settings; hooksconversationFilters,conversationRows,inboxAudience),teaminbox-engine.ts(inbound handling, answer timers in SQL triggers, scheduler,/api/inbox/streampresence),teaminbox-automation.ts(three triggers, three steps, report sources),teaminbox-tools.ts(MCP and/api/mobile/inbox*); servicesteaminbox-model.ts(pure: settings, deadlines on the helpdesk’s business-hours maths, mentions, view SQL),teaminbox-store.ts,teaminbox-assign.ts(round robin / least busy / rules). Calendars:teamcal.ts(config, members, holds, waiting list, admin routes),teamcal-public.ts(booking, manage, pay-return, routing and waiting-list pages),teamcal-tools.ts; servicesteamcal-model.ts(pure slot maths for several people),teamcal-engine.ts(loads, busy times incl. OAuth calendars),teamcal-book.ts,teamcal-pay.ts(Stripe Checkout holds via thestripeSessionshook ofmodules/stripe.ts),teamcal-video.ts,teamcal-pages.ts. The booking pipeline incalendars.tshas small hooks (teamcalPrepare,teamcalSlots,teamcalPage,teamcalManagePage,teamcalCancel,teamcalVars,teamcalPayload). Docs: team-inbox.md, team-calendars.md. -
Calendar view, recurring appointments, AI booking (booking.md):
modules/calview.ts(one registry line for three modules) –appointment-series.ts(tableappointment_series,series_id/recurrence/occurrence_date/series_exceptionon appointments, rolling generation in the tick),calendar-view.ts(grid data, conflict check, move/resize throughrescheduleAppointmentorPATCH /api/appointments/:id, drawer, invoice draft, tableappointment_invoices),ai-booking.ts(settings, MCP tools,/book/move/:token); logic inservices/calview.ts(viewData,checkTime),services/appt-series.ts,services/appt-recur.ts(pure RRULE subset on top ofpm-recur.ts),services/ai-booking.ts(free times and booking shared with the AI phone agent). Hooks:chatBotTurn(the AI assistant’s chat bot asks it every turn),aiBooking,appointmentCancel(the calendar page’s cancel with refund rules),calendarView; the MCPToolflagbookingand the agent modebook(“Book appointments”) inservices/agent-safety.ts. UI:web/src/pages/CalendarView.tsx+CalendarGrid.tsx,CalendarDrawer.tsx,calview-model.ts(pure wall-clock maths),calview.css– one lazy chunk, i18n namespacecalview. -
Wiki and team chat:
modules/wiki.ts(spaces, page tree, versions, comments, favourites, shares, import/export, uploads),wiki-extras.ts(help-center publishing through the/api/kb/articlesroutes viacallRoute, AI helper throughaiRun),wiki-public.ts(read-only share pages under/wiki/s/<token>),wiki-mcp.ts;modules/teamchat.ts(channels, DMs, messages, threads, reactions, pins, files, search, SSE stream, retention),teamchat-commands.ts(/aufgabe,/kontakt,/agent– the agent runs inaskmode with the asker’s rights, confirmation is bound to the asker),teamchat-mcp.ts,collab-mobile.ts(/api/mobile/chat/*,/api/mobile/wiki/*). Shared pieces inservices/:wiki-markdown.ts(escaping renderer + allow-list sanitiser, shared by pages, comments and chat),collab-fts.ts(FTS5 withLIKEfallback, joined by rowid so isolation never depends on the index),collab-cards.ts(live record cards; other modules add kinds withregisterCardKind),collab-actor.ts,wiki-store.ts,teamchat-store.ts,teamchat-hub.ts(in-memory SSE hub: presence, typing, revalidation of open streams),teamchat-notify.ts. Hooks inapp.services:wikiFts,chatFts,chatHub,chatRate(rate-limit overrides for tests),wikiPdf,wikiPageOut,collabCards,teamchat(post/notify/channelOut),teamchatCommands,teamchatRetention,collabIdle/collabPending(wait for background notifications), andcollabTasks.create– the seam the project-management module can use to take over task creation for/aufgabe(default:POST /api/tasks). Both areas map to the permission areaswikiandteamchat; the audit log gets one line per sensitive write and a throttled line for autosaves and chat posts. Docs: wiki.md, team-chat.md. -
Office (documents, spreadsheets, presentations, PDF):
modules/office.ts(tablesoffice_files/office_versions– a trigger forbids changes to versions –,office_locks,office_comments; files, versions, content, editor routes/office/app/<kind>/,/office/bridge.js,/office/blob/<token>/<name>,contactFiles, push kindoffice),office-collab.ts(edit lock, comments, people),office-tools.ts(read, edit, PDF, convert through the engines; five MCP toolsghl_office_*); logic inservices/office-model.ts(pure: kinds, names, command validation, lock),office-store.ts(bytes under<data>/office-files/,PGHL_OFFICE_MAX_MB/PGHL_OFFICE_QUOTA_MB),office-engine.ts(adapter to the optional command-line engines fromPGHL_OFFICE_BIN_DIR: own temp folder, no shell, time and size limits),office-blank.ts(blank files, text without an engine),office-bridge.ts/office-bridge-js.ts(editor page fromPGHL_OFFICE_DIRwith CSP and nonce, load/save bridge, signed links). Areaofficeincore/permissions.ts, pageofficeinPRESENCE_ROUTES; upstream programs and installation:deploy/office/. Details: docs/office.md. -
Live collaboration everywhere:
modules/presence.ts(routes/api/presence*, tablepresence_prefs, switch inlocations.settings.presence),services/presence-hub.ts(state in memory only: streams and tabs, visibility per recipient, TTL),services/presence-model.ts(pure, also used in the browser:PRESENCE_ROUTES– page → area, own real time, cursor kind –, page address validation, switch), UIweb/src/presence/*(own chunk;store.tsin the first download for the sidebar and top bar). New pages register their area inPRESENCE_ROUTES; a surface of its own for mouse cursors is markeddata-presence-surface. Hooks:presenceHub,presence. Details: collaboration.md. -
Mobile app:
modules/mobile.ts(device tokenghm_…viaapp.bearerAuth,/api/mobile/*, notification feed),modules/mobile-app.ts(APK,/android, App Links),services/mobile.ts+services/mobile-read.ts. It checks permissions by running theapp.guardsagainst a typical route of the area (scope/checkArea), and writes throughcallRoute– the normal route runs in the same process with guards, events andafterWrite(audit log with pathapp); the outer route setsctx.skipAfterWritefor that./api/mobileis therefore deliberately not inpermissions.ts. -
Project planning & time tracking:
modules/pm.ts(projects, tasks, views,/api/pm/*),pm-time.ts(timer, entries, timesheets, billing),pm-automation.ts(rules, workflow triggerstask.*and actionspm_*, overdue scan),pm-portal.ts(customer portal viaportalHeader/portalDashboard/portalMemberCheck),pm-tools.ts(17 MCP tools, report sourcestime_entries/pm_projects),pm-mobile.ts(/api/mobile/pm/*). The logic lives inservices/pm-*.ts;pm-schedule.ts,pm-tree.tsandpm-recur.tsare pure modules without imports (date maths, hierarchy rules, recurrence) and are also used by the Gantt view. Thetaskstable is extended instead of replaced: triggers keepstatus_id/done/assigned_toin sync for all old writers (task list, workflows, Todoist, app), new tasks land in the project “General”. Hooks:app.services.pm.getTask,pmTime.autoStopTimers,pmScan.overdue/dueToday; tasks and projects are association types (task,pm_project) – this is how documents and chat of the collab modules attach to them. Costing (budgets with thresholds, cost rates, third-party costs, profitability, retainers):pm-costing.ts,pm-retainers.tswithservices/pm-costing*.ts/pm-retainers.ts, hookspmCosting/pmRetainers. Contract: docs/projects.md. -
Twilio helpers:
verifyTwilio(signature check) andtwilioRequest(REST with the sub-account’s credentials) frommodules/telephony.tsare also used by the AI phone agent (modules/voice-ai.ts, pure building blocks inservices/voice-ai.ts) and the browser phone (modules/browser-phone.ts,services/browser-phone.ts). -
Shared helpers:
services/markdown.ts(safe Markdown, video embedding, excerpts) for blog and courses;services/courses.ts(access, drip, progress) for courses, the member area and workflows;services/email-render.ts(email designer: design JSON → HTML + text, without imports, also used by the UI for the preview) withservices/email-design.ts(rendersdesignwhen templates, campaigns and workflow steps are saved);services/community.ts(access, levels, word filter, image check) andservices/community-actions.ts(posts, points, notifications, moderation, digest) for community admin (modules/community.ts) and member pages (modules/community-portal.ts). -
Secrets in settings (passwords, tokens) never go back to the browser (
maskSettings). They are stored sealed:sealSecret/openSecret/openIfSealedfromcore/secrets.ts(AES-256-GCM, data key + earlier keys,npm run rotate-secret);app.location()itself seals credentials in the sub-account settings (sealSettings). -
Provider links:
modules/referrals.ts(catalog,REFERRAL_DEFAULTS,/go/:provider,app.services.signupLink(id)), in the UI<SignupLink id="…" />fromweb/src/referrals.tsxnext to every key field – docs/referrals.md. -
Nango & MCP client:
services/nango.tsis the only place that talks to Nango (test hooknangoFetch),modules/nango.tsprovidesapp.services.nango(requestwith an approval list).services/mcp-client.tsis the MCP client (Streamable HTTP + SSE, OAuth 2.1 client, sanitizing of tools/results; test hookmcpClientFetch),modules/mcp-client.tsmanages external servers (app.services.mcpClient). Two general hooks:agentToolSources(tool sources for the AI agent, contract in mcp-client.md) andmcpToolSources(async (caller) => Tool[]– tools of/mcpthat depend on the caller).safeFetchknowsallowPrivatefor targets that the agency has explicitly allowed as an “internal network”. -
Connectors:
services/composio.tsis the only place that talks to Composio (test hookcomposioFetch); other modules useapp.services.composio(keys, connections,executewith an approval list, webhook) andcomposioTriggers/composioAgent. Direct connections register withregisterConnectorfromservices/connectors.ts(fields, test, workflow actions, catalog entry); the framework (modules/connectors.ts) providesconnectorConfig/connectorStatus, and outgoing calls go throughconnectorFetch(test hook, otherwisesafeFetch). Guide: docs/connectors.md. -
Bookkeeping live sync & Google offline conversions:
modules/books-sync.ts(Lexware Office/sevDesk: connection per sub-account with a sealed key, queuebooks_sync_jobs– one job per document, listener oninvoice.finalised, payment reconciliation in the tick viarecordPayment, remote ID ininvoice_links), logic inservices/books-sync-model.ts(pure: documents, mapping, rate limiter, backoff) andbooks-sync-http.ts(clients viasafeFetch, test hooksbooksSyncFetch/booksSyncSleep);modules/ads-conversions.ts+services/ads-gconv.ts(UploadClickConversions through the Google connection of the ads manager, consent check viaapp.services.consent.status, hookapp.services.adsGoogleConversions). Docs: integrations.md, ads-manager.md. -
Werkstatt, plugins, Marktplatz:
modules/werkstatt.ts(jobs: plan → consent → approval → build, progress, cost, policy),modules/plugins.ts(plugin storage, bridge, isolated UI, jobs, export/import),modules/marktplatz.ts(catalog, install with consent, review, publish),modules/werkstatt-core.ts(core feature proposals viaapp.services.githubIssues),modules/werkstatt-tools.ts(MCP tools) andmodules/flags.ts(app.services.flags.enabled(name, {locationId, userId})); the pure logic lives inservices/werkstatt/*(manifest, checks, bridge, sandbox runner, declarative steps, notifications), the names “Werkstatt”/“Marktplatz” inservices/werkstatt/names.ts. Hooks:app.services.plugins,werkstatt(idle()for tests),pluginPreviewUrl,pluginScreenshot(test hook for preview images),agentPromptExtras(additions to the agent prompt),githubIssues(create/status/hasToken/defaultRepo),workflowActions.plugin_action;app.guardsrestrictvia: plugin/werkstattto approved routes. The test hooksaiConnectionsFetchandfeedbackMailercover AI providers and emails. Security model and limits: werkstatt.md. -
Security building blocks:
safeFetch(services/net.ts) for every URL that comes from a user,contentMatches/extFor(core/files.ts) for uploads,safeEqual/verifyPasswordAsync(core/util.ts),clampLimit(core/http.ts) for page sizes,csvSafe(core/csv.ts) for exports,oneLine(services/messaging.ts) for mail headers.app.services.bodyLimits({match(method, path, req), bytes}) lets public routes exceed 1 MB;app.afterReadruns after every signed-in GET (this is how the audit log captures exports). GDPR:app.services.privacyExport/privacyErase((app, locationId, contact)) for a module’s own data (modules/privacy.ts; runs inside the delete transaction),contactFiles((app, locationId, contactId) → paths– files of a contact that go along on deletion and on GDPR erasure,services/contacts.ts).tests/tenant-isolation.test.tscovers new routes automatically – always filter lookups bylocation_id. Details: security.md. -
Landing page (
PGHL_LANDING=1):modules/landing.ts(routes/,/de,/vergleich…,/sitemap.xmlonly on the instance’s own host; data, star-count cache) with the pure partsservices/landing-calc.ts(HighLevel prices with date, calculator, share links),landing-compare.ts(research matrix),landing-stack.ts(“Replaces your stack”),landing-switch.ts,landing-copy.ts(DE/EN, one interface),landing-waitlist-copy.ts(the texts in cloud waitlist mode) andlanding-page.ts(HTML, CSS, the hash-allowed inline script). Hooks:robotsExtra(lines for/robots.txt),landingFetch(test hook for GitHub),landing(refreshStars/stats/matrix). Data and images inweb/public/landing/– docs/landing.md. -
Hosted cloud (
PGHL_CLOUD_MODE=1): every customer is a tenant (cloud_tenants) around one primary sub-account (Agency plan: plus client sub-accounts incloud_locations); billing sits on the primary’ssaas_subscriptionsrow, so the SaaS statuses, usage gate, read-only mode and banner are reused. Modules (all return early when the mode is off):modules/cloud.ts(tables, settings inagency_settings.cloud, plan sync intosaas_plans, guards,hideUser,/api/cloud/me|plans),cloud-pages.ts(/cloud,/start, legal pages – server-rendered, cookie-free, script-free CSP),cloud-signup.ts(form, verification, Google, Checkout, provisioning, housekeeping),cloud-billing.ts(plan changes, portal, cancel, invoices, Stripe event taps,reconciletick: trial, dunning, read-only, deletion schedule),cloud-account.ts(team, clients, branding, domain, export, delete, support consent, AVV, onboarding),cloud-console.ts(operator, admin-only),cloud-waitlist.ts(waitlist mode – with cloud mode or the landing page: a router wrapper refuses the signup routes, the waitlist form’s endpoint into the landing hook’s form or the built-incloud_waitlisttable, the operator’s list;app.services.cloudWaitlist). Logic inservices/cloud.ts(plans, entitlements, settings validation, pure timeline maths),cloud-legal.ts,cloud-mail.ts,cloud-export.ts(ZIP export and the erase that finds every row),cloud-pages.ts,cloud-waitlist.ts(texts and form of the waitlist mode). Hooks inapp.services:saasEventTaps(functions called after every handled Stripe event of the agency webhook),cloudCompleteSignup(SaaS webhook → cloud for checkouts withcloud_signup_id),hideUser(user, locationId)withhiddenUser()incore/permissions.ts(people lists skip operator admins in tenants),hiddenLocations(user)withvisibleLocations()(the operator’s/api/me,/api/locations, agency overview and projects skip customers not open for support),cloudMailer/cloudFormMinMs/ssoFetch(test hooks),cloud(info/reconcile/tenantOfLocation/tenantOfUser/resetLimits/cloudGoogleReady).GET /api/versioncarriescloud. Decision record, setup and limits: docs/cloud.md. -
Operations (backups, alerts, status page):
modules/offsite-backup.ts(hourly, encrypted off-site backups to S3-compatible storage, monthly restore test; logic inservices/backup-offsite.ts,backup-crypto.ts,s3-lite.ts,backup-retention.ts, commandcli/backup-restore.ts),ops-alerts.ts(wraps the/healthzroute ofcore.tsviarouter.wrappers, chainsapp.services.serverErrorfor the error rate; hookapp.services.ops–health/evaluate/send–,opsAlert.event(alert)for one-off alerts from other modules andopsEveryMinute– functions after every evaluation),status-page.ts(public page/status, incidents). In cloud modecloud-abuse.ts(DSA notices, measures, automatic sending pause; wrapsusageGate,app.services.cloudAbuse) andcloud-sites.ts(customer websites never under the login address, own domains only after TXT confirmation; wrapswantedDomains/domainStatus). UI through the UI slotsagency.systemandcloud.abuse. Details: backups.md, cloud.md. -
Payment providers, online store, gift cards (payment-providers.md, store.md): one registry line
...storeModules–modules/pay-registry.ts(Stripe, PayPal, Mollie behind a small interface fromservices/pay-registry.ts:create/fetch/refundper provider inservices/pay-stripe.ts,pay-paypal.ts,pay-mollie.ts; payment page/pay/<id>, webhooks, connection to invoices, purchase links and appointments),giftcards.ts(ledger with triggers, reservations),store.ts,store-public.ts(cart, checkout, blocksstore_products/store_product),store-tools.ts; pure logic inservices/store-model.ts(shipping, tax type, quote =calcTotalsof the invoice lines) andservices/store-catalog.ts. Hooks:registerPayProvider/registerPayPurpose(custom providers and purposes –invoice,product,teamcal_hold,store_order),app.services.payRegistry(start/checkout/refund/refundAll/choiceNeeded… – used byservices/teamcal-pay.ts),payBuy/buyPayNote(purchase page with another provider, handed over bymodules/stripe.ts), the chainedinvoicePayHtml,giftCards(hold/commit/release/payInvoice…),store; UI slotsettings.payments; test hookpayFetch(PayPal and Mollie). -
HighLevel parity (quick wins):
modules/tag-meta.ts+services/tag-meta.ts(colors and categories of tags, snapshot parttag_meta; chips in the UI viaweb/src/tag-chip.tsx),modules/pipeline-board.ts(order of the pipelines; stage colors instages[].color),modules/deal-list.ts+services/deal-query.ts(list view of deals: filters/sorting forGET /api/opportunitiesthrough the filter engine fromservices/objects.ts, bulk actions, CSV; building blockweb/src/pages/ListKit.tsx),modules/inbox-spam.ts(blocked senders),modules/call-scripts.ts(call scripts; in the UI via the slotcall.panel–web/src/slots.tsxis the list of UI slots, one line per filling),modules/notes-plus.ts(title, color, pinning, Markdown and search of notes; hooknoteExtras– additional columns for the workflow stepadd_note). New hookinboundGates(services/inbound-gate.ts): every inbound path (email/SMS webhooks, Meta channels,channel-kit, mailbox sync) asks it before a message counts – a gate that claims the message archives it, and there is nomessage.inbound. Details: team-inbox.md, api.md. -
HighLevel parity (CRM, inbox, automations): one registry line
...crmGapModules(modules/crm-gaps.ts) –tag-bulk.ts(usage, rename/merge/delete/color in SQL, undo via the entitytag),inbox-filters.ts(filterstag/pipeline_id/stage_id/ownerviaconversationFilters, full-text searchGET /api/conversations/searchover the FTS5 tablemessages_fts, maintained by SQL triggers onmessages),contact-bulk-owner.ts,field-cleanup.ts(undo entitycustom_field),message-triggers.ts(triggersmessage.outbound/message.team_reply: SQL triggers write sent messages intomessage_outbound_queue, emptied after every request viaafterWriteand in the tick),contact-documents.ts(files on contacts and on notes, bytes incontact_document_blobs),workflow-folders.ts(folders,workflows.folder_id,POST /api/workflows/bulkviacallRoute),field-history.ts(tablefield_changes, actor fromafterWrite/viaWorkflow),deal-smart-tags.ts,deal-forecast.ts,split-winner.ts(optionauto_winneron the stepsplit_test, check in the tick),company-bulk.ts(companies in bulk viacallRoute),smart-lists.ts(Smart Lists 2.0: columns, sorting, shared/private on the tablesmart_lists, routes/api/smart-lists*, hookcontactOrder, MCPghl_smart_lists/ghl_smart_list_contacts; pure parts inservices/smart-lists.ts, UIweb/src/pages/SmartLists.tsx, i18n areasmartlists). UI:web/src/pages/TagBulk.tsx,InboxFilters.tsx,ContactBulkOwner.tsx,FieldCleanup.tsx,ContactDocuments.tsx,WorkflowFolders.tsx,DealSmartTags.tsx,DealForecast.tsx(one line each in the page that shows them), i18n areacrmgaps. -
HighLevel parity (reach):
modules/review-sources.ts+services/review-sources.ts(reviews from further platforms: adapter registryregisterReviewAdapter– Facebook, Trustpilot, ProvenExpert, Yelp, Tripadvisor, import via CSV/RSS/webhook –, one tablereview_items, triggerreview.receivedwithsource, AI drafts viaapp.services.reviewReplies.suggest, widget viaapp.services.reviewWidgetParts, test hookreviewSourceFetch– reputation.md),modules/social-comments.ts+services/social-comments.ts(comments from Facebook/Instagram viametaWebhookHooks, YouTube/LinkedIn viaapp.services.nango, channelcommentin the inbox, actions viamessageRowsand the UI slotinbox.message;app.services.socialComments.ingest– social-comments.md), several Facebook pages/Instagram accounts inmodules/meta-channels.ts(tablemeta_pages, mapping by page ID,meta_identities.account_id,metaKit.graph(…, accountId)),modules/voice-outbound.ts(workflow actionvoice_ai_call, check viaapp.services.dialer.verdict+ daily limit, Twilio with answering-machine detection,VoiceAgentInput.stage = 'outbound', hookvoicePromptExtras, four outcome triggers – voice.md) andmodules/caller-ids.ts(verified own numbers as caller ID,app.services.callerIds.pick/allowedfor the browser phone, click-to-call, power dialer and AI calls – telephony.md). UI through the UI slotsreviews.panel,channels.meta,voiceai.panel,telephony.settings(web/src/slots.tsx). -
HighLevel parity (booking, forms, payment, store, contracts): one registry line
...parityBookingModules(modules/parity-booking.ts) –contract-signers.ts(several signers with roles and order; hooks into/api/contracts/:id/send,/s/:tokenand the single-signature routes ofcontracts.tsviarouter.wrappers, tablescontract_sign_runs/contract_signers, triggercontract.signer_signed),service-menu.ts(service menu/book/s/<slug>viabookTeamwithhostId),calendar-ai.ts(explainSlotfromcomputeSlots/slotsOfDayandcheckTime, MCPghl_calendar_create/ghl_calendar_explain_slot),form-pdf.ts(PDF of a submission, MCPghl_forms/ghl_form_submission_pdfs),form-drafts.ts(save progress: script attached via wrapper to/f/:id, tableform_draftswith token hash),coupon-codes.ts(one-time discount codes asbuy_couponswithmax_uses1, workflow stepcoupon_create,issueCodes),loyalty.ts(loyalty points ledger frominvoice.paid, tiers, rewards viaissueCodes/app.services.giftCards),store-extras.ts(packing slip PDF, abandoned carts),portal-appointments.ts(“My appointments” viaportalHeader/portalDashboard/portalMemberCheck),booking-guests.ts(guests frommeta.guestsofappointment.booked, invitations,appointmentRows),event-checkin.ts(ticket page with QR viarenderQr, check-in viacallRouteonPATCH /api/appointments/:id),booking-multi.ts(several times in one booking viabookTeam, withdrawal viacancelAppointment),rentals.ts(rentals: rental offer = calendar withsettings.rental, tablesrentals/rental_bookings, availability and writing in one transaction viaservices/rentals-book.ts, payment via the purposerentalof the payment registry, pages/book/rent/<slug>and via the chainsteamcalPage/teamcalManagePage/teamcalCancel/teamcalVars). In the team calendar itself:show_ifon questions (checkConditions/questionShowninservices/teamcal-model.ts),guests_max,durationsandmulti_maxin the configuration (guestsAllowed,durationChoices,multiAllowed). UI slotscontract.signersandgiftcards.panel. -
HighLevel parity (websites, marketing, agency – wave 10): one registry line
...gapsSitesModules(modules/gaps-sites.ts) forsite-seo.ts(SEO check, Schema.org per page),site-dyntext.ts({{url.…}},{{#if url.…}}),site-fonts.ts(custom font files),site-protect.ts(password protection – the lastpageServewrapper),social-approval.ts+social-queues.ts(viaapp.services.socialPost),agency-banners.ts,role-templates.ts(second guard for read-only areas,afterWritedetaches the role on a manual change),template-conditions.ts(new hooktemplatePassincore/template.ts, parserservices/template-conditions.ts),video-testimonials.ts(raw upload viarawBodyLimit) andprompt-lab.ts(viaaiRun, apply viacallRoute). UI through the new UI slotsshell.banner,agency.clients,settings.team,agents.testsandreviews.panel; teststests/gaps-sites*.test.ts.
Reports: custom data sources
Section titled “Reports: custom data sources”Reports & dashboards (modules/reports.ts, engine in services/report-engine.ts, built-in sources in services/report-sources.ts) read everything through the list app.services.reportSources. A module registers its table with a few lines – no matter whether it is registered before or after the reports module:
import type { ReportSource } from '../services/report-engine.js';import { contactJoin, lookupTable } from '../services/report-sources.js';
const ticketContact = contactJoin('t.contact_id'); // optional: contact fields as “Contact: …”const TICKETS: ReportSource = { key: 'tickets', label: 'Tickets', area: 'conversations', // permission that client logins need (+ SaaS plan) from: 'tickets t', location: 't.location_id', dateField: 'created_at', requires: ['tickets'], countLabel: 'Anzahl Tickets', joins: ticketContact.joins, fields: [ { key: 'created_at', label: 'Erstellt am', type: 'date', sql: 't.created_at' }, { key: 'status', label: 'Status', type: 'enum', sql: 't.status', options: { open: 'Offen', closed: 'Geschlossen' } }, { key: 'agent', label: 'Bearbeiter', type: 'string', sql: 't.assigned_to', lookup: lookupTable('users', 'name'), empty: 'Nicht zugewiesen' }, { key: 'hours', label: 'Lösungszeit', type: 'number', sql: '(julianday(t.closed_at) - julianday(t.created_at)) * 24', format: 'number' }, ...ticketContact.fields, ], calcs: [{ key: 'close_rate', label: 'Lösungsquote', sql: `100.0 * SUM(t.status = 'closed') / NULLIF(COUNT(*), 0)`, format: 'percent' }],};app.services.reportSources = [...(app.services.reportSources ?? []), TICKETS];sql,from,joinsandcalcsare fixed code – user values enter the query only as parameters; the engine itself appends the sub-account filter (location IN (…)), the period, filters,LIMITand a time limit.- Field types:
string,enum(withoptions),number(format: number, money – in euros –, percent, days, minutes),date(ISO timestamp; grouping by day … hour in the sub-account’s time zone),boolean(SQL returns 1/0),tags(JSON array).lookuptranslates IDs into names (and order, e.g. pipeline stages);noGroupfor free text. - Every source automatically gets the group Sub-account; sources with missing tables (
requires) are hidden. A test that compiles all fields is intests/reports.test.ts.