Skip to content

Branded module — Developer Guide

Billable marketplace module (slug: branded, not default-included). Licensing: module:branded. Permissions: branded.view, branded.manage.

Domain model

Stancl domains rows:

ColumnNotes
typeplatform | custom
verification_tokenTXT ownership token
verified_atSet after DNS verify
is_primaryPreferred custom host for frontend links
claimed_atUnverified claim TTL (branded.claim_ttl_hours)

PlatformDomainClassifier treats {label}.{suffix} under config('branded.platform_domain_suffixes') as platform. Everything else (including myai.com.pk, app.domain.co.uk) is custom — no two-label TLD assumption.

Resolution gate

WorkspaceResolver::resolveFromHost:

  • Platform domains → always bind
  • Custom domains → only when verified_at is set and EntitlementService::hasModule($tenant, 'branded')

Pointing DNS/IP without a verified entitled row does nothing.

Tenant API

Prefix /api/tenant/v1, middleware module:branded + Spatie can::

MethodPathPermission
GET/branded/domainbranded.view
POST/branded/domainbranded.manage
POST/branded/domain/verifybranded.manage
DELETE/branded/domainbranded.manage

Service: App\Services\Tenant\BrandedDomainService. DNS via App\Contracts\DomainDnsLookup (DnsGetRecordLookup / FakeDomainDnsLookup in tests).

Central / public registration never accept a client domain. TenantService always derives the platform hostname as {slug}.{primary PLATFORM_DOMAIN_SUFFIXES} (slug from company name / explicit slug). Custom domains remain tenant self-service under Branded.

Brand chrome

  • App\Support\BrandedMail::apply() on tenant mail notifications
  • Published resources/views/vendor/mail/html/message.blade.php (+ button) for logo / brand name
  • EmailConfigResolver overrides From name when branded is active
  • PlatformNotificationPayloadMapper uses tenant logo/favicon + title prefix

Cancel / deactivate

ModuleSubscriptionService::cancel / deactivate clears custom-domain verification when the module slug is branded.

Production hardening

  • Custom domain rows are force-deleted on remove / stale claim expiry so the unique domains.domain index can be reclaimed.
  • Verify fails closed when BRANDED_SERVER_IPV4 / BRANDED_SERVER_IPV6 / BRANDED_CNAME_TARGET are all empty.
  • BrandedCustomDomainCors allows API CORS only for Origins whose host is a verified + entitled custom domain.
  • Scheduler: branded:expire-stale-domain-claims hourly.

Tests

  • Pest: tests/Feature/Tenant/Branded/BrandedDomainTest.php, tests/Feature/Notifications/BrandedNotificationPayloadTest.php, tests/Unit/DomainRuleTest.php, tests/Unit/PlatformDomainClassifierTest.php
  • Playwright: npm run test:e2e:branded (Domain tab hidden without module)

Official documentation for the SaleOS SaaS Platform.