Skip to content

Communication Templates — Developer Guide

Reusable platform module for plain-text communication templates. Templates are licensed via the marketplace catalog (communication-templates) and authorized via Spatie permissions. The first consumer is Leads → WhatsApp Web (wa.me). There is no Meta/Twilio/server-side send in this version.

Purpose

  • Give workspaces a single place to author reusable messages
  • Keep message composition module-agnostic through a placeholder registry
  • Let product modules (Leads today; Contacts/etc. later) open a picker, render, and hand off to an external channel UI

Templates store plain text only (no HTML/rich text). Channels include whatsapp, sms, and email in the enum; MVP create/update allows whatsapp only.

Architecture

text
Product UI (e.g. Lead detail)
  → POST /communication-templates/{uuid}/render
      → CommunicationTemplateService
          → TemplateRenderer
              → PlaceholderRegistry
                  → context PlaceholderProvider (e.g. Lead)
                  → shared SharedPlaceholderProvider (agent / workspace / system)
          → channel extras (WhatsApp phone + wa.me URL)
  → SPA opens wa_me_url in a new tab
ConcernImplementation
LicensingCatalog module communication-templates + module: middleware
Authorizationcommunication-templates.{view,create,update,delete,use}
StorageTenant-scoped communication_templates rows
PlaceholdersModular providers registered on PlaceholderRegistry
RenderingGeneric TemplateRenderer (module-agnostic)
AuditDomain events → CommunicationTemplateEventSubscriberPlatformAuditService

Backend layout

PiecePath
Modelapp/Models/CommunicationTemplate.php
Enumapp/Enums/Tenant/CommunicationTemplateChannelEnum.php
Contractsapp/Contracts/CommunicationTemplates/PlaceholderProvider.php, SharedPlaceholderProvider.php
Support DTOsapp/Support/CommunicationTemplates/*
Serviceapp/Services/Tenant/CommunicationTemplates/CommunicationTemplateService.php
Registry / rendererPlaceholderRegistry.php, TemplateRenderer.php
ProvidersProviders/LeadPlaceholderProvider.php, Agent*, Workspace*, System*
ControllerCommunicationTemplateController.php
Requestsapp/Http/Requests/Tenant/Api/V1/CommunicationTemplate/*
Resourcesapp/Http/Resources/Tenant/Api/V1/CommunicationTemplate/*
Policyapp/Policies/CommunicationTemplatePolicy.php
Events / subscriberCommunicationTemplateCreated/Updated/Deleted + CommunicationTemplateEventSubscriber
Teststests/Feature/Tenant/CommunicationTemplates/*, tests/Feature/ProductionModuleDeploymentTest.php

Frontend layout

PiecePath
List pagesrc/pages/communication-templates/communication-templates-page.tsx
Form dialogtemplate-form-dialog.tsx (placeholder chip inserter)
WhatsApp pickersrc/components/crm/whatsapp-template-picker-dialog.tsx
Lead wiringWhatsApp action in lead-detail-sheet.tsx
API clientcommunicationTemplateService in src/api/services.ts
E2Ee2e/tests/communication-templates/, npm run test:e2e:communication-templates

Data model

Table: communication_templates

ColumnNotes
tenant_idWorkspace FK
uuidPublic route key (getRouteKeyName()uuid)
nameUnique per tenant + context + channel among non-deleted rows
contextSingle context slug (e.g. leads) — drives placeholder set
channelEnum string; MVP writes: whatsapp
categoryOptional free-text grouping
bodyPlain text, max 4000; only known
is_activeInactive templates hidden from pickers; still editable
created_by / updated_byNullable user FKs
last_used_atSet on successful render
soft deletes

Indexes cover (tenant_id, context, channel, is_active), name, and last_used_at.

Domain rules

  • One context per template — filters placeholders and picker lists
  • Tokens are (optional surrounding whitespace inside braces)
  • Unknown tokens are rejected on save
  • Render replaces missing/null values with "" and returns missing_placeholders
  • WhatsApp phone normalization: digits only, minimum 8 digits; no silent country-code inventing
  • Render requires an active template and (for WhatsApp) a valid entity phone

Placeholder registry

The registry is modular and extensible. Context providers own entity-specific tokens; shared providers merge into every context.

Contracts

  • PlaceholderProvidercontext(), label(), definitions(), resolve(Model, ctx), findEntity()
  • SharedPlaceholderProvidergroup(), definitions(), resolve(ctx) (no entity)

Registration

Bound as a singleton in AppServiceProvider:

php
new PlaceholderRegistry(
    contextProviders: [
        $app->make(LeadPlaceholderProvider::class),
        // future: ContactPlaceholderProvider::class, …
    ],
    sharedProviders: [
        $app->make(AgentPlaceholderProvider::class),
        $app->make(WorkspacePlaceholderProvider::class),
        $app->make(SystemPlaceholderProvider::class),
    ],
);

Built-in keys

Lead context: name, email, phone, company, job_title, source, status, priority, lead_value, stage_name, assignee_name

Shared — agent: agent_name, agent_email

Shared — workspace: workspace_name

Shared — system: today, now (workspace timezone)

Rendering & preview flow

EndpointPermissionEntityBehavior
POST …/previewview or useoptionalWithout entity_id: sample + live shared values. With entity_id: live resolve (entity view policy). No last_used_at update.
POST …/renderuserequiredLive resolve; WhatsApp extras (phone, wa_me_url); updates last_used_at.

TemplateRenderer is generic: it does not know about Leads beyond what the registry returns for template.context.

WhatsApp flow (Leads)

  1. User opens a lead with a phone number
  2. SPA opens the template picker (module installed + communication-templates.use)
  3. User selects a template → POST …/render with entity_id
  4. API returns wa_me_url (https://wa.me/{digits}?text=…)
  5. Browser opens the URL; the agent sends from WhatsApp Web/app

SaleOS does not send messages or store outbound message history in this version.

Future Cloud API send/receive, conversations, and shared inbox are documented separately — see WhatsApp Cloud Integration (planned; not implemented).

Permission model

Config: config/tenant-permissions.phpcommunication-templates actions.

PermissionPurpose
viewList, show, meta, preview
create / update / deleteCRUD
useRender / open channel handoff

Default role map (config/tenant-default-role-permissions.php):

  • admin — all
  • manager — all except delete
  • staffview + use
  • superadmin (owner) — all permissions via provisioning / additive deploy grants

Routes: module:communication-templates then can: / policies.

Production registration (no seeders)

Communication Templates ships with data migrations, not production db:seed:

MigrationResponsibility
Schemacreate_communication_templates_table
Catalogregister_communication_templates_module via DefaultModuleRegistrar
Permissionsadd_communication_template_permissions via TenantPermissionSynchronizer

Production:

bash
php artisan migrate --force
php artisan optimize

Do not run CatalogSeeder or permission seeders in production for this module. See deployment/communication-templates.md.

Extension guide (future product modules)

To add a new context (e.g. Contacts):

  1. Implement PlaceholderProvider for the context (definitions + resolve + findEntity)
  2. Register it in the PlaceholderRegistry singleton
  3. Expose picker UI on that module’s detail surface
  4. Gate with module entitlement (communication-templates installed) and communication-templates.use
  5. Call preview/render with the new context’s entity_id
  6. Add Pest coverage for the provider + an E2E path if the UI is user-facing

No changes to TemplateRenderer are required when placeholders and context registration stay behind the contracts.

Official documentation for the SaleOS SaaS Platform.