Skip to content

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)
server/modules/memberships.ts
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 with location filters on ctx.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.contactId starts workflows for that contact.

  • Permissions: app.guards run after sign-in; the agency module checks the areas of restricted logins there (server/core/permissions.ts maps 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() from web/src/i18n (const t = useT('bereich'), the German text is the message ID); the English translations live per area in web/src/i18n/en/*.ts. npm run i18n:check keeps areas with complete: true in web/src/i18n/registry.ts complete. Each person’s language is stored by modules/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 new WHERE/ORDER BY on large tables, read-only lists/reports through cached() (core/cache.ts) or app.db.memo, background jobs in slices of at most ~50 ms (sliceTimer), new pages with lazyPages – 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.services instead of direct imports: instrumentEmail/personalizeLinks (tracking → sending and mailbox sending; instrumentEmail sets click links and the counting pixel, trackingCommit takes 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 for POST /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 /blog and 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/buyAfterPaid and stripeBuy (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 – with prepare so 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 fake oauthFetch), googleBusinessReady/googleBusinessFetch/googleBusinessStatus (Business Profile → Google reviews instead of the Places API). externalBusy is 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: registerType for 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/refsOf of 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 in ctx.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/updateTicket for 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), socialPost with mediaUrls + video (video via a signed link to Postiz, then also YouTube/TikTok) and thread (further parts as replies, e.g. an X thread), aiRun with timeoutMs for 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 behind deliverEmail/deliverSms – they find the app through the non-enumerable property app of the sub-account row from app.location()), test hooks channelFetch/discordSocket (docs/channels.md). A RawResult may have a readable stream as body (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 for aiRun; 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), mcpTools is 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 provider mailbox plugs into deliverEmail via emailTransports (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): callRoute from services/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 option payload of metaSend (messages with buttons), creatorPlans, dunning/dunningInvoices/billingPage (dunning and subscription page), quizKit/assignmentKit/certificates (quizzes, assignments, certificates), igComments. The AI usage workflows (mapping in the AI provider) serves the AI workflow builder (aiRun with the purpose workflow_builder).

  • Knowledge memory (Cognee): modules/cognee.ts (tables, agency + sub-account routes, governance status), cognee-sync.ts (scheduler job, privacyErase hook queues the deletion of a contact’s data items, app.services.cognee), cognee-tools.ts (MCP/agent tools ghl_memory_recall/ghl_memory_remember); logic in services/cognee/* (client.ts REST client with the sub-account-bound LocationClient, sources.ts one KindDef per kind of record, sync.ts engine + deletion queue, recall.ts, redact.ts, model.ts). Hooks: app.services.cogneeFetch/cogneeSleep/cogneeNow/cogneeGapMs (test hooks), the optional untrusted: true on an MCP Tool (the agent treats its output as untrusted). Dataset isolation, erasure and what is sent: docs/cognee.md.

  • Consent & legal: modules/consent.ts (ledger consent_events/consent_texts/consent_doi, double-opt-in tick, routes, triggers consent.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 in services/consent-model.ts (pure), consent.ts, consent-proof.ts, cmp.ts (pure), legal-texts.ts (pure), legal-pdf.ts. Hooks in app.services: consentGate (services/messaging.ts and WhatsApp sending ask it before every marketing message; purpose: 'transactional' for service messages), consent.* (capture, status, allowMarketingMany for audiences – callers use ?.), cmp (attributionConsent, linkCmp), htmlTransforms (core: post-process public HTML pages, in a try/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 (table undo_entries, guard + after-write hook, routes /api/undo/*, retention, GDPR step), modules/undo-entities.ts (the records and their routes), modules/undo-tools.ts (MCP ghl_undo_list/ghl_undo, /api/mobile/undo/*); engine in services/undo-journal.ts, undo-core.ts, undo-table.ts, undo-versioned.ts. Hooks: app.services.undo with register(type, {snapshot, restore, label, …}) (what a record is and how it comes back – tableEntity covers the table including foreign keys), track({method, pattern, entity, targets, action, when, notice, coalesceMs, quiet}) (which route changes which records), enabled, cleanup(at); ctx.undoSink collects the entries of an agent tool; Db.tx nests (savepoints). New modules register their records there – contract and limits: docs/undo.md.

  • Website builder (sites.md): modules/funnels.ts (pages, delivery servePage, 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 widget funnel_overview, MCP ghl_funnel_analytics), site-templates.ts (23 templates), site-media.ts (image sizes, /uploads-opt), site-snapshot.ts (copying); pure logic in services/site-*.ts (blocks, layout, page, design tokens, redirects, search, check, images, templates). Hooks in app.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}; sharp is detected, never required) as well as the existing pageServe, pageVisit, htmlTransforms, buyPageHtml, buyCheckout, boardWidgets, mcpTools, snapshotParts. UI: web/src/pages/site/* as separate chunks (lazyPages), tree operations purely in model.ts.

  • Funnel & Website AI (site-ai.md): modules/site-ai.ts (brief → page plan → block JSON, runs, preview, apply, MCP ghl_site_generate, agent hint) and site-ai-drafts.ts (unpublished AI websites through a wrapper around app.services.siteRedirect, restore points, AI writer for blog and email) – one registry line ...siteAiModules; pure logic in services/site-ai-schema.ts (block schema against siteBlocks(app), validating, compiling, merging a section), services/site-ai.ts (prompts, plan/page validation, section commands, diff, estimate) and services/site-ai-kit.ts (model, budget, estimate, ask-check-repeat loop askChecked – also used by modules/agent-ai.ts, “Build with AI” for AI agents → draft agent, MCP ghl_agent_generate). Hooks: siteAiDrafts (mark/isDraft/restorePoint); BYOK usage sites.

  • Custom Values (custom-values.md): modules/custom-values.ts (tables custom_values, custom_value_form_targets, routes, pageServe replacement on published pages, snapshot part with placeholders, undo entry, MCP), pure helpers in services/custom-values.ts. For this, core/template.ts has a small namespace registry (templateNamespace(name, resolver)): {{custom_values.key}} therefore works in every render(); 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 router wrappers hook that checks every request of a restricted person, app.services.recordAccess), deal-lines.ts, quotes.ts (+ quote-model/engine/page, signature via createSignedContract in contracts.ts, deposit invoice through the invoice flow), sequences.ts (+ sequence-model/engine, sends through sendMail, reply sweep on the tick), contact-duplicates.ts (+ contact-dedupe/merge, undo through the journal entity contact_merge), quotas.ts (+ board widgets), meeting-notes.ts (app.services.attachCallSummary); internal approval quote-approvals.ts (+ services/quote-approval.ts) through the hook app.services.quoteSendGates – sendQuote asks 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: aiRun with 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 in services/boards-*.ts: boards-layout.ts and boards-consts.ts are 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) with boards-widgets-core.ts (the 13 built-in widgets) and boards-presets.ts, boards-data.ts (widget data through callRoute as the viewer), boards-store.ts (state, versions, access), boards-hub.ts (SSE hub with revalidation), boards-embed.ts (host allow-list). Hooks in app.services: boardWidgets (use boardWidgets(app).register(def) from services/boards-widgets.ts; or push a definition onto boardWidgetDefs before the module starts), boards (apply/load/full/data/list/registry/hub), boardsEmbedHosts, boardsStart, boardsHub. Permission area dashboard (/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 + lazy spatial.css, type scale, Workspace.tsx, XrViewer.tsx and the in-house WebGL2 engine in web/src/xr/engine/), the preference spatial in appearance-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 through app.services.accounting.post.

  • Workflow builder (canvas, copy/paste, pause, validation): one module each – modules/workflow-canvas.ts (keeps settings.layout clean, via app.router.wrappers), workflow-clipboard.ts (POST /api/workflows/:id/copy|paste, format gohigh-steps), workflow-pause.ts (disabled: true → workflowStepSkips), workflow-validate.ts (issues in the responses of GET/PATCH /api/workflows/:id, POST …/validate); pure logic in services/workflow-validate.ts and services/workflow-layout.ts, which the browser uses too. UI web/src/workflow-canvas/*: tools.tsx (list/canvas switch, menu, toggles, hints – for both editors), WorkflowCanvas.tsx (own chunk via lazyPages), layout.ts/model.ts/stats.ts pure. Details: workflow-actions.md.

  • Version, demo mode, TLS: modules/version.ts (GET /api/version: name, version from package.json via server/version.ts, demo info), modules/demo.ts (PGHL_DEMO_MODE=1: seed from services/demo-seed.ts via callRoute, nightly reset, app.guards against dangerous writes, app.services.demo with info/reset/blocked) and services/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/ask for Caddy’s on-demand certificates, reads app.services.wantedDomains from the agency module).

  • Test hooks: memberMailer replaces the sending of login links (member area), communityMailer that of the community digest (communityDigest(at) starts it), affiliateMailer that of the partner area, adsFetch the Meta/Google APIs, restHookFetch the delivery of REST hooks, hubspotFetch/hubspotSleep/hubspotPauseMs the HubSpot API and its waiting, openshortsFetch the OpenShorts API, feedFetch the retrieval of the autopilot feeds – like *Factory in other modules.

  • AI tools & providers: aiProvider(locationId) (modules/ai-providers.ts: null = Claude, else the OpenAI provider with model, key, call and cost – aiRun dispatches to it, so every AI consumer works with either), openaiFetch/githubFetch (test hooks for the OpenAI and GitHub APIs), mcpAuth (list of resolvers bearer → {agency, locationId} – further credentials for /mcp, e.g. OAuth access tokens from modules/mcp-oauth.ts), mcpChallenge (the WWW-Authenticate header of a 401 from /mcp, pointing to the protected resource metadata), mcpOAuth (list/revoke connected apps for the AI tools page), brandName (white-label name for MCP metadata). Pure OAuth helpers (redirect URI rules, PKCE, metadata) live in services/mcp-oauth.ts, the Responses API client in services/openai.ts.

  • AI studio (media generation): modules/genmedia.ts (routes, callbacks, files, app API) with one adapter per provider in services/genmedia/ (fal, kie, replicate, openai, gemini, stability, elevenlabs, runway, luma, custom-http) and the job engine services/genmedia/engine.ts. It reads keys only through aiConnections (resolve(locationId, capability, connectionId?)/list(locationId) – the BYOK registry of the agent module) and offers genmedia (create/get/out/process/pollDue/idle) to other modules; genmediaFetch is the test hook for all provider HTTP (URL checks still apply). Workflow actions genmedia_image/genmedia_video/genmedia_tts, trigger genmedia.completed, five MCP tools.

  • Voice layer (Stimme): services/voice-engine.ts (synthesize/transcribe/listVoices/liveTts/openLiveStt/phonePlan over the BYOK registry – the speech providers are AI studio adapters in services/voice-adapters.ts, registered with registerAdapter), 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.ts hands HTTP upgrades to app.services.upgradeHandlers), services/voice-audio.ts (G.711, resampling, WAV, VAD). modules/voice.ts (settings, push-to-talk, prefs, MCP, mobile relay) and modules/voice-phone.ts (Twilio hooks, recorded turns, voicemail). The phone agent’s brain stays in modules/voice-ai.ts and is offered as app.services.voiceBrain; voiceTransport lets 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 through app.services.companionCheckinHooks) and modules/companion-live.ts (event triggers via app.events listeners and its own scan in onTick, the initiative engine over aiRun, the Gemini Live proxy on upgradeHandlers at /ws/companion-live); pure logic in services/companion-prefs.ts, briefing.ts, companion-triggers.ts (registry, routing, rate limit), companion-initiative.ts (schema, cool-down, levels, learning) and gemini-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) and call-scorecards.ts; pure logic in services/ivr.ts, call-groups.ts, call-audio.ts, twilio-numbers.ts, dni.ts, dialer.ts, call-intel.ts. Hooks in app.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; roomClock decides waiting/running/late/ended), webinar-model.ts (types, defaults, honesty guard LIVE_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, action webinar_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 by umzug-write.ts’s Writer with 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), one umzug-<vendor>.ts per 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.ts check SPF/DKIM/DMARC/MX with an injectable DnsResolver; messaging.ts adds 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 hooks umzugFetch/umzugSleep/umzugPauseMs (fake vendor servers: tests/umzug-fakes.ts), mailDns (fake resolver) and mailCheckFetch (provider APIs). outbound-guard.ts also blocks the promise API’s DNS Resolver in 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 on conversations, views, assign / status / snooze / priority / notes / bulk, settings; hooks conversationFilters, conversationRows, inboxAudience), teaminbox-engine.ts (inbound handling, answer timers in SQL triggers, scheduler, /api/inbox/stream presence), teaminbox-automation.ts (three triggers, three steps, report sources), teaminbox-tools.ts (MCP and /api/mobile/inbox*); services teaminbox-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; services teamcal-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 the stripeSessions hook of modules/stripe.ts), teamcal-video.ts, teamcal-pages.ts. The booking pipeline in calendars.ts has 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 (table appointment_series, series_id/recurrence/occurrence_date/series_exception on appointments, rolling generation in the tick), calendar-view.ts (grid data, conflict check, move/resize through rescheduleAppointment or PATCH /api/appointments/:id, drawer, invoice draft, table appointment_invoices), ai-booking.ts (settings, MCP tools, /book/move/:token); logic in services/calview.ts (viewData, checkTime), services/appt-series.ts, services/appt-recur.ts (pure RRULE subset on top of pm-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 MCP Tool flag booking and the agent mode book (“Book appointments”) in services/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 namespace calview.

  • 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/articles routes via callRoute, AI helper through aiRun), 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 in ask mode 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 in services/: wiki-markdown.ts (escaping renderer + allow-list sanitiser, shared by pages, comments and chat), collab-fts.ts (FTS5 with LIKE fallback, joined by rowid so isolation never depends on the index), collab-cards.ts (live record cards; other modules add kinds with registerCardKind), 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 in app.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), and collabTasks.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 areas wiki and teamchat; 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 (tables office_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 kind office), office-collab.ts (edit lock, comments, people), office-tools.ts (read, edit, PDF, convert through the engines; five MCP tools ghl_office_*); logic in services/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 from PGHL_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 from PGHL_OFFICE_DIR with CSP and nonce, load/save bridge, signed links). Area office in core/permissions.ts, page office in PRESENCE_ROUTES; upstream programs and installation: deploy/office/. Details: docs/office.md.

  • Live collaboration everywhere: modules/presence.ts (routes /api/presence*, table presence_prefs, switch in locations.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), UI web/src/presence/* (own chunk; store.ts in the first download for the sidebar and top bar). New pages register their area in PRESENCE_ROUTES; a surface of its own for mouse cursors is marked data-presence-surface. Hooks: presenceHub, presence. Details: collaboration.md.

  • Mobile app: modules/mobile.ts (device token ghm_… via app.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 the app.guards against a typical route of the area (scope/checkArea), and writes through callRoute – the normal route runs in the same process with guards, events and afterWrite (audit log with path app); the outer route sets ctx.skipAfterWrite for that. /api/mobile is therefore deliberately not in permissions.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 triggers task.* and actions pm_*, overdue scan), pm-portal.ts (customer portal via portalHeader/portalDashboard/portalMemberCheck), pm-tools.ts (17 MCP tools, report sources time_entries/pm_projects), pm-mobile.ts (/api/mobile/pm/*). The logic lives in services/pm-*.ts; pm-schedule.ts, pm-tree.ts and pm-recur.ts are pure modules without imports (date maths, hierarchy rules, recurrence) and are also used by the Gantt view. The tasks table is extended instead of replaced: triggers keep status_id/done/assigned_to in 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.ts with services/pm-costing*.ts/pm-retainers.ts, hooks pmCosting/pmRetainers. Contract: docs/projects.md.

  • Twilio helpers: verifyTwilio (signature check) and twilioRequest (REST with the sub-account’s credentials) from modules/telephony.ts are also used by the AI phone agent (modules/voice-ai.ts, pure building blocks in services/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) with services/email-design.ts (renders design when templates, campaigns and workflow steps are saved); services/community.ts (access, levels, word filter, image check) and services/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/openIfSealed from core/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="…" /> from web/src/referrals.tsx next to every key field – docs/referrals.md.

  • Nango & MCP client: services/nango.ts is the only place that talks to Nango (test hook nangoFetch), modules/nango.ts provides app.services.nango (request with an approval list). services/mcp-client.ts is the MCP client (Streamable HTTP + SSE, OAuth 2.1 client, sanitizing of tools/results; test hook mcpClientFetch), modules/mcp-client.ts manages external servers (app.services.mcpClient). Two general hooks: agentToolSources (tool sources for the AI agent, contract in mcp-client.md) and mcpToolSources (async (caller) => Tool[] – tools of /mcp that depend on the caller). safeFetch knows allowPrivate for targets that the agency has explicitly allowed as an “internal network”.

  • Connectors: services/composio.ts is the only place that talks to Composio (test hook composioFetch); other modules use app.services.composio (keys, connections, execute with an approval list, webhook) and composioTriggers/composioAgent. Direct connections register with registerConnector from services/connectors.ts (fields, test, workflow actions, catalog entry); the framework (modules/connectors.ts) provides connectorConfig/connectorStatus, and outgoing calls go through connectorFetch (test hook, otherwise safeFetch). 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, queue books_sync_jobs – one job per document, listener on invoice.finalised, payment reconciliation in the tick via recordPayment, remote ID in invoice_links), logic in services/books-sync-model.ts (pure: documents, mapping, rate limiter, backoff) and books-sync-http.ts (clients via safeFetch, test hooks booksSyncFetch/booksSyncSleep); modules/ads-conversions.ts + services/ads-gconv.ts (UploadClickConversions through the Google connection of the ads manager, consent check via app.services.consent.status, hook app.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 via app.services.githubIssues), modules/werkstatt-tools.ts (MCP tools) and modules/flags.ts (app.services.flags.enabled(name, {locationId, userId})); the pure logic lives in services/werkstatt/* (manifest, checks, bridge, sandbox runner, declarative steps, notifications), the names “Werkstatt”/“Marktplatz” in services/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.guards restrict via: plugin/werkstatt to approved routes. The test hooks aiConnectionsFetch and feedbackMailer cover 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.afterRead runs 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.ts covers new routes automatically – always filter lookups by location_id. Details: security.md.

  • Landing page (PGHL_LANDING=1): modules/landing.ts (routes /, /de, /vergleich…, /sitemap.xml only on the instance’s own host; data, star-count cache) with the pure parts services/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) and landing-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 in web/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 in cloud_locations); billing sits on the primary’s saas_subscriptions row, 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 in agency_settings.cloud, plan sync into saas_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, reconcile tick: 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-in cloud_waitlist table, the operator’s list; app.services.cloudWaitlist). Logic in services/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 in app.services: saasEventTaps (functions called after every handled Stripe event of the agency webhook), cloudCompleteSignup (SaaS webhook → cloud for checkouts with cloud_signup_id), hideUser(user, locationId) with hiddenUser() in core/permissions.ts (people lists skip operator admins in tenants), hiddenLocations(user) with visibleLocations() (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/version carries cloud. 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 in services/backup-offsite.ts, backup-crypto.ts, s3-lite.ts, backup-retention.ts, command cli/backup-restore.ts), ops-alerts.ts (wraps the /healthz route of core.ts via router.wrappers, chains app.services.serverError for the error rate; hook app.services.ops – health/evaluate/send –, opsAlert.event(alert) for one-off alerts from other modules and opsEveryMinute – functions after every evaluation), status-page.ts (public page /status, incidents). In cloud mode cloud-abuse.ts (DSA notices, measures, automatic sending pause; wraps usageGate, app.services.cloudAbuse) and cloud-sites.ts (customer websites never under the login address, own domains only after TXT confirmation; wraps wantedDomains/domainStatus). UI through the UI slots agency.system and cloud.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 from services/pay-registry.ts: create/fetch/refund per provider in services/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, blocks store_products/store_product), store-tools.ts; pure logic in services/store-model.ts (shipping, tax type, quote = calcTotals of the invoice lines) and services/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 by services/teamcal-pay.ts), payBuy/buyPayNote (purchase page with another provider, handed over by modules/stripe.ts), the chained invoicePayHtml, giftCards (hold/commit/release/payInvoice …), store; UI slot settings.payments; test hook payFetch (PayPal and Mollie).

  • HighLevel parity (quick wins): modules/tag-meta.ts + services/tag-meta.ts (colors and categories of tags, snapshot part tag_meta; chips in the UI via web/src/tag-chip.tsx), modules/pipeline-board.ts (order of the pipelines; stage colors in stages[].color), modules/deal-list.ts + services/deal-query.ts (list view of deals: filters/sorting for GET /api/opportunities through the filter engine from services/objects.ts, bulk actions, CSV; building block web/src/pages/ListKit.tsx), modules/inbox-spam.ts (blocked senders), modules/call-scripts.ts (call scripts; in the UI via the slot call.panel – web/src/slots.tsx is the list of UI slots, one line per filling), modules/notes-plus.ts (title, color, pinning, Markdown and search of notes; hook noteExtras – additional columns for the workflow step add_note). New hook inboundGates (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 no message.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 entity tag), inbox-filters.ts (filters tag/pipeline_id/stage_id/owner via conversationFilters, full-text search GET /api/conversations/search over the FTS5 table messages_fts, maintained by SQL triggers on messages), contact-bulk-owner.ts, field-cleanup.ts (undo entity custom_field), message-triggers.ts (triggers message.outbound/message.team_reply: SQL triggers write sent messages into message_outbound_queue, emptied after every request via afterWrite and in the tick), contact-documents.ts (files on contacts and on notes, bytes in contact_document_blobs), workflow-folders.ts (folders, workflows.folder_id, POST /api/workflows/bulk via callRoute), field-history.ts (table field_changes, actor from afterWrite/viaWorkflow), deal-smart-tags.ts, deal-forecast.ts, split-winner.ts (option auto_winner on the step split_test, check in the tick), company-bulk.ts (companies in bulk via callRoute), smart-lists.ts (Smart Lists 2.0: columns, sorting, shared/private on the table smart_lists, routes /api/smart-lists*, hook contactOrder, MCP ghl_smart_lists/ghl_smart_list_contacts; pure parts in services/smart-lists.ts, UI web/src/pages/SmartLists.tsx, i18n area smartlists). 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 area crmgaps.

  • HighLevel parity (reach): modules/review-sources.ts + services/review-sources.ts (reviews from further platforms: adapter registry registerReviewAdapter – Facebook, Trustpilot, ProvenExpert, Yelp, Tripadvisor, import via CSV/RSS/webhook –, one table review_items, trigger review.received with source, AI drafts via app.services.reviewReplies.suggest, widget via app.services.reviewWidgetParts, test hook reviewSourceFetch – reputation.md), modules/social-comments.ts + services/social-comments.ts (comments from Facebook/Instagram via metaWebhookHooks, YouTube/LinkedIn via app.services.nango, channel comment in the inbox, actions via messageRows and the UI slot inbox.message; app.services.socialComments.ingest – social-comments.md), several Facebook pages/Instagram accounts in modules/meta-channels.ts (table meta_pages, mapping by page ID, meta_identities.account_id, metaKit.graph(…, accountId)), modules/voice-outbound.ts (workflow action voice_ai_call, check via app.services.dialer.verdict + daily limit, Twilio with answering-machine detection, VoiceAgentInput.stage = 'outbound', hook voicePromptExtras, four outcome triggers – voice.md) and modules/caller-ids.ts (verified own numbers as caller ID, app.services.callerIds.pick/allowed for the browser phone, click-to-call, power dialer and AI calls – telephony.md). UI through the UI slots reviews.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/:token and the single-signature routes of contracts.ts via router.wrappers, tables contract_sign_runs/contract_signers, trigger contract.signer_signed), service-menu.ts (service menu /book/s/<slug> via bookTeam with hostId), calendar-ai.ts (explainSlot from computeSlots/slotsOfDay and checkTime, MCP ghl_calendar_create/ghl_calendar_explain_slot), form-pdf.ts (PDF of a submission, MCP ghl_forms/ghl_form_submission_pdfs), form-drafts.ts (save progress: script attached via wrapper to /f/:id, table form_drafts with token hash), coupon-codes.ts (one-time discount codes as buy_coupons with max_uses 1, workflow step coupon_create, issueCodes), loyalty.ts (loyalty points ledger from invoice.paid, tiers, rewards via issueCodes/app.services.giftCards), store-extras.ts (packing slip PDF, abandoned carts), portal-appointments.ts (“My appointments” via portalHeader/portalDashboard/portalMemberCheck), booking-guests.ts (guests from meta.guests of appointment.booked, invitations, appointmentRows), event-checkin.ts (ticket page with QR via renderQr, check-in via callRoute on PATCH /api/appointments/:id), booking-multi.ts (several times in one booking via bookTeam, withdrawal via cancelAppointment), rentals.ts (rentals: rental offer = calendar with settings.rental, tables rentals/rental_bookings, availability and writing in one transaction via services/rentals-book.ts, payment via the purpose rental of the payment registry, pages /book/rent/<slug> and via the chains teamcalPage/teamcalManagePage/teamcalCancel/teamcalVars). In the team calendar itself: show_if on questions (checkConditions/questionShown in services/teamcal-model.ts), guests_max, durations and multi_max in the configuration (guestsAllowed, durationChoices, multiAllowed). UI slots contract.signers and giftcards.panel.

  • HighLevel parity (websites, marketing, agency – wave 10): one registry line ...gapsSitesModules (modules/gaps-sites.ts) for site-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 last pageServe wrapper), social-approval.ts + social-queues.ts (via app.services.socialPost), agency-banners.ts, role-templates.ts (second guard for read-only areas, afterWrite detaches the role on a manual change), template-conditions.ts (new hook templatePass in core/template.ts, parser services/template-conditions.ts), video-testimonials.ts (raw upload via rawBodyLimit) and prompt-lab.ts (via aiRun, apply via callRoute). UI through the new UI slots shell.banner, agency.clients, settings.team, agents.tests and reviews.panel; tests tests/gaps-sites*.test.ts.

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, joins and calcs are fixed code – user values enter the query only as parameters; the engine itself appends the sub-account filter (location IN (…)), the period, filters, LIMIT and a time limit.
  • Field types: string, enum (with options), 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). lookup translates IDs into names (and order, e.g. pipeline stages); noGroup for free text.
  • Every source automatically gets the group Sub-account; sources with missing tables (requires) are hidden. A test that compiles all fields is in tests/reports.test.ts.