# Cloudflare Subdomain Automation Implementation Plan

> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.

**Goal:** Automatically create DNS records in Cloudflare when dealers publish, enabling their landing pages at chosen subdomains with zero manual configuration.

**Architecture:** Direct Cloudflare API integration on publish + Next.js middleware for subdomain routing + ISR for static page generation. DNS records created synchronously during publish flow, with rollback on failure.

**Tech Stack:** Cloudflare REST API, Next.js 14 App Router, Prisma, TypeScript

---

## Task 1: Database Migration - Add Cloudflare Record Tracking

**Files:**

- Create: `prisma/migrations/XXXXXX_add_cloudflare_record_id/migration.sql`
- Modify: `prisma/schema.prisma:46-91`

**Step 1: Update Prisma schema**

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

```prisma
model Dealer {
  // ... existing fields ...

  cloudflareRecordId  String?    // Store DNS record ID for updates/deletion
  publishedAt         DateTime?  // Track when dealer published

  // ... rest of model ...
}
```

**Step 2: Create migration**

Run: `npx prisma migrate dev --name add_cloudflare_record_id`
Expected: Migration file created, database updated

**Step 3: Verify migration**

Run: `npx prisma studio`
Expected: Dealer table shows new `cloudflareRecordId` and `publishedAt` columns

**Step 4: Commit**

```bash
git add prisma/schema.prisma prisma/migrations/
git commit -m "db: add cloudflare record tracking fields to Dealer model"
```

---

## Task 2: Environment Variables Configuration

**Files:**

- Modify: `.env.local`
- Create: `.env.example`

**Step 1: Add Cloudflare credentials to .env.local**

```bash
# Cloudflare DNS Automation
CLOUDFLARE_API_TOKEN=your_token_here
CLOUDFLARE_ZONE_ID=your_zone_id_here
SERVER_IP_ADDRESS=your_server_ip_here
```

**Step 2: Update .env.example**

Add to `.env.example`:

```bash
# Cloudflare DNS Automation
CLOUDFLARE_API_TOKEN=
CLOUDFLARE_ZONE_ID=
SERVER_IP_ADDRESS=
```

**Step 3: Document token creation**

Add comment in `.env.example`:

```bash
# Get CLOUDFLARE_API_TOKEN from Cloudflare dashboard:
# 1. Go to My Profile > API Tokens
# 2. Create Token with Zone.DNS (Edit) + Zone.Zone (Read)
# 3. Scope to specific zone only
#
# Get CLOUDFLARE_ZONE_ID from domain overview page
# Get SERVER_IP_ADDRESS from hosting provider
```

**Step 4: Commit**

```bash
git add .env.example
git commit -m "config: add Cloudflare DNS automation environment variables"
```

---

## Task 3: Cloudflare API Wrapper - Types and Interface

**Files:**

- Create: `lib/cloudflare-api.ts`
- Create: `lib/__tests__/cloudflare-api.test.ts`

**Step 1: Write test for DNS record creation**

Create `lib/__tests__/cloudflare-api.test.ts`:

