OpenSEO Rank Tracking Architecture: A Deep Dive into the Layered Implementation
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.tsxrenders the main dashboard - Feature registration:
web/src/lib/feature-pages.tsregisters "Rank Tracking" in the product navigation menu - Auto-generated routing:
web/src/routeTree.gen.tshandles the/features/rank-trackingendpoint
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 and include:
createConfigSchema— validates new tracking configurationstriggerCheckSchema— 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 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 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. This Drizzle ORM wrapper handles:
rank_tracking_configstable 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
waitUntilmechanism - 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:
-
User initiates request — Frontend calls
triggerRankCheckfromsrc/serverFunctions/rank-tracking.ts -
Validation gate — Zod schema
triggerCheckSchemaparses the payload -
Service routing decision —
RankTrackingService.triggerCheckselects:- Live endpoint — Instant results, higher cost
- Queued endpoint — Batched processing, lower cost
-
Credit estimation —
estimateRankCheckCreditscalculates cost based on:- Keyword count
- Device selection (
desktop,mobile, orboth) - SERP depth (number of result pages)
- Method (live vs. queued)
Exceeding the approved limit throws
rankCheckCostApprovalError -
External API call — Service submits request to DataForSEO API
-
Persistence — Response stored via
RankTrackingRepository -
Optional metric refresh —
refreshKeywordMetricsupdates derived statistics -
UI query methods — Frontend retrieves data through:
getLatestRankResultsgetRankKeywordHistorygetRankConfigTrendgetRankPositionMatrix
Shared Utilities: Cost and Scheduling Mathematics
The src/shared/rank-tracking.ts file contains pure functions used across both frontend and backend:
Credit Estimation Formula
// 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_USDorQUEUED_BASE_PAGE_COST_USDper page- Multiplication by keyword-device pair count
- Rounding with
SEO_DATA_COST_MARKUPfor credit conversion
Next-Run Time Computation
// 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 |
Zod validation schemas for all rank tracking endpoints |
src/shared/rank-tracking.ts |
Cost constants, estimation functions, scheduling logic, display utilities |
src/serverFunctions/rank-tracking.ts |
TanStack Server Function wrappers exposing API to frontend |
src/server/features/rank-tracking/services/RankTrackingService.ts |
Core business logic: configs, triggers, cost checks, metrics |
src/server/features/rank-tracking/repositories/RankTrackingRepository.ts |
Drizzle ORM data access for rank tracking tables |
web/src/routes/_marketing/features/rank-tracking.tsx |
React-based rank tracking dashboard page |
web/src/lib/feature-pages.ts |
Navigation registration for "Rank Tracking" feature |
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 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, the shared device counting utilities, and corresponding DataForSEO API parameters. The repository and service layers remain unchanged as they operate on abstracted device identifiers.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →