Backlinks — Data Model
Backlink data lives across five models. Backlink is the unified core entity for both outreach and self-serve opportunities. BacklinkDirectory is the admin-managed platform database. Campaign and CampaignEmail are shared with other campaign types. CrawlCache is backlink-specific and tenant-agnostic.
Backlink
Table: backlink
| Field | Type | Notes |
|---|---|---|
id | String (cuid) | PK |
tenantId | String | FK → Tenant (cascade delete) |
sourceType | String | outreach | directory_opportunity | competitor_gap (default: outreach) |
directoryId | String? | FK → BacklinkDirectory; set for directory_opportunity rows |
steps | Json? | Copied from BacklinkDirectory.steps at creation; self-contained |
sourceDomain | String | Domain of the linking site |
sourceUrl | String? | Contact/target page on the linking site; populated by website-crawler (outreach) or directory URL |
targetUrl | String? | The client page being linked to |
anchorText | String? | Desired or actual anchor text |
domainRating | Int? | 0–100; from Ahrefs/SEMrush (outreach) or BacklinkDirectory.domainRating (opportunity) |
linkType | String | dofollow | nofollow | ugc | sponsored (default: dofollow) |
prospectType | String? | resource-page | guest-post | broken-link | competitor-backlink | mention — outreach only |
relevanceScore | Int? | 1–10; set by researcher (outreach) or opportunity-matcher (opportunity/gap) |
pitchRationale | String? | Why this domain is a good prospect — outreach only |
outreachStatus | String | See status lifecycle table below |
outreachSentAt | DateTime? | When the outreach email was sent — outreach only |
respondedAt | DateTime? | When the prospect replied — outreach only |
agreedAt | DateTime? | When the prospect agreed to add the link — outreach only |
publishedAt | DateTime? | When the link went live |
completedAt | DateTime? | When the client marked the opportunity as completed — opportunity only |
isLive | Boolean? | null = not yet checked; populated by DataForSEO daily health check |
isIndexed | Boolean? | Whether the linking page is indexed by Google |
lastCheckedAt | DateTime? | Last DataForSEO health check timestamp |
activityId | String? | Activity that produced the outreach (no @relation — separate query) |
campaignId | String? | FK → Campaign (SetNull on delete) — outreach only |
gapCompetitors | String[] | Competitor domains already linked from this source — competitor_gap only |
notes | String? | DM or client notes |
createdAt | DateTime | |
updatedAt | DateTime |
outreachStatus by sourceType:
sourceType | Status progression |
|---|---|
outreach | prospecting → outreach_sent → responded → agreed → published (also: rejected, no_response) |
directory_opportunity | opportunity → in_progress → completed | not_applicable |
competitor_gap | opportunity → in_progress → completed | not_applicable |
Indexes:
[tenantId, outreachStatus]— status-filtered list queries[tenantId, sourceType]— type filter on list page[tenantId, createdAt DESC]— default list sort[tenantId, isLive, lastCheckedAt]— daily health check sweep
Relations:
tenant— Tenant (required)directory— BacklinkDirectory (optional)campaign— Campaign (optional)campaignEmail— CampaignEmail (1:1, optional)
BacklinkDirectory
Table: backlink_directory
Platform-wide. Managed by superadmins in the Manage portal. Not tenant-scoped.
| Field | Type | Notes |
|---|---|---|
id | String (cuid) | PK |
name | String | Display name, e.g. “HealthGrades” |
url | String | Submission/listing URL |
category | String | business_directory | social_bookmarking | press_release | forum | review_platform | resource_site |
subcategory | String | Industry/topic subcategory (Healthcare, IT & SaaS, General, etc.) |
domainRating | Int? | 0–100 |
linkType | String | dofollow | nofollow (default: nofollow) |
isFree | Boolean | Free submission available |
isPaid | Boolean | Paid tier available |
paidPriceUsd | Int? | Approximate paid listing cost in USD |
regions | String[] | ["IN", "US", "GLOBAL"] — applicable regions |
difficulty | String | easy | medium | hard |
estimatedMinutes | Int? | Approximate time to complete submission |
steps | Json | Ordered { order, title, description, url? }[] |
isActive | Boolean | Toggle visibility across all tenants (default: true) |
createdAt | DateTime | |
updatedAt | DateTime |
Indexes:
[category, subcategory]— agent filtering[isActive]
Steps JSON shape:
type DirectoryStep = {
order: number
title: string
description: string
url?: string // direct link for this step
}Campaign (backlink context)
Shared model. Only used for sourceType: "outreach" rows.
| Field | Value |
|---|---|
type | "email_outreach" |
status | draft → dm_review → dm_approved → sent |
totalEmails | Counter; decremented when a prospect is skipped |
CampaignEmail (backlink context)
1:1 with Backlink (outreach only). Not created for opportunity/gap rows.
| Field | Notes |
|---|---|
status | draft → approved → sent |
subject | Agent-generated |
body | Agent-generated |
recipientEmail | From CrawlCache |
recipientName | From CrawlCache |
CrawlCache
Table: crawl_cache
Tenant-agnostic. Shared across all tenants. 14-day TTL. Used by the outreach pipeline only.
| Field | Type | Notes |
|---|---|---|
id | String (cuid) | PK |
domain | String | Unique |
contactPageUrl | String? | |
recipientEmail | String? | |
recipientName | String? | |
rawExtract | String? | Full crawl output |
isReachable | Boolean | |
hasCaptcha | Boolean | |
isBlocked | Boolean | |
crawledAt | DateTime | TTL reference |
Entity Relationships
Platform (superadmin)
└── BacklinkDirectory (many, tenant-agnostic)
Tenant
├── Backlink (sourceType: outreach)
│ ├── Campaign
│ │ └── CampaignEmail (1:1 per Backlink)
│ └── CrawlCache (shared, by domain)
│
├── Backlink (sourceType: directory_opportunity)
│ └── BacklinkDirectory (FK; steps copied at creation)
│
└── Backlink (sourceType: competitor_gap)
└── gapCompetitors[] (which competitors link here)