```typescript
import { createDnsRecord, deleteDnsRecord } from '../cloudflare-api';

// Mock fetch globally
global.fetch = jest.fn();

describe('Cloudflare API', () => {
  beforeEach(() => {
    jest.clearAllMocks();
    process.env.CLOUDFLARE_API_TOKEN = 'test_token';
    process.env.CLOUDFLARE_ZONE_ID = 'test_zone';
    process.env.SERVER_IP_ADDRESS = '1.2.3.4';
  });

  describe('createDnsRecord', () => {
    it('should create A record with correct parameters', async () => {
      const mockResponse = {
        success: true,
        result: {
          id: 'record_123',
          name: 'testdealer',
          type: 'A',
          content: '1.2.3.4',
        },
      };

      (global.fetch as jest.Mock).mockResolvedValueOnce({
        ok: true,
        json: async () => mockResponse,
      });

      const result = await createDnsRecord('testdealer');

      expect(global.fetch).toHaveBeenCalledWith(
        'https://api.cloudflare.com/client/v4/zones/test_zone/dns_records',
        {
          method: 'POST',
          headers: {
            Authorization: 'Bearer test_token',
            'Content-Type': 'application/json',
          },
          body: JSON.stringify({
            type: 'A',
            name: 'testdealer',
            content: '1.2.3.4',
            ttl: 1,
            proxied: true,
          }),
        }
      );

      expect(result).toEqual({
        success: true,
        recordId: 'record_123',
      });
    });

    it('should handle API errors', async () => {
      (global.fetch as jest.Mock).mockResolvedValueOnce({
        ok: false,
        status: 400,
        json: async () => ({
          success: false,
          errors: [{ message: 'Record already exists' }],
        }),
      });

      const result = await createDnsRecord('testdealer');

      expect(result.success).toBe(false);
      expect(result.error).toContain('Record already exists');
    });
  });

  describe('deleteDnsRecord', () => {
    it('should delete DNS record by ID', async () => {
      (global.fetch as jest.Mock).mockResolvedValueOnce({
        ok: true,
        json: async () => ({ success: true }),
      });

      const result = await deleteDnsRecord('record_123');

      expect(global.fetch).toHaveBeenCalledWith(
        'https://api.cloudflare.com/client/v4/zones/test_zone/dns_records/record_123',
        {
          method: 'DELETE',
          headers: {
            Authorization: 'Bearer test_token',
          },
        }
      );

      expect(result.success).toBe(true);
    });
  });
});
```

**Step 2: Run test to verify it fails**

Run: `npm test -- cloudflare-api.test.ts`
Expected: FAIL - "Cannot find module '../cloudflare-api'"

**Step 3: Implement Cloudflare API wrapper**

Create `lib/cloudflare-api.ts`:

```typescript
/**
 * Cloudflare DNS API wrapper for subdomain automation
 */

interface CloudflareResponse {
  success: boolean;
  result?: {
    id: string;
    name: string;
    type: string;
    content: string;
  };
  errors?: Array<{ message: string }>;
}

interface DnsRecordResult {
  success: boolean;
  recordId?: string;
  error?: string;
}

const CLOUDFLARE_API_BASE = 'https://api.cloudflare.com/client/v4';

/**
 * Get Cloudflare API configuration from environment
 */
function getConfig() {
  const token = process.env.CLOUDFLARE_API_TOKEN;
  const zoneId = process.env.CLOUDFLARE_ZONE_ID;
  const serverIp = process.env.SERVER_IP_ADDRESS;

  if (!token || !zoneId || !serverIp) {
    throw new Error(
      'Missing Cloudflare configuration: CLOUDFLARE_API_TOKEN, CLOUDFLARE_ZONE_ID, and SERVER_IP_ADDRESS must be set'
    );
  }

  return { token, zoneId, serverIp };
}

/**
 * Create DNS A record for dealer subdomain
 *
 * @param subdomain - Dealer subdomain (e.g., "bobsoil")
 * @returns Result with record ID or error
 */
export async function createDnsRecord(subdomain: string): Promise<DnsRecordResult> {
  try {
    const { token, zoneId, serverIp } = getConfig();

    const response = await fetch(`${CLOUDFLARE_API_BASE}/zones/${zoneId}/dns_records`, {
      method: 'POST',
      headers: {
        Authorization: `Bearer ${token}`,
        'Content-Type': 'application/json',
      },
      body: JSON.stringify({
        type: 'A',
        name: subdomain,
        content: serverIp,
        ttl: 1, // Auto (Cloudflare optimizes)
        proxied: true, // Enable SSL and DDoS protection
      }),
    });

    const data: CloudflareResponse = await response.json();

    if (!response.ok || !data.success) {
      const errorMsg = data.errors?.[0]?.message || 'Unknown error';
      return {
        success: false,
        error: `Cloudflare API error: ${errorMsg}`,
      };
    }

    return {
      success: true,
      recordId: data.result!.id,
    };
  } catch (error) {
    return {
      success: false,
      error: error instanceof Error ? error.message : 'Unknown error',
    };
  }
}

/**
 * Delete DNS record by ID (for rollback on errors)
 *
 * @param recordId - Cloudflare DNS record ID
 * @returns Success status
 */
export async function deleteDnsRecord(
  recordId: string
): Promise<{ success: boolean; error?: string }> {
  try {
    const { token, zoneId } = getConfig();

    const response = await fetch(`${CLOUDFLARE_API_BASE}/zones/${zoneId}/dns_records/${recordId}`, {
      method: 'DELETE',
      headers: {
        Authorization: `Bearer ${token}`,
      },
    });

    const data: CloudflareResponse = await response.json();

    if (!response.ok || !data.success) {
      const errorMsg = data.errors?.[0]?.message || 'Unknown error';
      return {
        success: false,
        error: `Failed to delete DNS record: ${errorMsg}`,
      };
    }

    return { success: true };
  } catch (error) {
    return {
      success: false,
      error: error instanceof Error ? error.message : 'Unknown error',
    };
  }
}

/**
 * Check if DNS record already exists for subdomain
 *
 * @param subdomain - Dealer subdomain
 * @returns Record ID if exists, null otherwise
 */
export async function findExistingRecord(subdomain: string): Promise<string | null> {
  try {
    const { token, zoneId } = getConfig();

    const response = await fetch(
      `${CLOUDFLARE_API_BASE}/zones/${zoneId}/dns_records?name=${subdomain}`,
      {
        method: 'GET',
        headers: {
          Authorization: `Bearer ${token}`,
        },
      }
    );

    const data: CloudflareResponse & {
      result?: Array<{ id: string; name: string }>;
    } = await response.json();

    if (data.success && data.result && data.result.length > 0) {
      return data.result[0].id;
    }

    return null;
  } catch (error) {
    console.error('Error checking for existing DNS record:', error);
    return null;
  }
}
```

**Step 4: Run tests to verify they pass**

Run: `npm test -- cloudflare-api.test.ts`
Expected: PASS - All tests passing

**Step 5: Commit**

```bash
git add lib/cloudflare-api.ts lib/__tests__/cloudflare-api.test.ts
git commit -m "feat: add Cloudflare DNS API wrapper with create/delete/find methods"
```

---

## Task 4: ISR Revalidation Helper

**Files:**

- Create: `lib/isr-revalidation.ts`
- Create: `lib/__tests__/isr-revalidation.test.ts`

**Step 1: Write test for revalidation trigger**

Create `lib/__tests__/isr-revalidation.test.ts`:

```typescript
import { triggerRevalidation } from '../isr-revalidation';

global.fetch = jest.fn();

describe('ISR Revalidation', () => {
  beforeEach(() => {
    jest.clearAllMocks();
    process.env.NEXTAUTH_URL = 'http://localhost:3000';
  });

  it('should trigger revalidation for dealer subdomain path', async () => {
    (global.fetch as jest.Mock).mockResolvedValueOnce({
      ok: true,
      json: async () => ({ revalidated: true }),
    });

    const result = await triggerRevalidation('bobsoil');

    expect(global.fetch).toHaveBeenCalledWith('http://localhost:3000/api/revalidate', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ path: '/dealers/bobsoil' }),
    });

    expect(result.success).toBe(true);
  });

  it('should handle revalidation errors gracefully', async () => {
    (global.fetch as jest.Mock).mockResolvedValueOnce({
      ok: false,
      status: 500,
    });

    const result = await triggerRevalidation('bobsoil');

    expect(result.success).toBe(false);
    expect(result.error).toBeDefined();
  });
});
```

**Step 2: Run test to verify it fails**

Run: `npm test -- isr-revalidation.test.ts`
Expected: FAIL - "Cannot find module '../isr-revalidation'"

**Step 3: Implement ISR revalidation helper**

Create `lib/isr-revalidation.ts`:

```typescript
/**
 * ISR (Incremental Static Regeneration) revalidation helper
 */

interface RevalidationResult {
  success: boolean;
  error?: string;
}

/**
 * Trigger on-demand revalidation for dealer page
 *
 * @param subdomain - Dealer subdomain
 * @returns Success status
 */
export async function triggerRevalidation(subdomain: string): Promise<RevalidationResult> {
  try {
    const baseUrl = process.env.NEXTAUTH_URL || 'http://localhost:3000';
    const path = `/dealers/${subdomain}`;

    const response = await fetch(`${baseUrl}/api/revalidate`, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
      },
      body: JSON.stringify({ path }),
    });

    if (!response.ok) {
      return {
        success: false,
        error: `Revalidation API returned status ${response.status}`,
      };
    }

    return { success: true };
  } catch (error) {
    // Log but don't fail - ISR will work on next request
    console.warn('ISR revalidation failed (non-critical):', error);
    return {
      success: false,
      error: error instanceof Error ? error.message : 'Unknown error',
    };
  }
}
```

**Step 4: Run tests to verify they pass**

Run: `npm test -- isr-revalidation.test.ts`
Expected: PASS - All tests passing

**Step 5: Commit**

```bash
git add lib/isr-revalidation.ts lib/__tests__/isr-revalidation.test.ts
git commit -m "feat: add ISR revalidation helper for dealer pages"
```

---

## Task 5: Revalidation API Endpoint

**Files:**

- Create: `app/api/revalidate/route.ts`

**Step 1: Create revalidation API route**

Create `app/api/revalidate/route.ts`:

```typescript
import { revalidatePath } from 'next/cache';
import { NextRequest, NextResponse } from 'next/server';

/**
 * API endpoint for on-demand ISR revalidation
 * Called internally after dealer publishes or updates
 */
export async function POST(request: NextRequest) {
  try {
    const body = await request.json();
    const { path } = body;

    if (!path || typeof path !== 'string') {
      return NextResponse.json({ error: 'Path is required' }, { status: 400 });
    }

    // Trigger revalidation
    revalidatePath(path);

    return NextResponse.json({
      revalidated: true,
      path,
      timestamp: new Date().toISOString(),
    });
  } catch (error) {
    console.error('Revalidation error:', error);
    return NextResponse.json({ error: 'Failed to revalidate' }, { status: 500 });
  }
}
```

**Step 2: Test endpoint manually**

Run dev server: `npm run dev`
Test: `curl -X POST http://localhost:3000/api/revalidate -H "Content-Type: application/json" -d '{"path":"/dealers/test"}'`
Expected: `{"revalidated":true,"path":"/dealers/test","timestamp":"..."}`

**Step 3: Commit**

```bash
git add app/api/revalidate/route.ts
git commit -m "feat: add API endpoint for on-demand ISR revalidation"
```

---

## Task 6: Subdomain Middleware

**Files:**

- Create: `middleware.ts` (root directory)
- Modify: `next.config.mjs`

**Step 1: Create middleware for subdomain routing**

Create `middleware.ts` in project root:

```typescript
import { NextRequest, NextResponse } from 'next/server';

/**
 * Middleware to detect subdomains and route to dealer pages
 *
 * Example: bobsoil.acdev3.com -> /dealers/bobsoil
 */
export function middleware(request: NextRequest) {
  const hostname = request.headers.get('host') || '';

  // Extract subdomain (first part before domain)
  const subdomain = hostname.split('.')[0];

  // Skip main domain and localhost
  const mainDomains = ['acdev3', 'localhost', 'shopamsoil', 'myamsoil'];
  if (mainDomains.includes(subdomain) || subdomain.includes('localhost')) {
    return NextResponse.next();
  }

  // Skip if accessing non-root paths (API, static files, etc.)
  if (request.nextUrl.pathname !== '/') {
    return NextResponse.next();
  }

  // Rewrite to dealer page
  const url = request.nextUrl.clone();
  url.pathname = `/dealers/${subdomain}`;

  return NextResponse.rewrite(url);
}

export const config = {
  matcher: [
    /*
     * Match all request paths except:
     * - api (API routes)
     * - _next/static (static files)
     * - _next/image (image optimization files)
     * - favicon.ico (favicon file)
     */
    '/((?!api|_next/static|_next/image|favicon.ico).*)',
  ],
};
```

**Step 2: Test middleware logic**

Run: `npm run dev`
Expected: Middleware compiles without errors

**Step 3: Commit**

```bash
git add middleware.ts
git commit -m "feat: add middleware for subdomain-to-dealer-page routing"
```

---

## Task 7: Dealer Page with ISR

**Files:**

- Create: `app/dealers/[subdomain]/page.tsx`

**Step 1: Create dealer page with ISR**

Create `app/dealers/[subdomain]/page.tsx`:

```typescript
import { notFound } from 'next/navigation';
import { prisma } from '@/lib/prisma';
import { DealerTemplate } from '@/components/DealerTemplate';
import type { DealerInfo } from '@/types/dealer';

/**
 * Generate static params for all active dealers at build time
 */
export async function generateStaticParams() {
  const dealers = await prisma.dealer.findMany({
    where: {
      status: 'active',
      subdomain: { not: null },
    },
    select: { subdomain: true },
  });

  return dealers.map((dealer) => ({
    subdomain: dealer.subdomain!,
  }));
}

/**
 * ISR: Cache forever, revalidate on-demand
 */
export const revalidate = false;

/**
 * Dealer landing page - served at {subdomain}.acdev3.com
 */
export default async function DealerPage({
  params,
}: {
  params: { subdomain: string };
}) {
  const dealer = await prisma.dealer.findUnique({
    where: {
      subdomain: params.subdomain,
    },
    select: {
      subdomain: true,
      businessName: true,
      phone: true,
      user: {
        select: {
          email: true,
        },
      },
      address: true,
      city: true,
      state: true,
      zip: true,
      country: true,
    },
  });

  if (!dealer || !dealer.subdomain) {
    notFound();
  }

  // Transform to DealerInfo format
  const dealerInfo: DealerInfo = {
    subdomain: dealer.subdomain,
    name: dealer.businessName || 'AMSOIL Dealer',
    phone: dealer.phone,
    email: dealer.user.email,
    address: {
      city: dealer.city,
      state: dealer.state,
      zip: dealer.zip,
      country: dealer.country,
    },
  };

  return <DealerTemplate dealer={dealerInfo} />;
}
```

**Step 2: Test page routing**

Run: `npm run dev`
Visit: `http://localhost:3000/dealers/test`
Expected: Page renders or 404 if no dealer exists

**Step 3: Commit**

```bash
git add app/dealers/[subdomain]/page.tsx
git commit -m "feat: add dealer landing page with ISR and generateStaticParams"
```

---

## Task 8: Update Publish Endpoint - Add DNS Creation

**Files:**

- Modify: `app/api/dealers/[id]/publish/route.ts`

**Step 1: Import DNS and revalidation helpers**

At top of `app/api/dealers/[id]/publish/route.ts`, add imports:

```typescript
import { createDnsRecord, deleteDnsRecord } from '@/lib/cloudflare-api';
import { triggerRevalidation } from '@/lib/isr-revalidation';
```

**Step 2: Add DNS creation before status update**

Find the section where dealer status is updated to 'active'. Add DNS logic BEFORE that:

```typescript
// ... existing validation code ...

// NEW: Create Cloudflare DNS record
let dnsRecordId: string | undefined;

if (dealer.subdomain) {
  const dnsResult = await createDnsRecord(dealer.subdomain);

  if (!dnsResult.success) {
    return NextResponse.json(
      {
        error: 'Failed to create DNS record',
        details: dnsResult.error,
      },
      { status: 500 }
    );
  }

  dnsRecordId = dnsResult.recordId;
}

// NEW: Trigger ISR revalidation (non-blocking)
if (dealer.subdomain) {
  triggerRevalidation(dealer.subdomain).catch((err) => {
    console.warn('ISR revalidation failed (non-critical):', err);
  });
}

try {
  // Update dealer status to active and store DNS record ID
  await prisma.dealer.update({
    where: { id: dealerId },
    data: {
      status: 'active',
      migrationStatus: 'completed',
      cloudflareRecordId: dnsRecordId, // NEW
      publishedAt: new Date(), // NEW
    },
  });

  // ... existing success response ...
} catch (error) {
  // NEW: Rollback DNS record on database error
  if (dnsRecordId) {
    await deleteDnsRecord(dnsRecordId);
  }
  throw error;
}
```

**Step 3: Test publish flow manually**

Run: `npm run dev`
Prerequisites:

