# OpenSEO Rank Tracking Architecture: A Deep Dive into the Layered Implementation

> Explore OpenSEO's five-layer rank tracking architecture. Understand its type-safe implementation separating UI, API, business logic, data, and automation via Cloudflare Workers.

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

---

**OpenSEO implements rank tracking as a five-layer, type-safe architecture separating UI components, API validation, business logic, data persistence, and scheduled automation through Cloudflare Workers.**

This article examines how the every-app/open-seo repository structures its rank tracking system, from the TanStack-powered frontend to the DataForSEO API integration. The architecture emphasizes clean separation of concerns with Zod validation, shared cost-calculation utilities, and Drizzle ORM for database operations.

## Architecture Overview: Five Distinct Layers

OpenSEO's rank tracking architecture follows a predictable data flow through five specialized layers. Each layer has a single responsibility and communicates through well-defined interfaces.

### Layer 1: Frontend and Routing

The user interface exposes rank tracking functionality through dedicated routes and feature navigation.

- **Route implementation**: [`web/src/routes/_marketing/features/rank-tracking.tsx`](https://github.com/every-app/open-seo/blob/main/web/src/routes/_marketing/features/rank-tracking.tsx) renders the main dashboard
- **Feature registration**: [`web/src/lib/feature-pages.ts`](https://github.com/every-app/open-seo/blob/main/web/src/lib/feature-pages.ts) registers "Rank Tracking" in the product navigation menu
- **Auto-generated routing**: [`web/src/routeTree.gen.ts`](https://github.com/every-app/open-seo/blob/main/web/src/routeTree.gen.ts) handles the `/features/rank-tracking` endpoint

This TanStack Router-based approach ensures type-safe links between the navigation and page components.

### Layer 2: API Validation with Zod

All incoming requests pass through strict schema validation before reaching business logic.

The schemas live in [`src/types/schemas/rank-tracking.ts`](https://github.com/every-app/open-seo/blob/main/src/types/schemas/rank-tracking.ts) and include:

- `createConfigSchema` — validates new tracking configurations
- `triggerCheckSchema` — validates rank check requests
- Additional schemas for config updates and metric queries

Server functions apply these validators via `createServerFn(...).validator(...)` pattern.

### Layer 3: Service Layer and Business Logic

The `RankTrackingService` class in [`src/server/features/rank-tracking/services/RankTrackingService.ts`](https://github.com/every-app/open-seo/blob/main/src/server/features/rank-tracking/services/RankTrackingService.ts) orchestrates all rank tracking operations. It coordinates:

- Configuration CRUD operations
- Live vs. queued check selection
- Credit limit enforcement
- Metric refresh scheduling

This service relies on shared utilities from [`src/shared/rank-tracking.ts`](https://github.com/every-app/open-seo/blob/main/src/shared/rank-tracking.ts) for calculations that must remain consistent between server and client.

### Layer 4: Repository and Data Access

Database operations are abstracted through `RankTrackingRepository` in [`src/server/features/rank-tracking/repositories/RankTrackingRepository.ts`](https://github.com/every-app/open-seo/blob/main/src/server/features/rank-tracking/repositories/RankTrackingRepository.ts). This Drizzle ORM wrapper handles:

- `rank_tracking_configs` table queries
- Keyword history retrieval
- Result matrix storage and lookup

The repository pattern allows the service layer to remain database-agnostic.

### Layer 5: Scheduler and Cloudflare Workers

Periodic execution uses Cloudflare Workers with calculated next-run times. The scheduler:

- Enqueues tasks via the `waitUntil` mechanism
- Respects user-selected intervals (`daily`, `weekly`, `monthly`)
- Handles drift compensation for delayed runs

## How a Rank Check Executes: Step-by-Step Flow

Understanding the complete request lifecycle reveals how these layers interact:

1. **User initiates request** — Frontend calls `triggerRankCheck` from [`src/serverFunctions/rank-tracking.ts`](https://github.com/every-app/open-seo/blob/main/src/serverFunctions/rank-tracking.ts)

2. **Validation gate** — Zod schema `triggerCheckSchema` parses the payload

3. **Service routing decision** — `RankTrackingService.triggerCheck` selects:
   - **Live endpoint** — Instant results, higher cost
   - **Queued endpoint** — Batched processing, lower cost

4. **Credit estimation** — `estimateRankCheckCredits` calculates cost based on:
   - Keyword count
   - Device selection (`desktop`, `mobile`, or `both`)
   - SERP depth (number of result pages)
   - Method (live vs. queued)

   Exceeding the approved limit throws `rankCheckCostApprovalError`

5. **External API call** — Service submits request to DataForSEO API

6. **Persistence** — Response stored via `RankTrackingRepository`

7. **Optional metric refresh** — `refreshKeywordMetrics` updates derived statistics

8. **UI query methods** — Frontend retrieves data through:
   - `getLatestRankResults`
   - `getRankKeywordHistory`
   - `getRankConfigTrend`
   - `getRankPositionMatrix`

## Shared Utilities: Cost and Scheduling Mathematics

The [`src/shared/rank-tracking.ts`](https://github.com/every-app/open-seo/blob/main/src/shared/rank-tracking.ts) file contains pure functions used across both frontend and backend:

### Credit Estimation Formula

```typescript
// From estimateRankCheckCredits in src/shared/rank-tracking.ts
function estimateRankCheckCredits(params: {
  keywordCount: number;
  devices: DeviceType[];
  serpDepth: number;
  method: 'live' | 'queued';
}): { costUsd: number; costCredits: number }

```

The calculation applies:
- `LIVE_BASE_PAGE_COST_USD` or `QUEUED_BASE_PAGE_COST_USD` per page
- Multiplication by keyword-device pair count
- Rounding with `SEO_DATA_COST_MARKUP` for credit conversion

### Next-Run Time Computation

```typescript
// From computeNextCheckAt in src/shared/rank-tracking.ts
function computeNextCheckAt(
  scheduleInterval: 'daily' | 'weekly' | 'monthly' | 'manual',
  fromDate: Date
): Date

```

Scheduling behavior:
- **Daily**: Adds 24 hours
- **Weekly**: Adds 7 days
- **Monthly**: Adds ~30 days with random hour selection (04:00–09:00 UTC) for load distribution
- **Manual**: Returns `null` (no automatic scheduling)

## Key Source Files Reference

| File Path | Purpose |
|-----------|---------|
| [`src/types/schemas/rank-tracking.ts`](https://github.com/every-app/open-seo/blob/main/src/types/schemas/rank-tracking.ts) | Zod validation schemas for all rank tracking endpoints |
| [`src/shared/rank-tracking.ts`](https://github.com/every-app/open-seo/blob/main/src/shared/rank-tracking.ts) | Cost constants, estimation functions, scheduling logic, display utilities |
| [`src/serverFunctions/rank-tracking.ts`](https://github.com/every-app/open-seo/blob/main/src/serverFunctions/rank-tracking.ts) | TanStack Server Function wrappers exposing API to frontend |
| [`src/server/features/rank-tracking/services/RankTrackingService.ts`](https://github.com/every-app/open-seo/blob/main/src/server/features/rank-tracking/services/RankTrackingService.ts) | Core business logic: configs, triggers, cost checks, metrics |
| [`src/server/features/rank-tracking/repositories/RankTrackingRepository.ts`](https://github.com/every-app/open-seo/blob/main/src/server/features/rank-tracking/repositories/RankTrackingRepository.ts) | Drizzle ORM data access for rank tracking tables |
| [`web/src/routes/_marketing/features/rank-tracking.tsx`](https://github.com/every-app/open-seo/blob/main/web/src/routes/_marketing/features/rank-tracking.tsx) | React-based rank tracking dashboard page |
| [`web/src/lib/feature-pages.ts`](https://github.com/every-app/open-seo/blob/main/web/src/lib/feature-pages.ts) | Navigation registration for "Rank Tracking" feature |
| [`web/src/routeTree.gen.ts`](https://github.com/every-app/open-seo/blob/main/web/src/routeTree.gen.ts) | Generated route tree including `/features/rank-tracking` |

## Design Decisions and Trade-offs

### Type Safety Through Zod

Using Zod schemas at the API boundary catches validation errors before they reach business logic. The `.validator()` integration with TanStack Server Functions provides end-to-end type safety from client to database.

### Shared Module Pattern

Placing `estimateRankCheckCredits` and `computeNextCheckAt` in `src/shared/` rather than server-only allows:
- Frontend cost previews before submission
- Consistent calculation logic across environments
- Reduced bundle overhead through tree-shaking

### Credit-First Architecture

Cost estimation runs before any external API call, preventing:
- Unexpected billing surprises
- Failed requests due to insufficient credits
- Manual refund workflows

The `rankCheckCostApprovalError` exception provides clear feedback when limits are exceeded.

## Summary

OpenSEO's rank tracking architecture demonstrates several effective patterns for SaaS feature development:

- **Five-layer separation** — UI, validation, service, repository, and scheduler each have distinct responsibilities
- **Type safety everywhere** — Zod schemas and TanStack Server Functions eliminate runtime validation failures
- **Cost transparency** — Shared estimation utilities provide accurate pricing before API calls execute
- **Cloud-native scheduling** — Cloudflare Workers with calculated next-run times handle automation without dedicated infrastructure
- **Repository abstraction** — Drizzle ORM queries remain isolated from business logic for database flexibility

## Frequently Asked Questions

### How does OpenSEO estimate rank check costs before execution?

`estimateRankCheckCredits` in [`src/shared/rank-tracking.ts`](https://github.com/every-app/open-seo/blob/main/src/shared/rank-tracking.ts) multiplies per-page DataForSEO pricing constants by the number of keyword-device pairs and SERP depth, then applies the `SEO_DATA_COST_MARKUP` multiplier. This runs on both client and server to enable pre-submission cost displays and server-side validation.

### What determines whether a rank check uses live or queued processing?

The `RankTrackingService.triggerCheck` method selects the endpoint based on cost and latency requirements. Live checks return immediately but cost more per request; queued checks batch processing for lower cost but require polling or webhook completion. The estimation function accounts for this in its cost calculation.

### How does the scheduler handle monthly intervals without date drift?

`computeNextCheckAt` uses calendar-aware date addition rather than fixed 30-day increments. For monthly schedules, it also randomizes the execution hour between 04:00 and 09:00 UTC to distribute server load across the user base rather than triggering all monthly checks simultaneously.

### Can the rank tracking system support additional search engines or devices?

The architecture supports extension through the `DeviceType` union and configuration schemas. Adding new device types requires updating [`src/types/schemas/rank-tracking.ts`](https://github.com/every-app/open-seo/blob/main/src/types/schemas/rank-tracking.ts), the shared device counting utilities, and corresponding DataForSEO API parameters. The repository and service layers remain unchanged as they operate on abstracted device identifiers.