# How OpenSEO Lighthouse Integration Works: A Complete Technical Guide

> Explore the OpenSEO Lighthouse integration. Learn how workflow orchestration, DataforSEO API, and Cloudflare R2 deliver seamless site audit data to your UI. A complete technical guide.

- Repository: [Every App/open-seo](https://github.com/every-app/open-seo)
- Tags: deep-dive
- Published: 2026-06-26

---

**OpenSEO embeds Lighthouse audits directly into its site-audit workflow through a three-layer integration: workflow orchestration, DataforSEO API execution with Cloudflare R2 storage, and server-side API endpoints that expose trimmed data to the UI.**

The OpenSEO project (`every-app/open-seo`) implements a server-side-only integration with Google's Lighthouse auditing tool. Rather than running Lighthouse locally, the system leverages the DataforSEO Lighthouse API, stores raw JSON payloads in Cloudflare R2 object storage, and serves filtered results through TanStack Start server functions. This architecture ensures the UI never contacts third-party APIs directly while maintaining efficient access to performance metrics.

## Architecture Overview

The **OpenSEO Lighthouse integration** consists of three distinct layers that handle the complete audit lifecycle:

1. **Workflow orchestration** – The audit runner decides *if* and *when* Lighthouse executes based on configuration flags.
2. **Lighthouse execution and storage** – A thin wrapper calls the DataforSEO Lighthouse API, persists the raw JSON payload to Cloudflare R2, and returns lightweight metadata.
3. **Server-side API** – Two server functions expose stored Lighthouse data through endpoints for issues viewing and CSV/JSON export.

## Step-by-Step Integration Flow

The following workflow executes whenever a site audit runs with Lighthouse enabled.

### Configuration and Strategy Selection

Every audit request contains a `lighthouseStrategy` field configured through `AuditConfig`. Valid strategies include `"none"`, `"all"`, `"auto"`, and custom variants defined in **[`src/server/lib/audit/types.ts`](https://github.com/every-app/open-seo/blob/main/src/server/lib/audit/types.ts)**.

```typescript
// src/server/lib/audit/types.ts
interface AuditConfig {
  maxPages: number;
  lighthouseStrategy: LighthouseStrategy; // "none" | "all" | "auto" | ...
  // ... other audit options
}

```

The workflow checks this flag before invoking any Lighthouse logic.

### Workflow Phase Execution

In **[`src/server/workflows/siteAuditWorkflowPhases.ts`](https://github.com/every-app/open-seo/blob/main/src/server/workflows/siteAuditWorkflowPhases.ts)**, the `runAuditPhases` function conditionally triggers `runLighthousePhase` **only** when `config.lighthouseStrategy !== "none"`:

```typescript
// Conditional execution based on strategy
if (config.lighthouseStrategy !== "none") {
  await runLighthousePhase(auditContext);
}

```

### Page Sampling and Selection

When Lighthouse is enabled, `selectLighthousePages` builds a targeted URL list using `selectLighthouseSample`. This sample includes the homepage plus one representative URL per template pattern to optimize API usage:

```typescript
// Page selection logic
const urlsToAudit = selectLighthouseSample(pages, startUrl, strategy);
// Returns: [homepage, templateA_instance, templateB_instance, ...]

```

### API Execution and Cloud Storage

For each selected URL, `fetchAndStoreLighthouseResult` executes mobile and desktop audits. This function in **[`src/server/lib/audit/lighthouse.ts`](https://github.com/every-app/open-seo/blob/main/src/server/lib/audit/lighthouse.ts)** performs two critical operations:

1. **API Call**: `fetchLighthouseResult` contacts DataforSEO's `lighthouse.live` endpoint with retry logic.
2. **Storage**: On success, `putTextToR2` writes the raw JSON payload to Cloudflare R2 and returns a storage key.

```typescript
// src/server/lib/audit/lighthouse.ts
export async function fetchAndStoreLighthouseResult(
  url: string,
  device: "mobile" | "desktop"
): Promise<LighthouseResult> {
  const payload = await fetchLighthouseResult(url, device);
  const r2Key = await putTextToR2(`lighthouse/${auditId}/${device}.json`, payload);
  
  return {
    url,
    device,
    r2Key,
    payloadSizeBytes: payload.length,
    // ... metadata
  };
}

```

### Data Persistence

The `LighthouseResult` objects (containing `r2Key` and `payloadSizeBytes` rather than full JSON) are persisted via `AuditRepository.batchWriteResults` alongside crawled page data in **[`siteAuditWorkflowPhases.ts`](https://github.com/every-app/open-seo/blob/main/siteAuditWorkflowPhases.ts)**.

### Server-Side API Endpoints

The UI accesses Lighthouse data through two server functions defined in **[`src/serverFunctions/lighthouse.ts`](https://github.com/every-app/open-seo/blob/main/src/serverFunctions/lighthouse.ts)**:

**Issues Retrieval**: `getAuditLighthouseIssues` loads the stored payload from R2 using `getJsonFromR2`, parses it with `readStoredLighthousePayload`, and returns a trimmed issue list:

```typescript
// src/serverFunctions/lighthouse.ts (lines 45-67)
export const getAuditLighthouseIssues = createServerFn({ method: "POST" })
  .handler(async ({ request }) => {
    const { resultId } = await request.json();
    const result = await auditRepository.findById(resultId);
    const payload = await getJsonFromR2(result.r2Key);
    return readStoredLighthousePayload(payload, { 
      filterCategories: ["performance", "accessibility"] 
    });
  });

```

**Export Endpoint**: `exportAuditLighthouseIssues` rebuilds export files on demand via `buildLighthouseExportFile`, supporting three modes: `"full"` (complete payload), `"issues"` (trimmed reports only), or `"category"` (filtered by category):

```typescript
// src/serverFunctions/lighthouse.ts (lines 69-88)
export const exportAuditLighthouseIssues = createServerFn({ method: "POST" })
  .handler(async ({ request }) => {
    const { resultId, mode, category } = await request.json();
    const result = await auditRepository.findById(resultId);
    const payload = await getJsonFromR2(result.r2Key);
    
    const { filename, content } = buildLighthouseExportFile(payload, {
      mode,
      category,
      auditDate: result.createdAt
    });
    
    return { filename, content };
  });

```

## Code Implementation Examples

### Starting a Site Audit with Lighthouse Enabled

Trigger a new audit with Lighthouse strategy set to `"auto"`:

```typescript
import { startSiteAudit } from "@/client/api";

await startSiteAudit({
  projectId: "proj_123",
  startUrl: "https://example.com",
  config: {
    maxPages: 500,
    lighthouseStrategy: "auto",  // Enables Lighthouse phase
    concurrency: 5
  },
});

```

Behind the scenes, this invokes the Cloudflare Worker running `runAuditPhases`, which detects the non-"none" strategy and executes the Lighthouse workflow.

### Retrieving Lighthouse Issues

Fetch parsed issues for a specific result ID:

```typescript
const response = await fetch("/api/lighthouse/issues", {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({ resultId: "lhr_abc123" }),
});

const { issues, scores } = await response.json();
console.log(issues); // Array of StoredLighthouseIssue objects

```

### Exporting Lighthouse Results

Request a full payload export or issues-only download:

```typescript
const exportResponse = await fetch("/api/lighthouse/export", {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({
    resultId: "lhr_abc123",
    mode: "full",              // Options: "full", "issues", "category"
    category: "performance"    // Required when mode === "category"
  }),
});

const { filename, content } = await exportResponse.json();
// Download: new Blob([content], { type: "application/json" })

```

### Customizing Page Selection Strategies

To implement a custom sampling strategy (e.g., "first-10-pages"), modify **[`src/server/lib/audit/lighthouse.ts`](https://github.com/every-app/open-seo/blob/main/src/server/lib/audit/lighthouse.ts)**:

```typescript
export function selectLighthouseSample(
  pages: LighthouseSamplePage[],
  startUrl: string,
  strategy: LighthouseStrategy,
): string[] {
  if (strategy === "first-10") {
    return pages.slice(0, 10).map(p => p.url);
  }
  // Existing logic for "auto", "all", etc.
  return defaultSampling(pages, startUrl);
}

```

Then extend `LighthouseStrategy` in **[`src/server/lib/audit/types.ts`](https://github.com/every-app/open-seo/blob/main/src/server/lib/audit/types.ts)** to expose the new option through the UI.

## Key Source Files and Responsibilities

| File | Responsibility |
|------|----------------|
| **[`src/server/workflows/siteAuditWorkflowPhases.ts`](https://github.com/every-app/open-seo/blob/main/src/server/workflows/siteAuditWorkflowPhases.ts)** | Orchestrates the overall audit flow; conditionally runs `runLighthousePhase` based on strategy configuration. |
| **[`src/server/lib/audit/lighthouse.ts`](https://github.com/every-app/open-seo/blob/main/src/server/lib/audit/lighthouse.ts)** | Wraps DataforSEO's Lighthouse API, handles retry logic, and manages R2 storage via `putTextToR2`. |
| **[`src/server/lib/lighthousePayload.ts`](https://github.com/every-app/open-seo/blob/main/src/server/lib/lighthousePayload.ts)** | Parses stored JSON payloads with `readStoredLighthousePayload` and generates export files via `buildLighthouseExportFile`. |
| **[`src/serverFunctions/lighthouse.ts`](https://github.com/every-app/open-seo/blob/main/src/serverFunctions/lighthouse.ts)** | Exposes public server functions: `getAuditLighthouseIssues` and `exportAuditLighthouseIssues`. |
| **[`src/server/lib/audit/types.ts`](https://github.com/every-app/open-seo/blob/main/src/server/lib/audit/types.ts)** | Defines TypeScript interfaces including `LighthouseResult`, `LighthouseStrategy`, and `AuditConfig`. |
| **[`src/shared/lighthouse.ts`](https://github.com/every-app/open-seo/blob/main/src/shared/lighthouse.ts)** | Contains shared constants like `LIGHTHOUSE_CATEGORIES` used across client and server. |

## Summary

- **OpenSEO Lighthouse integration** operates entirely server-side, keeping third-party API credentials and heavy JSON payloads away from the client.
- The workflow triggers only when `lighthouseStrategy` is not `"none"`, sampling pages via `selectLighthouseSample` before calling DataforSEO.
- Raw Lighthouse results are stored in **Cloudflare R2** via `putTextToR2`, while the database stores only lightweight metadata including the `r2Key`.
- **Two server functions**—`getAuditLighthouseIssues` and `exportAuditLighthouseIssues`—provide UI access to parsed issues and exportable data without exposing raw API responses.
- The payload parsing layer supports filtering by category and multiple export modes ("full", "issues", "category") while generating descriptive filenames that encode audit dates.

## Frequently Asked Questions

### How does OpenSEO decide which pages to audit with Lighthouse?

OpenSEO uses the `selectLighthouseSample` function in [`src/server/lib/audit/lighthouse.ts`](https://github.com/every-app/open-seo/blob/main/src/server/lib/audit/lighthouse.ts) to intelligently sample pages based on the configured `lighthouseStrategy`. For the `"auto"` strategy, it selects the homepage plus one representative URL from each unique page template pattern. This prevents redundant API calls on similar pages while ensuring comprehensive coverage of site structure.

### Where are the raw Lighthouse JSON payloads stored?

The system stores complete Lighthouse results in **Cloudflare R2** object storage using the `putTextToR2` utility. The database persists only a `LighthouseResult` record containing the `r2Key` (storage path), payload size, and metadata. This architecture keeps the database lightweight while preserving full audit data for on-demand retrieval and export.

### Can I export Lighthouse results in formats other than JSON?

Yes. The `exportAuditLighthouseIssues` server function supports three export modes via the `buildLighthouseExportFile` utility in [`src/server/lib/lighthousePayload.ts`](https://github.com/every-app/open-seo/blob/main/src/server/lib/lighthousePayload.ts). You can request `"full"` (complete raw payload), `"issues"` (trimmed report containing only violations and scores), or `"category"` (filtered to a specific Lighthouse category like "performance" or "accessibility"). The response includes both the formatted content and a descriptive filename.

### What happens if the DataforSEO Lighthouse API fails during an audit?

The `fetchLighthouseResult` function in [`src/server/lib/audit/lighthouse.ts`](https://github.com/every-app/open-seo/blob/main/src/server/lib/audit/lighthouse.ts) implements retry logic for transient failures. If the API ultimately fails for a specific URL, the error is logged and the audit continues processing other URLs. The failed attempt is recorded in the audit results, allowing the UI to display partial data and retry options without crashing the entire site audit workflow.