# Proposal: Plan-Gated White-Label

Draft. 2026-05-16. Iter 4 of audit loop.

---

## Why

Today Pitchbar has two branding tiers:

| Tier | Where it lives | Who controls |
|---|---|---|
| **Platform-wide** | `app_settings.{site_title, header_logo, footer_logo, pitchbar_brand_url, pitchbar_brand_label}` | super_admin in `/settings/branding` |
| **Per-workspace agent** | `agent.theme` (color, position, launcher label) | customer in `/app/agents/{id}/customize` |

**Gap:** the visitor-facing widget always renders "Powered by Pitchbar" (or whatever the
super_admin renamed it to at the platform level). A workspace OWNER paying for a Pro plan
cannot remove that pill or replace it with their own brand. Buyer ask, recurring across
CodeCanyon agencies: their clients pay them, see "Powered by Pitchbar", and ask "what is
that?"

Competitive baseline:
- Intercom: white-label hidden behind enterprise tier
- Drift: removed on Premium ($2400/mo)
- Tidio: hidden on Plus plan ($59/mo)
- Crisp: hidden on Business plan ($95/mo)

Pitchbar value: ship a per-workspace plan flag that, when on, strips the powered-by line
from the widget AND lets the workspace pick its own pill label / link. Plan-gateable means
operators can sell it as a Pro-tier feature; super_admin remains in control of which plans
get it.

## Scope (v1)

**In:**
- New `plans.white_label_enabled` boolean (default false)
- New `workspaces.widget_brand` JSON column with shape:
  ```json
  {
    "label": "Powered by Acme",
    "url": "https://acme.com",
    "show_pill": true | false
  }
  ```
- Widget reads `branding.workspace` from `/api/v1/widget/init` response when set; falls
  back to platform branding otherwise
- `/app/settings/widget` exposes the customisation when plan allows; shows an
  "Upgrade to white-label" CTA when plan disallows
- `PlanLimits::whiteLabelEnabled(Workspace)` helper mirrors the existing resource-limit
  pattern from 2026-05-15 batch

**Out (v2+):**
- Custom-domain widget hosting (`widget.acme.com`)
- Custom email "From" address per workspace (separately scoped; partly built already)
- Per-workspace favicon override in browser tabs

## Data model

Migration 1: `add_white_label_enabled_to_plans`
- `plans.white_label_enabled` boolean default false

Migration 2: `add_widget_brand_to_workspaces`
- `workspaces.widget_brand` JSON nullable
- Defaults to null on every existing workspace = no override

`Plan` model fillable + cast extended:
```php
'white_label_enabled' => 'boolean',
```

`Workspace` model fillable + cast extended:
```php
'widget_brand' => 'array',
```

## API surface

**Server side (already existing endpoint, payload addition):**

`/api/v1/widget/init` response gains:
```json
{
  "branding": {
    "platform": { ...existing platform branding... },
    "workspace": {
      "label": "Powered by Acme",
      "url": "https://acme.com",
      "show_pill": true
    }
  }
}
```

When the workspace's plan has `white_label_enabled = true` AND `workspace.widget_brand !== null`,
emit the workspace block; else emit `null` and widget falls back to platform branding.

**Admin / customer side:**
- New section in `/app/settings/widget` (already exists for workspace-level widget defaults).
  Add a "Brand pill" card with three fields and a plan-gate guard.

## Widget client side

`resources/widget/src/ui/Bar.tsx` already has a `Powered by Pitchbar` rendering near the
panel footer. Update to:
1. Read `state.init?.branding.workspace` first
2. Render the workspace pill if present + `show_pill = true`
3. Render `null` if `show_pill = false` (strip entirely)
4. Fall back to platform branding when workspace branding is absent

Hot-path impact: zero. Branding is read once on widget mount from the init payload.

## Plan-gating

Mirrors `PlanLimits` pattern shipped 2026-05-15 for resource caps:

```php
$limits = app(PlanLimits::class);
if (! $limits->whiteLabelEnabled($workspace)) {
    abort(403, 'Plan does not include white-label');
}
```

Customer-facing UI shows an upgrade nudge instead of the form when the plan flag is off —
matches the existing "Upgrade to Pro" CTA pattern.

## Effort estimate

| Phase | Days | What ships |
|---|---|---|
| Migrations + model fillable/cast | 0.5 | Schema + Plan/Workspace fields |
| PlanLimits helper + plan-form UI | 0.5 | super_admin can tick "White-label" on a plan |
| InitController response shape | 0.5 | branding.workspace field added |
| WorkspaceWidgetController endpoint + Inertia page | 1 | Customer-side brand pill editor with plan-gate guard |
| Widget Bar.tsx rendering | 0.5 | Workspace pill takes precedence over platform |
| Tests | 0.5 | Plan-gate, init payload, widget rendering |
| Docs | 0.5 | New page under "Run your workspace" |
| **Total** | **4 days** | |

## Open questions

1. Should "show_pill = false" require a higher plan than "rebrand the pill"? Some
   competitors (Crisp) split these — branded pill is a lower tier, stripping it
   entirely is higher. Probably overkill; default to one flag for v1.
2. Should the workspace pill link enforce `rel="noopener noreferrer"`? Yes — same
   pattern as widget already uses for sources. Implement at render time, not store time.
3. URL validation: only allow `https://` schemes. Reject `javascript:`, `data:`, etc.
   Reuse `UrlSafetyGuard` for outbound link safety.
4. White-label tier should also let workspaces override the `<title>` of any standalone
   pages they own (e.g. agent's `/kb/{workspace.slug}/{slug}` route). Defer to v2.

## Why this is the right next bet

| Signal | Source |
|---|---|
| Buyer-direct ask | Lucian (KrazyKlicks), past CodeCanyon reviews |
| Existing infrastructure | `PlanLimits` pattern already shipped — minimal new architecture |
| Plan-gateable → revenue lever | Free workspaces see pill, Pro workspaces don't; clear upgrade path |
| Engineering effort low (~4 days) | Reuses existing model casts + Inertia page + InitController payload |
| Zero hot-path impact | Branding read on widget boot, not per-turn |

## Next step

Approval to scope into board cards #76-#80, parallel to email-channel (proposal at
`docs/PROPOSAL-EMAIL-CHANNEL.md`). Ship target same week as email channel since the
two share zero code paths.

## Implementation pre-flight

| Risk | Mitigation |
|---|---|
| Existing buyers on the "Pro" plan suddenly lose the Pitchbar brand pill across all their workspaces | Default `plans.white_label_enabled = false` even for Pro; require super_admin to tick it explicitly per plan. Migration cannot retroactively unbrand anyone. |
| Workspace owner sets a malicious `https://evil.com` link in their pill | URL validated through `UrlSafetyGuard`; rendered with `rel="noopener noreferrer"`. |
| Pre-v1 widget bundles still cached on visitor sites render the old "Powered by Pitchbar" pill | Bundle is one file with cache busting on the script src. After deploy, visitors on a hard reload pick up the new bundle. Buyers can force-refresh via a query-string version bump in the embed snippet. |
| White-label customer points pill at `https://google.com` and brand reps come after Pitchbar | Settings page text: "This URL is shown publicly. Only point at a domain you control." TOS update. Same as widget custom-domain in other SaaS products. |
