# Starter Package: Custom Domain Add-On

**Issue:** #436
**Date:** 2026-01-13
**Status:** Draft

## Overview

Allow Starter tier dealers to purchase a custom domain add-on for $36/year without upgrading to Growth tier. The add-on is a separate Stripe subscription.

## Pricing

- **Add-on price:** $36/year (annual only, matches Starter tier billing)
- **Stripe price ID:** `STRIPE_PRICE_CUSTOM_DOMAIN_ANNUAL` environment variable
- **Availability:** Starter tier only (Growth+ includes custom domains)

## Database Changes

Add three fields to the Dealer model in `prisma/schema.prisma`:

```prisma
// Custom domain add-on (Starter tier only)
hasCustomDomainAddon            Boolean   @default(false)
customDomainAddonSubscriptionId String?   @unique
customDomainAddonStatus         String    @default("none")  // none|active|cancelled
```

**Status values:**

- `none` - Never purchased
- `active` - Currently active subscription
- `cancelled` - Subscription cancelled, domain inactive but config preserved

## Access Control Changes

Modify `hasCustomDomainAccess()` in `lib/cms/types.ts`:

```typescript
export function hasCustomDomainAccess(
  subscriptionTier: string,
  hasGrandfatheredAccess: boolean = false,
  hasCustomDomainAddon: boolean = false
): boolean {
  if (hasGrandfatheredAccess) return true;
  if (hasCustomDomainAddon) return true;
  return CUSTOM_DOMAIN_TIERS.includes(subscriptionTier.toLowerCase());
}
```

Update all call sites to pass the new parameter.

## API Routes

### New Routes

| Route                               | Method | Purpose                                                    |
| ----------------------------------- | ------ | ---------------------------------------------------------- |
| `/api/checkout/custom-domain-addon` | POST   | Checkout session for add-on (existing Starter dealers)     |
| `/api/checkout/starter-bundle`      | POST   | Checkout session for Starter + Custom Domain (new signups) |
| `/api/subscription/cancel-addon`    | POST   | Cancel the add-on subscription                             |

### Webhook Handling

Add to existing `/api/webhooks/stripe/route.ts`:

**`checkout.session.completed`:**

- Detect add-on purchase via metadata `type: 'custom_domain_addon'`
- Set `hasCustomDomainAddon: true`, `customDomainAddonStatus: 'active'`
- Store `customDomainAddonSubscriptionId`

**`customer.subscription.deleted`:**

- If add-on subscription: set `hasCustomDomainAddon: false`
- Keep domain config but mark inactive

**`customer.subscription.updated`:**

- Handle add-on subscription status changes

### Upgrade Flow Modification

In `/api/checkout/upgrade/route.ts`, when upgrading from Starter to Growth+:

1. Check if `hasCustomDomainAddon: true`
2. Cancel add-on subscription via `stripe.subscriptions.cancel()` with proration
3. Set `hasCustomDomainAddon: false`, `customDomainAddonStatus: 'none'`
4. Custom domain stays active (now covered by Growth tier)

## UI Changes

### Custom Domain Settings Page

**Path:** `/dashboard/settings/custom-domain`

For Starter dealers without the add-on, show:

- Heading: "Custom Domain Add-On"
- Price: "$36/year"
- Description of the feature
- CTA: "Add Custom Domain" button
- Secondary: "Or upgrade to Growth for more features" link

After purchase, show normal custom domain configuration UI.

### Subscription Management Page

**Path:** `/dashboard/subscription`

Add "Add-ons" section (visible to Starter tier only):

- **Not purchased:** Price + "Add" button
- **Active:** Status, renewal date, "Cancel Add-on" button
- **Cancelled:** "Reactivate" button, shows when access ends

### Homepage

**Path:** `public/index.html`

Add option near Starter tier pricing:

- "+ Custom Domain: $36/year" as optional add-on
- When selected, checkout creates both subscriptions in single session

### No Dealer Setup Page

**Path:** `app/components/NoDealerSetup.tsx`

For authenticated users without a dealer account, show the bundle option:

- Same pricing display as homepage
- "+ Custom Domain: $36/year" checkbox or toggle
- When selected, checkout creates both subscriptions

## Checkout Flows

### Add-on Purchase (Existing Starter Dealer)

1. Dealer clicks "Add Custom Domain"
2. API validates: Starter tier, no existing add-on
3. Create Stripe Checkout session with `STRIPE_PRICE_CUSTOM_DOMAIN_ANNUAL`
4. Metadata: `{ dealerId, type: 'custom_domain_addon' }`
5. Webhook updates dealer record on success
6. Redirect to custom domain settings

### Bundle Purchase (New Signup)

1. User selects Starter + Custom Domain on homepage
2. Create Checkout session with two subscription line items
3. Metadata: `{ type: 'starter_bundle' }`
4. Webhook creates dealer with Starter tier + add-on flags
5. Redirect to onboarding

### Upgrade to Growth+ (Auto-Cancel Add-on)

1. During upgrade, check `hasCustomDomainAddon`
2. After tier upgrade succeeds, cancel add-on subscription (prorated refund)
3. Update dealer: `hasCustomDomainAddon: false`, `customDomainAddonStatus: 'none'`
4. Custom domain remains active under Growth tier

### Manual Add-on Cancellation

1. Dealer clicks "Cancel Add-on"
2. Confirmation dialog: domain stays active until period ends
3. API calls `stripe.subscriptions.update({ cancel_at_period_end: true })`
4. Set `customDomainAddonStatus: 'cancelled'`
5. On subscription deletion webhook: set `hasCustomDomainAddon: false`, mark domain inactive

## Edge Cases

| Scenario                                            | Behavior                                            |
| --------------------------------------------------- | --------------------------------------------------- |
| Dealer has grandfathered custom domain              | Hide add-on option, already has access              |
| Non-Starter dealer tries to buy add-on              | API error, UI hides option                          |
| Downgrade from Growth to Starter with active domain | Domain stays active (grandfathered)                 |
| Add-on payment fails on renewal                     | Stripe retries; on final failure, mark cancelled    |
| Upgrade to Growth then downgrade to Starter         | Loses custom domain access, must re-purchase add-on |

## Implementation Order

1. Database schema changes and migration
2. Update `hasCustomDomainAccess()` and all call sites
3. Add-on checkout route (`/api/checkout/custom-domain-addon`)
4. Webhook handling for add-on events
5. Custom domain settings page UI changes
6. Subscription page add-ons section
7. Upgrade flow modification (auto-cancel add-on)
8. Add-on cancellation route and UI
9. Homepage bundle option
10. NoDealerSetup.tsx bundle option
11. Bundle checkout route (`/api/checkout/starter-bundle`)