- Valid Cloudflare credentials in .env.local
- Test dealer in database with subdomain

Test: Click "Publish" button in onboarding
Expected: DNS record created in Cloudflare, dealer status = active

**Step 4: Commit**

```bash
git add app/api/dealers/[id]/publish/route.ts
git commit -m "feat: integrate DNS creation and ISR revalidation into publish flow"
```

---

## Task 9: Integration Test - End-to-End Flow

**Files:**

- Create: `app/api/dealers/[id]/publish/__tests__/integration.test.ts`

**Step 1: Write integration test**

Create `app/api/dealers/[id]/publish/__tests__/integration.test.ts`:

```typescript
import { POST } from '../route';
import { NextRequest } from 'next/server';
import { prisma } from '@/lib/prisma';
import * as cloudflareApi from '@/lib/cloudflare-api';

// Mock modules
jest.mock('@/lib/cloudflare-api');
jest.mock('@/lib/isr-revalidation');

describe('Publish Endpoint Integration', () => {
  const mockCreateDns = cloudflareApi.createDnsRecord as jest.Mock;
  const mockDeleteDns = cloudflareApi.deleteDnsRecord as jest.Mock;

  beforeEach(() => {
    jest.clearAllMocks();
  });

  it('should create DNS record and publish dealer successfully', async () => {
    // Setup: Create test dealer
    const dealer = await prisma.dealer.create({
      data: {
        userId: 'user_123',
        subdomain: 'testdealer',
        domain: 'com',
        stripeCustomerId: 'cus_test',
        subscriptionTier: 'starter',
        phone: '555-0100',
        address: '123 Test St',
        city: 'Test City',
        state: 'CA',
        zip: '90210',
        country: 'US',
        status: 'registration_pending',
      },
    });

    // Mock successful DNS creation
    mockCreateDns.mockResolvedValueOnce({
      success: true,
      recordId: 'cf_record_123',
    });

    // Create mock request
    const request = new NextRequest(`http://localhost:3000/api/dealers/${dealer.id}/publish`, {
      method: 'POST',
    });

    // Call endpoint
    const response = await POST(request, { params: { id: dealer.id } });
    const data = await response.json();

    // Verify
    expect(response.status).toBe(200);
    expect(mockCreateDns).toHaveBeenCalledWith('testdealer');

    const updatedDealer = await prisma.dealer.findUnique({
      where: { id: dealer.id },
    });

    expect(updatedDealer?.status).toBe('active');
    expect(updatedDealer?.cloudflareRecordId).toBe('cf_record_123');
    expect(updatedDealer?.publishedAt).toBeDefined();

    // Cleanup
    await prisma.dealer.delete({ where: { id: dealer.id } });
  });

  it('should rollback on database error', async () => {
    // Setup
    const dealer = await prisma.dealer.create({
      data: {
        userId: 'user_456',
        subdomain: 'rollbacktest',
        domain: 'com',
        stripeCustomerId: 'cus_test2',
        subscriptionTier: 'starter',
        phone: '555-0200',
        address: '456 Test Ave',
        city: 'Test City',
        state: 'CA',
        zip: '90210',
        country: 'US',
        status: 'registration_pending',
      },
    });

    // Mock successful DNS creation
    mockCreateDns.mockResolvedValueOnce({
      success: true,
      recordId: 'cf_record_456',
    });

    // Mock database error (simulate by updating dealer to invalid state first)
    await prisma.dealer.update({
      where: { id: dealer.id },
      data: { status: 'active' }, // Already published
    });

    const request = new NextRequest(`http://localhost:3000/api/dealers/${dealer.id}/publish`, {
      method: 'POST',
    });

    await POST(request, { params: { id: dealer.id } });

    // Verify DNS record was deleted (rollback)
    expect(mockDeleteDns).toHaveBeenCalledWith('cf_record_456');

    // Cleanup
    await prisma.dealer.delete({ where: { id: dealer.id } });
  });
});
```

**Step 2: Run integration test**

Run: `npm test -- integration.test.ts`
Expected: Tests pass with mocked Cloudflare API

**Step 3: Commit**

```bash
git add app/api/dealers/[id]/publish/__tests__/integration.test.ts
git commit -m "test: add integration tests for publish flow with DNS creation"
```

---

## Task 10: Manual Testing Checklist

**Step 1: Verify environment setup**

Check `.env.local` has all required values:

```bash
grep -E "(CLOUDFLARE_API_TOKEN|CLOUDFLARE_ZONE_ID|SERVER_IP_ADDRESS)" .env.local
```

Expected: All three variables present with values

**Step 2: Test full flow in development**

1. Start dev server: `npm run dev`
2. Create test dealer via registration flow
3. Complete onboarding through Step 2 (choose subdomain: "testflow")
4. Click "Publish" in Step 3
5. Verify in Cloudflare dashboard: DNS record created for testflow.acdev3.com
6. Visit `testflow.acdev3.com` (or localhost with host header)
7. Verify dealer page renders with correct info

**Step 3: Test error scenarios**

1. Test with invalid Cloudflare credentials (expect error, dealer stays unpublished)
2. Test with existing subdomain (expect error or idempotent behavior)
3. Test ISR revalidation failure (should not block publish)

**Step 4: Document test results**

Create `docs/testing/cloudflare-dns-test-results.md` with findings

---

## Task 11: Documentation Updates

**Files:**

- Update: `README.md`
- Update: `docs/REGISTRATION_FLOW_MAP.md`

**Step 1: Update README with Cloudflare setup**

Add section to `README.md`:

````markdown
## Cloudflare DNS Automation

Dealer subdomains are automatically configured via Cloudflare API when dealers publish.

### Setup

1. Get Cloudflare API token:
   - Go to Cloudflare Dashboard > My Profile > API Tokens
   - Create token with `Zone.DNS (Edit)` and `Zone.Zone (Read)` permissions
   - Scope to specific zone (acdev3.com)

2. Add to `.env.local`:
   ```bash
   CLOUDFLARE_API_TOKEN=your_token
   CLOUDFLARE_ZONE_ID=your_zone_id
   SERVER_IP_ADDRESS=your_ip
   ```
````

3. DNS records are created automatically on dealer publish

### Testing

Test subdomain creation:

```bash
npm test -- cloudflare-api.test.ts
```

Test full integration:

```bash
npm test -- integration.test.ts
```

````

**Step 2: Update registration flow documentation**

Add to `docs/REGISTRATION_FLOW_MAP.md` in the "Publish Step" section:

```markdown
### Publish Flow (Updated)

