Skip to Content
BacklinksBacklinks — Data Model

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.


Table: backlink

FieldTypeNotes
idString (cuid)PK
tenantIdStringFK → Tenant (cascade delete)
sourceTypeStringoutreach | directory_opportunity | competitor_gap (default: outreach)
directoryIdString?FK → BacklinkDirectory; set for directory_opportunity rows
stepsJson?Copied from BacklinkDirectory.steps at creation; self-contained
sourceDomainStringDomain of the linking site
sourceUrlString?Contact/target page on the linking site; populated by website-crawler (outreach) or directory URL
targetUrlString?The client page being linked to
anchorTextString?Desired or actual anchor text
domainRatingInt?0–100; from Ahrefs/SEMrush (outreach) or BacklinkDirectory.domainRating (opportunity)
linkTypeStringdofollow | nofollow | ugc | sponsored (default: dofollow)
prospectTypeString?resource-page | guest-post | broken-link | competitor-backlink | mention — outreach only
relevanceScoreInt?1–10; set by researcher (outreach) or opportunity-matcher (opportunity/gap)
pitchRationaleString?Why this domain is a good prospect — outreach only
outreachStatusStringSee status lifecycle table below
outreachSentAtDateTime?When the outreach email was sent — outreach only
respondedAtDateTime?When the prospect replied — outreach only
agreedAtDateTime?When the prospect agreed to add the link — outreach only
publishedAtDateTime?When the link went live
completedAtDateTime?When the client marked the opportunity as completed — opportunity only
isLiveBoolean?null = not yet checked; populated by DataForSEO daily health check
isIndexedBoolean?Whether the linking page is indexed by Google
lastCheckedAtDateTime?Last DataForSEO health check timestamp
activityIdString?Activity that produced the outreach (no @relation — separate query)
campaignIdString?FK → Campaign (SetNull on delete) — outreach only
gapCompetitorsString[]Competitor domains already linked from this source — competitor_gap only
notesString?DM or client notes
createdAtDateTime
updatedAtDateTime

outreachStatus by sourceType:

sourceTypeStatus progression
outreachprospectingoutreach_sentrespondedagreedpublished (also: rejected, no_response)
directory_opportunityopportunityin_progresscompleted | not_applicable
competitor_gapopportunityin_progresscompleted | 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.

FieldTypeNotes
idString (cuid)PK
nameStringDisplay name, e.g. “HealthGrades”
urlStringSubmission/listing URL
categoryStringbusiness_directory | social_bookmarking | press_release | forum | review_platform | resource_site
subcategoryStringIndustry/topic subcategory (Healthcare, IT & SaaS, General, etc.)
domainRatingInt?0–100
linkTypeStringdofollow | nofollow (default: nofollow)
isFreeBooleanFree submission available
isPaidBooleanPaid tier available
paidPriceUsdInt?Approximate paid listing cost in USD
regionsString[]["IN", "US", "GLOBAL"] — applicable regions
difficultyStringeasy | medium | hard
estimatedMinutesInt?Approximate time to complete submission
stepsJsonOrdered { order, title, description, url? }[]
isActiveBooleanToggle visibility across all tenants (default: true)
createdAtDateTime
updatedAtDateTime

Indexes:

  • [category, subcategory] — agent filtering
  • [isActive]

Steps JSON shape:

type DirectoryStep = { order: number title: string description: string url?: string // direct link for this step }

Shared model. Only used for sourceType: "outreach" rows.

FieldValue
type"email_outreach"
statusdraftdm_reviewdm_approvedsent
totalEmailsCounter; decremented when a prospect is skipped

1:1 with Backlink (outreach only). Not created for opportunity/gap rows.

FieldNotes
statusdraftapprovedsent
subjectAgent-generated
bodyAgent-generated
recipientEmailFrom CrawlCache
recipientNameFrom CrawlCache

CrawlCache

Table: crawl_cache
Tenant-agnostic. Shared across all tenants. 14-day TTL. Used by the outreach pipeline only.

FieldTypeNotes
idString (cuid)PK
domainStringUnique
contactPageUrlString?
recipientEmailString?
recipientNameString?
rawExtractString?Full crawl output
isReachableBoolean
hasCaptchaBoolean
isBlockedBoolean
crawledAtDateTimeTTL 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)

© 2026 Leadmetrics — Internal use only