1. Validate dealer ownership
2. **NEW: Create Cloudflare DNS record**
   - Call `createDnsRecord(subdomain)`
   - Store `cloudflareRecordId` in database
   - Rollback on failure
3. **NEW: Trigger ISR revalidation** (non-blocking)
4. Update dealer status to `active`
5. Redirect to dashboard
````

**Step 3: Commit**

```bash
git add README.md docs/REGISTRATION_FLOW_MAP.md
git commit -m "docs: document Cloudflare DNS automation setup and flow"
```

---

## Verification Checklist

Before marking complete, verify:

- [ ] All tests passing: `npm test`
- [ ] Build succeeds: `npm run build`
- [ ] TypeScript compiles: `npx tsc --noEmit`
- [ ] Linting passes: `npm run lint`
- [ ] Manual test: DNS record created in Cloudflare on publish
- [ ] Manual test: Subdomain routes to dealer page
- [ ] Manual test: ISR generates static page
- [ ] Manual test: Error rollback works (delete DNS on DB failure)
- [ ] Documentation updated
- [ ] Environment variables documented

---

## Notes

- **DNS Propagation**: 30-60 seconds typical, inform dealers
- **Error Handling**: All Cloudflare API failures prevent publish
- **Rollback**: DNS records deleted if database update fails
- **ISR**: Non-blocking, will work on next request if initial trigger fails
- **Custom Domains**: Out of scope for v1, manual setup only
- **Monitoring**: Add logging for Cloudflare API calls in production

## Related Documentation

- Design: `docs/plans/2025-01-19-cloudflare-subdomain-automation-design.md`
- Future Features: `docs/CLOUDFLARE_FUTURE_ENHANCEMENTS.md`
